|
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() |