1 | #! /usr/bin/env python
|
---|
2 |
|
---|
3 | """Remote CVS -- command line interface"""
|
---|
4 |
|
---|
5 | # XXX To do:
|
---|
6 | #
|
---|
7 | # Bugs:
|
---|
8 | # - if the remote file is deleted, "rcvs update" will fail
|
---|
9 | #
|
---|
10 | # Functionality:
|
---|
11 | # - cvs rm
|
---|
12 | # - descend into directories (alraedy done for update)
|
---|
13 | # - conflict resolution
|
---|
14 | # - other relevant commands?
|
---|
15 | # - branches
|
---|
16 | #
|
---|
17 | # - Finesses:
|
---|
18 | # - retain file mode's x bits
|
---|
19 | # - complain when "nothing known about filename"
|
---|
20 | # - edit log message the way CVS lets you edit it
|
---|
21 | # - cvs diff -rREVA -rREVB
|
---|
22 | # - send mail the way CVS sends it
|
---|
23 | #
|
---|
24 | # Performance:
|
---|
25 | # - cache remote checksums (for every revision ever seen!)
|
---|
26 | # - translate symbolic revisions to numeric revisions
|
---|
27 | #
|
---|
28 | # Reliability:
|
---|
29 | # - remote locking
|
---|
30 | #
|
---|
31 | # Security:
|
---|
32 | # - Authenticated RPC?
|
---|
33 |
|
---|
34 |
|
---|
35 | from cvslib import CVS, File
|
---|
36 | import md5
|
---|
37 | import os
|
---|
38 | import string
|
---|
39 | import sys
|
---|
40 | from cmdfw import CommandFrameWork
|
---|
41 |
|
---|
42 |
|
---|
43 | DEF_LOCAL = 1 # Default -l
|
---|
44 |
|
---|
45 |
|
---|
46 | class MyFile(File):
|
---|
47 |
|
---|
48 | def action(self):
|
---|
49 | """Return a code indicating the update status of this file.
|
---|
50 |
|
---|
51 | The possible return values are:
|
---|
52 |
|
---|
53 | '=' -- everything's fine
|
---|
54 | '0' -- file doesn't exist anywhere
|
---|
55 | '?' -- exists locally only
|
---|
56 | 'A' -- new locally
|
---|
57 | 'R' -- deleted locally
|
---|
58 | 'U' -- changed remotely, no changes locally
|
---|
59 | (includes new remotely or deleted remotely)
|
---|
60 | 'M' -- changed locally, no changes remotely
|
---|
61 | 'C' -- conflict: changed locally as well as remotely
|
---|
62 | (includes cases where the file has been added
|
---|
63 | or removed locally and remotely)
|
---|
64 | 'D' -- deleted remotely
|
---|
65 | 'N' -- new remotely
|
---|
66 | 'r' -- get rid of entry
|
---|
67 | 'c' -- create entry
|
---|
68 | 'u' -- update entry
|
---|
69 |
|
---|
70 | (and probably others :-)
|
---|
71 | """
|
---|
72 | if not self.lseen:
|
---|
73 | self.getlocal()
|
---|
74 | if not self.rseen:
|
---|
75 | self.getremote()
|
---|
76 | if not self.eseen:
|
---|
77 | if not self.lsum:
|
---|
78 | if not self.rsum: return '0' # Never heard of
|
---|
79 | else:
|
---|
80 | return 'N' # New remotely
|
---|
81 | else: # self.lsum
|
---|
82 | if not self.rsum: return '?' # Local only
|
---|
83 | # Local and remote, but no entry
|
---|
84 | if self.lsum == self.rsum:
|
---|
85 | return 'c' # Restore entry only
|
---|
86 | else: return 'C' # Real conflict
|
---|
87 | else: # self.eseen
|
---|
88 | if not self.lsum:
|
---|
89 | if self.edeleted:
|
---|
90 | if self.rsum: return 'R' # Removed
|
---|
91 | else: return 'r' # Get rid of entry
|
---|
92 | else: # not self.edeleted
|
---|
93 | if self.rsum:
|
---|
94 | print "warning:",
|
---|
95 | print self.file,
|
---|
96 | print "was lost"
|
---|
97 | return 'U'
|
---|
98 | else: return 'r' # Get rid of entry
|
---|
99 | else: # self.lsum
|
---|
100 | if not self.rsum:
|
---|
101 | if self.enew: return 'A' # New locally
|
---|
102 | else: return 'D' # Deleted remotely
|
---|
103 | else: # self.rsum
|
---|
104 | if self.enew:
|
---|
105 | if self.lsum == self.rsum:
|
---|
106 | return 'u'
|
---|
107 | else:
|
---|
108 | return 'C'
|
---|
109 | if self.lsum == self.esum:
|
---|
110 | if self.esum == self.rsum:
|
---|
111 | return '='
|
---|
112 | else:
|
---|
113 | return 'U'
|
---|
114 | elif self.esum == self.rsum:
|
---|
115 | return 'M'
|
---|
116 | elif self.lsum == self.rsum:
|
---|
117 | return 'u'
|
---|
118 | else:
|
---|
119 | return 'C'
|
---|
120 |
|
---|
121 | def update(self):
|
---|
122 | code = self.action()
|
---|
123 | if code == '=': return
|
---|
124 | print code, self.file
|
---|
125 | if code in ('U', 'N'):
|
---|
126 | self.get()
|
---|
127 | elif code == 'C':
|
---|
128 | print "%s: conflict resolution not yet implemented" % \
|
---|
129 | self.file
|
---|
130 | elif code == 'D':
|
---|
131 | remove(self.file)
|
---|
132 | self.eseen = 0
|
---|
133 | elif code == 'r':
|
---|
134 | self.eseen = 0
|
---|
135 | elif code in ('c', 'u'):
|
---|
136 | self.eseen = 1
|
---|
137 | self.erev = self.rrev
|
---|
138 | self.enew = 0
|
---|
139 | self.edeleted = 0
|
---|
140 | self.esum = self.rsum
|
---|
141 | self.emtime, self.ectime = os.stat(self.file)[-2:]
|
---|
142 | self.extra = ''
|
---|
143 |
|
---|
144 | def commit(self, message = ""):
|
---|
145 | code = self.action()
|
---|
146 | if code in ('A', 'M'):
|
---|
147 | self.put(message)
|
---|
148 | return 1
|
---|
149 | elif code == 'R':
|
---|
150 | print "%s: committing removes not yet implemented" % \
|
---|
151 | self.file
|
---|
152 | elif code == 'C':
|
---|
153 | print "%s: conflict resolution not yet implemented" % \
|
---|
154 | self.file
|
---|
155 |
|
---|
156 | def diff(self, opts = []):
|
---|
157 | self.action() # To update lseen, rseen
|
---|
158 | flags = ''
|
---|
159 | rev = self.rrev
|
---|
160 | # XXX should support two rev options too!
|
---|
161 | for o, a in opts:
|
---|
162 | if o == '-r':
|
---|
163 | rev = a
|
---|
164 | else:
|
---|
165 | flags = flags + ' ' + o + a
|
---|
166 | if rev == self.rrev and self.lsum == self.rsum:
|
---|
167 | return
|
---|
168 | flags = flags[1:]
|
---|
169 | fn = self.file
|
---|
170 | data = self.proxy.get((fn, rev))
|
---|
171 | sum = md5.new(data).digest()
|
---|
172 | if self.lsum == sum:
|
---|
173 | return
|
---|
174 | import tempfile
|
---|
175 | tf = tempfile.NamedTemporaryFile()
|
---|
176 | tf.write(data)
|
---|
177 | tf.flush()
|
---|
178 | print 'diff %s -r%s %s' % (flags, rev, fn)
|
---|
179 | sts = os.system('diff %s %s %s' % (flags, tf.name, fn))
|
---|
180 | if sts:
|
---|
181 | print '='*70
|
---|
182 |
|
---|
183 | def commitcheck(self):
|
---|
184 | return self.action() != 'C'
|
---|
185 |
|
---|
186 | def put(self, message = ""):
|
---|
187 | print "Checking in", self.file, "..."
|
---|
188 | data = open(self.file).read()
|
---|
189 | if not self.enew:
|
---|
190 | self.proxy.lock(self.file)
|
---|
191 | messages = self.proxy.put(self.file, data, message)
|
---|
192 | if messages:
|
---|
193 | print messages
|
---|
194 | self.setentry(self.proxy.head(self.file), self.lsum)
|
---|
195 |
|
---|
196 | def get(self):
|
---|
197 | data = self.proxy.get(self.file)
|
---|
198 | f = open(self.file, 'w')
|
---|
199 | f.write(data)
|
---|
200 | f.close()
|
---|
201 | self.setentry(self.rrev, self.rsum)
|
---|
202 |
|
---|
203 | def log(self, otherflags):
|
---|
204 | print self.proxy.log(self.file, otherflags)
|
---|
205 |
|
---|
206 | def add(self):
|
---|
207 | self.eseen = 0 # While we're hacking...
|
---|
208 | self.esum = self.lsum
|
---|
209 | self.emtime, self.ectime = 0, 0
|
---|
210 | self.erev = ''
|
---|
211 | self.enew = 1
|
---|
212 | self.edeleted = 0
|
---|
213 | self.eseen = 1 # Done
|
---|
214 | self.extra = ''
|
---|
215 |
|
---|
216 | def setentry(self, erev, esum):
|
---|
217 | self.eseen = 0 # While we're hacking...
|
---|
218 | self.esum = esum
|
---|
219 | self.emtime, self.ectime = os.stat(self.file)[-2:]
|
---|
220 | self.erev = erev
|
---|
221 | self.enew = 0
|
---|
222 | self.edeleted = 0
|
---|
223 | self.eseen = 1 # Done
|
---|
224 | self.extra = ''
|
---|
225 |
|
---|
226 |
|
---|
227 | SENDMAIL = "/usr/lib/sendmail -t"
|
---|
228 | MAILFORM = """To: %s
|
---|
229 | Subject: CVS changes: %s
|
---|
230 |
|
---|
231 | ...Message from rcvs...
|
---|
232 |
|
---|
233 | Committed files:
|
---|
234 | %s
|
---|
235 |
|
---|
236 | Log message:
|
---|
237 | %s
|
---|
238 | """
|
---|
239 |
|
---|
240 |
|
---|
241 | class RCVS(CVS):
|
---|
242 |
|
---|
243 | FileClass = MyFile
|
---|
244 |
|
---|
245 | def __init__(self):
|
---|
246 | CVS.__init__(self)
|
---|
247 |
|
---|
248 | def update(self, files):
|
---|
249 | for e in self.whichentries(files, 1):
|
---|
250 | e.update()
|
---|
251 |
|
---|
252 | def commit(self, files, message = ""):
|
---|
253 | list = self.whichentries(files)
|
---|
254 | if not list: return
|
---|
255 | ok = 1
|
---|
256 | for e in list:
|
---|
257 | if not e.commitcheck():
|
---|
258 | ok = 0
|
---|
259 | if not ok:
|
---|
260 | print "correct above errors first"
|
---|
261 | return
|
---|
262 | if not message:
|
---|
263 | message = raw_input("One-liner: ")
|
---|
264 | committed = []
|
---|
265 | for e in list:
|
---|
266 | if e.commit(message):
|
---|
267 | committed.append(e.file)
|
---|
268 | self.mailinfo(committed, message)
|
---|
269 |
|
---|
270 | def mailinfo(self, files, message = ""):
|
---|
271 | towhom = "sjoerd@cwi.nl, jack@cwi.nl" # XXX
|
---|
272 | mailtext = MAILFORM % (towhom, string.join(files),
|
---|
273 | string.join(files), message)
|
---|
274 | print '-'*70
|
---|
275 | print mailtext
|
---|
276 | print '-'*70
|
---|
277 | ok = raw_input("OK to mail to %s? " % towhom)
|
---|
278 | if string.lower(string.strip(ok)) in ('y', 'ye', 'yes'):
|
---|
279 | p = os.popen(SENDMAIL, "w")
|
---|
280 | p.write(mailtext)
|
---|
281 | sts = p.close()
|
---|
282 | if sts:
|
---|
283 | print "Sendmail exit status %s" % str(sts)
|
---|
284 | else:
|
---|
285 | print "Mail sent."
|
---|
286 | else:
|
---|
287 | print "No mail sent."
|
---|
288 |
|
---|
289 | def report(self, files):
|
---|
290 | for e in self.whichentries(files):
|
---|
291 | e.report()
|
---|
292 |
|
---|
293 | def diff(self, files, opts):
|
---|
294 | for e in self.whichentries(files):
|
---|
295 | e.diff(opts)
|
---|
296 |
|
---|
297 | def add(self, files):
|
---|
298 | if not files:
|
---|
299 | raise RuntimeError, "'cvs add' needs at least one file"
|
---|
300 | list = []
|
---|
301 | for e in self.whichentries(files, 1):
|
---|
302 | e.add()
|
---|
303 |
|
---|
304 | def rm(self, files):
|
---|
305 | if not files:
|
---|
306 | raise RuntimeError, "'cvs rm' needs at least one file"
|
---|
307 | raise RuntimeError, "'cvs rm' not yet imlemented"
|
---|
308 |
|
---|
309 | def log(self, files, opts):
|
---|
310 | flags = ''
|
---|
311 | for o, a in opts:
|
---|
312 | flags = flags + ' ' + o + a
|
---|
313 | for e in self.whichentries(files):
|
---|
314 | e.log(flags)
|
---|
315 |
|
---|
316 | def whichentries(self, files, localfilestoo = 0):
|
---|
317 | if files:
|
---|
318 | list = []
|
---|
319 | for file in files:
|
---|
320 | if self.entries.has_key(file):
|
---|
321 | e = self.entries[file]
|
---|
322 | else:
|
---|
323 | e = self.FileClass(file)
|
---|
324 | self.entries[file] = e
|
---|
325 | list.append(e)
|
---|
326 | else:
|
---|
327 | list = self.entries.values()
|
---|
328 | for file in self.proxy.listfiles():
|
---|
329 | if self.entries.has_key(file):
|
---|
330 | continue
|
---|
331 | e = self.FileClass(file)
|
---|
332 | self.entries[file] = e
|
---|
333 | list.append(e)
|
---|
334 | if localfilestoo:
|
---|
335 | for file in os.listdir(os.curdir):
|
---|
336 | if not self.entries.has_key(file) \
|
---|
337 | and not self.ignored(file):
|
---|
338 | e = self.FileClass(file)
|
---|
339 | self.entries[file] = e
|
---|
340 | list.append(e)
|
---|
341 | list.sort()
|
---|
342 | if self.proxy:
|
---|
343 | for e in list:
|
---|
344 | if e.proxy is None:
|
---|
345 | e.proxy = self.proxy
|
---|
346 | return list
|
---|
347 |
|
---|
348 |
|
---|
349 | class rcvs(CommandFrameWork):
|
---|
350 |
|
---|
351 | GlobalFlags = 'd:h:p:qvL'
|
---|
352 | UsageMessage = \
|
---|
353 | "usage: rcvs [-d directory] [-h host] [-p port] [-q] [-v] [subcommand arg ...]"
|
---|
354 | PostUsageMessage = \
|
---|
355 | "If no subcommand is given, the status of all files is listed"
|
---|
356 |
|
---|
357 | def __init__(self):
|
---|
358 | """Constructor."""
|
---|
359 | CommandFrameWork.__init__(self)
|
---|
360 | self.proxy = None
|
---|
361 | self.cvs = RCVS()
|
---|
362 |
|
---|
363 | def close(self):
|
---|
364 | if self.proxy:
|
---|
365 | self.proxy._close()
|
---|
366 | self.proxy = None
|
---|
367 |
|
---|
368 | def recurse(self):
|
---|
369 | self.close()
|
---|
370 | names = os.listdir(os.curdir)
|
---|
371 | for name in names:
|
---|
372 | if name == os.curdir or name == os.pardir:
|
---|
373 | continue
|
---|
374 | if name == "CVS":
|
---|
375 | continue
|
---|
376 | if not os.path.isdir(name):
|
---|
377 | continue
|
---|
378 | if os.path.islink(name):
|
---|
379 | continue
|
---|
380 | print "--- entering subdirectory", name, "---"
|
---|
381 | os.chdir(name)
|
---|
382 | try:
|
---|
383 | if os.path.isdir("CVS"):
|
---|
384 | self.__class__().run()
|
---|
385 | else:
|
---|
386 | self.recurse()
|
---|
387 | finally:
|
---|
388 | os.chdir(os.pardir)
|
---|
389 | print "--- left subdirectory", name, "---"
|
---|
390 |
|
---|
391 | def options(self, opts):
|
---|
392 | self.opts = opts
|
---|
393 |
|
---|
394 | def ready(self):
|
---|
395 | import rcsclient
|
---|
396 | self.proxy = rcsclient.openrcsclient(self.opts)
|
---|
397 | self.cvs.setproxy(self.proxy)
|
---|
398 | self.cvs.getentries()
|
---|
399 |
|
---|
400 | def default(self):
|
---|
401 | self.cvs.report([])
|
---|
402 |
|
---|
403 | def do_report(self, opts, files):
|
---|
404 | self.cvs.report(files)
|
---|
405 |
|
---|
406 | def do_update(self, opts, files):
|
---|
407 | """update [-l] [-R] [file] ..."""
|
---|
408 | local = DEF_LOCAL
|
---|
409 | for o, a in opts:
|
---|
410 | if o == '-l': local = 1
|
---|
411 | if o == '-R': local = 0
|
---|
412 | self.cvs.update(files)
|
---|
413 | self.cvs.putentries()
|
---|
414 | if not local and not files:
|
---|
415 | self.recurse()
|
---|
416 | flags_update = '-lR'
|
---|
417 | do_up = do_update
|
---|
418 | flags_up = flags_update
|
---|
419 |
|
---|
420 | def do_commit(self, opts, files):
|
---|
421 | """commit [-m message] [file] ..."""
|
---|
422 | message = ""
|
---|
423 | for o, a in opts:
|
---|
424 | if o == '-m': message = a
|
---|
425 | self.cvs.commit(files, message)
|
---|
426 | self.cvs.putentries()
|
---|
427 | flags_commit = 'm:'
|
---|
428 | do_com = do_commit
|
---|
429 | flags_com = flags_commit
|
---|
430 |
|
---|
431 | def do_diff(self, opts, files):
|
---|
432 | """diff [difflags] [file] ..."""
|
---|
433 | self.cvs.diff(files, opts)
|
---|
434 | flags_diff = 'cbitwcefhnlr:sD:S:'
|
---|
435 | do_dif = do_diff
|
---|
436 | flags_dif = flags_diff
|
---|
437 |
|
---|
438 | def do_add(self, opts, files):
|
---|
439 | """add file ..."""
|
---|
440 | if not files:
|
---|
441 | print "'rcvs add' requires at least one file"
|
---|
442 | return
|
---|
443 | self.cvs.add(files)
|
---|
444 | self.cvs.putentries()
|
---|
445 |
|
---|
446 | def do_remove(self, opts, files):
|
---|
447 | """remove file ..."""
|
---|
448 | if not files:
|
---|
449 | print "'rcvs remove' requires at least one file"
|
---|
450 | return
|
---|
451 | self.cvs.remove(files)
|
---|
452 | self.cvs.putentries()
|
---|
453 | do_rm = do_remove
|
---|
454 |
|
---|
455 | def do_log(self, opts, files):
|
---|
456 | """log [rlog-options] [file] ..."""
|
---|
457 | self.cvs.log(files, opts)
|
---|
458 | flags_log = 'bhLNRtd:s:V:r:'
|
---|
459 |
|
---|
460 |
|
---|
461 | def remove(fn):
|
---|
462 | try:
|
---|
463 | os.unlink(fn)
|
---|
464 | except os.error:
|
---|
465 | pass
|
---|
466 |
|
---|
467 |
|
---|
468 | def main():
|
---|
469 | r = rcvs()
|
---|
470 | try:
|
---|
471 | r.run()
|
---|
472 | finally:
|
---|
473 | r.close()
|
---|
474 |
|
---|
475 |
|
---|
476 | if __name__ == "__main__":
|
---|
477 | main()
|
---|