blob: 5a053821219089299553ed1906e18da44dbea799 [file] [log] [blame]
Guido van Rossum097c55a1995-04-27 18:07:07 +00001#! /usr/local/bin/python
2
Guido van Rossumc218a7e1995-04-27 23:33:43 +00003"""Remote CVS -- command line interface"""
Guido van Rossum097c55a1995-04-27 18:07:07 +00004
Guido van Rossumd1972af1995-06-21 01:02:06 +00005# XXX To do:
6#
Guido van Rossum903afd01995-06-23 22:33:57 +00007# Bugs:
8# - if the remote file is deleted, "rcvs update" will fail
9#
Guido van Rossumd1972af1995-06-21 01:02:06 +000010# Functionality:
Guido van Rossumd1972af1995-06-21 01:02:06 +000011# - cvs log
Guido van Rossuma79f5a31995-07-18 18:34:34 +000012# - cvs rm
13# - descend into directories (alraedy done for update)
14# - conflict resolution
Guido van Rossumd1972af1995-06-21 01:02:06 +000015# - other relevant commands?
16# - branches
17#
18# - Finesses:
19# - retain file mode's x bits
20# - complain when "nothing known about filename"
21# - edit log message the way CVS lets you edit it
22# - cvs diff -rREVA -rREVB
23# - send mail the way CVS sends it
24#
25# Performance:
26# - cache remote checksums (for every revision ever seen!)
27# - translate symbolic revisions to numeric revisions
28#
29# Reliability:
30# - remote locking
31#
32# Security:
33# - Authenticated RPC?
34
35
Guido van Rossumc218a7e1995-04-27 23:33:43 +000036from cvslib import CVS, File
Guido van Rossum5f07b841995-04-26 22:57:11 +000037import md5
38import os
39import string
40import sys
Guido van Rossumc218a7e1995-04-27 23:33:43 +000041from cmdfw import CommandFrameWork
Guido van Rossum5f07b841995-04-26 22:57:11 +000042
43
Guido van Rossumd1972af1995-06-21 01:02:06 +000044DEF_LOCAL = 1 # Default -l
45
46
Guido van Rossumc218a7e1995-04-27 23:33:43 +000047class MyFile(File):
Guido van Rossum5f07b841995-04-26 22:57:11 +000048
Guido van Rossumae21ced1995-04-28 14:32:26 +000049 def action(self):
50 """Return a code indicating the update status of this file.
51
52 The possible return values are:
Guido van Rossumc218a7e1995-04-27 23:33:43 +000053
Guido van Rossumae21ced1995-04-28 14:32:26 +000054 '=' -- everything's fine
55 '0' -- file doesn't exist anywhere
56 '?' -- exists locally only
57 'A' -- new locally
58 'R' -- deleted locally
59 'U' -- changed remotely, no changes locally
Guido van Rossum6bb4a511995-04-28 15:26:37 +000060 (includes new remotely or deleted remotely)
Guido van Rossumae21ced1995-04-28 14:32:26 +000061 'M' -- changed locally, no changes remotely
62 'C' -- conflict: changed locally as well as remotely
63 (includes cases where the file has been added
64 or removed locally and remotely)
Guido van Rossumba244681995-04-28 15:33:03 +000065 'D' -- deleted remotely
66 'N' -- new remotely
Guido van Rossum6bb4a511995-04-28 15:26:37 +000067 'r' -- get rid of entry
68 'c' -- create entry
69 'u' -- update entry
Guido van Rossum2f7ef911995-05-01 20:22:01 +000070
71 (and probably others :-)
Guido van Rossumae21ced1995-04-28 14:32:26 +000072 """
Guido van Rossumec8cfd41995-05-01 20:06:44 +000073 if not self.lseen:
74 self.getlocal()
75 if not self.rseen:
76 self.getremote()
Guido van Rossumae21ced1995-04-28 14:32:26 +000077 if not self.eseen:
Guido van Rossum2f7ef911995-05-01 20:22:01 +000078 if not self.lsum:
79 if not self.rsum: return '0' # Never heard of
Guido van Rossum6bb4a511995-04-28 15:26:37 +000080 else:
81 return 'N' # New remotely
Guido van Rossum2f7ef911995-05-01 20:22:01 +000082 else: # self.lsum
83 if not self.rsum: return '?' # Local only
Guido van Rossum6bb4a511995-04-28 15:26:37 +000084 # Local and remote, but no entry
85 if self.lsum == self.rsum:
86 return 'c' # Restore entry only
87 else: return 'C' # Real conflict
88 else: # self.eseen
Guido van Rossum2f7ef911995-05-01 20:22:01 +000089 if not self.lsum:
90 if self.edeleted:
91 if self.rsum: return 'R' # Removed
Guido van Rossum6bb4a511995-04-28 15:26:37 +000092 else: return 'r' # Get rid of entry
Guido van Rossum2f7ef911995-05-01 20:22:01 +000093 else: # not self.edeleted
94 if self.rsum:
Guido van Rossum6bb4a511995-04-28 15:26:37 +000095 print "warning:",
96 print self.file,
97 print "was lost"
98 return 'U'
99 else: return 'r' # Get rid of entry
Guido van Rossum2f7ef911995-05-01 20:22:01 +0000100 else: # self.lsum
101 if not self.rsum:
Guido van Rossum6bb4a511995-04-28 15:26:37 +0000102 if self.enew: return 'A' # New locally
103 else: return 'D' # Deleted remotely
Guido van Rossum2f7ef911995-05-01 20:22:01 +0000104 else: # self.rsum
Guido van Rossum6bb4a511995-04-28 15:26:37 +0000105 if self.enew:
106 if self.lsum == self.rsum:
107 return 'u'
108 else:
109 return 'C'
110 if self.lsum == self.esum:
111 if self.esum == self.rsum:
112 return '='
113 else:
114 return 'U'
115 elif self.esum == self.rsum:
116 return 'M'
117 elif self.lsum == self.rsum:
118 return 'u'
119 else:
120 return 'C'
Guido van Rossumae21ced1995-04-28 14:32:26 +0000121
122 def update(self):
123 code = self.action()
Guido van Rossum8b5e0fa1995-04-28 21:48:16 +0000124 if code == '=': return
Guido van Rossumae21ced1995-04-28 14:32:26 +0000125 print code, self.file
Guido van Rossumba244681995-04-28 15:33:03 +0000126 if code in ('U', 'N'):
Guido van Rossumae21ced1995-04-28 14:32:26 +0000127 self.get()
128 elif code == 'C':
129 print "%s: conflict resolution not yet implemented" % \
130 self.file
Guido van Rossumba244681995-04-28 15:33:03 +0000131 elif code == 'D':
Guido van Rossum330e8841995-04-28 17:56:32 +0000132 remove(self.file)
Guido van Rossumba244681995-04-28 15:33:03 +0000133 self.eseen = 0
134 elif code == 'r':
135 self.eseen = 0
136 elif code in ('c', 'u'):
Guido van Rossumd22f59f1995-04-28 15:37:22 +0000137 self.eseen = 1
Guido van Rossumba244681995-04-28 15:33:03 +0000138 self.erev = self.rrev
139 self.enew = 0
140 self.edeleted = 0
141 self.esum = self.rsum
142 self.emtime, self.ectime = os.stat(self.file)[-2:]
Guido van Rossumd22f59f1995-04-28 15:37:22 +0000143 self.extra = ''
Guido van Rossumae21ced1995-04-28 14:32:26 +0000144
145 def commit(self, message = ""):
146 code = self.action()
147 if code in ('A', 'M'):
148 self.put(message)
Guido van Rossum7bde92b1995-10-07 19:47:26 +0000149 return 1
Guido van Rossumae21ced1995-04-28 14:32:26 +0000150 elif code == 'R':
151 print "%s: committing removes not yet implemented" % \
152 self.file
153 elif code == 'C':
154 print "%s: conflict resolution not yet implemented" % \
155 self.file
156
Guido van Rossum330e8841995-04-28 17:56:32 +0000157 def diff(self, opts = []):
Guido van Rossumec8cfd41995-05-01 20:06:44 +0000158 self.action() # To update lseen, rseen
Guido van Rossum330e8841995-04-28 17:56:32 +0000159 flags = ''
Guido van Rossumd1972af1995-06-21 01:02:06 +0000160 rev = self.rrev
161 # XXX should support two rev options too!
Guido van Rossum330e8841995-04-28 17:56:32 +0000162 for o, a in opts:
Guido van Rossumd1972af1995-06-21 01:02:06 +0000163 if o == '-r':
164 rev = a
165 else:
166 flags = flags + ' ' + o + a
167 if rev == self.rrev and self.lsum == self.rsum:
168 return
Guido van Rossum330e8841995-04-28 17:56:32 +0000169 flags = flags[1:]
170 fn = self.file
Guido van Rossumd1972af1995-06-21 01:02:06 +0000171 data = self.proxy.get((fn, rev))
172 sum = md5.new(data).digest()
173 if self.lsum == sum:
174 return
175 import tempfile
Guido van Rossum330e8841995-04-28 17:56:32 +0000176 tfn = tempfile.mktemp()
177 try:
178 tf = open(tfn, 'w')
179 tf.write(data)
180 tf.close()
Guido van Rossumd1972af1995-06-21 01:02:06 +0000181 print 'diff %s -r%s %s' % (flags, rev, fn)
Guido van Rossum330e8841995-04-28 17:56:32 +0000182 sts = os.system('diff %s %s %s' % (flags, tfn, fn))
183 if sts:
184 print '='*70
185 finally:
186 remove(tfn)
187
Guido van Rossumae21ced1995-04-28 14:32:26 +0000188 def commitcheck(self):
189 return self.action() != 'C'
190
191 def put(self, message = ""):
Guido van Rossumbafc14d1995-04-28 15:41:51 +0000192 print "Checking in", self.file, "..."
193 data = open(self.file).read()
Guido van Rossuma79f5a31995-07-18 18:34:34 +0000194 if not self.enew:
195 self.proxy.lock(self.file)
Guido van Rossumbafc14d1995-04-28 15:41:51 +0000196 messages = self.proxy.put(self.file, data, message)
197 if messages:
198 print messages
Guido van Rossum8b5e0fa1995-04-28 21:48:16 +0000199 self.setentry(self.proxy.head(self.file), self.lsum)
Guido van Rossumae21ced1995-04-28 14:32:26 +0000200
201 def get(self):
202 data = self.proxy.get(self.file)
203 f = open(self.file, 'w')
204 f.write(data)
205 f.close()
Guido van Rossum8b5e0fa1995-04-28 21:48:16 +0000206 self.setentry(self.rrev, self.rsum)
207
Guido van Rossum7bde92b1995-10-07 19:47:26 +0000208 def log(self, otherflags):
209 print self.proxy.log(self.file, otherflags)
210
Guido van Rossuma79f5a31995-07-18 18:34:34 +0000211 def add(self):
212 self.eseen = 0 # While we're hacking...
213 self.esum = self.lsum
214 self.emtime, self.ectime = 0, 0
215 self.erev = ''
216 self.enew = 1
217 self.edeleted = 0
218 self.eseen = 1 # Done
219 self.extra = ''
220
Guido van Rossum8b5e0fa1995-04-28 21:48:16 +0000221 def setentry(self, erev, esum):
222 self.eseen = 0 # While we're hacking...
223 self.esum = esum
Guido van Rossumae21ced1995-04-28 14:32:26 +0000224 self.emtime, self.ectime = os.stat(self.file)[-2:]
Guido van Rossum8b5e0fa1995-04-28 21:48:16 +0000225 self.erev = erev
Guido van Rossumae21ced1995-04-28 14:32:26 +0000226 self.enew = 0
227 self.edeleted = 0
Guido van Rossum8b5e0fa1995-04-28 21:48:16 +0000228 self.eseen = 1 # Done
Guido van Rossumd1972af1995-06-21 01:02:06 +0000229 self.extra = ''
230
231
232SENDMAIL = "/usr/lib/sendmail -t"
233MAILFORM = """To: %s
234Subject: CVS changes: %s
235
236...Message from rcvs...
237
238Committed files:
239 %s
240
241Log message:
242 %s
243"""
Guido van Rossum5f07b841995-04-26 22:57:11 +0000244
245
Guido van Rossumc218a7e1995-04-27 23:33:43 +0000246class RCVS(CVS):
247
248 FileClass = MyFile
249
250 def __init__(self):
251 CVS.__init__(self)
252
Guido van Rossumec8cfd41995-05-01 20:06:44 +0000253 def update(self, files):
254 for e in self.whichentries(files, 1):
255 e.update()
256
257 def commit(self, files, message = ""):
258 list = self.whichentries(files)
Guido van Rossumd1972af1995-06-21 01:02:06 +0000259 if not list: return
Guido van Rossumec8cfd41995-05-01 20:06:44 +0000260 ok = 1
261 for e in list:
262 if not e.commitcheck():
263 ok = 0
264 if not ok:
265 print "correct above errors first"
266 return
267 if not message:
268 message = raw_input("One-liner: ")
Guido van Rossuma79f5a31995-07-18 18:34:34 +0000269 committed = []
Guido van Rossumec8cfd41995-05-01 20:06:44 +0000270 for e in list:
Guido van Rossum7bde92b1995-10-07 19:47:26 +0000271 if e.commit(message):
272 committed.append(e.file)
Guido van Rossuma79f5a31995-07-18 18:34:34 +0000273 self.mailinfo(committed, message)
Guido van Rossumd1972af1995-06-21 01:02:06 +0000274
275 def mailinfo(self, files, message = ""):
276 towhom = "sjoerd@cwi.nl, jack@cwi.nl" # XXX
277 mailtext = MAILFORM % (towhom, string.join(files),
278 string.join(files), message)
279 print '-'*70
280 print mailtext
281 print '-'*70
282 ok = raw_input("OK to mail to %s? " % towhom)
283 if string.lower(string.strip(ok)) in ('y', 'ye', 'yes'):
284 p = os.popen(SENDMAIL, "w")
285 p.write(mailtext)
286 sts = p.close()
287 if sts:
288 print "Sendmail exit status %s" % str(sts)
289 else:
290 print "Mail sent."
291 else:
292 print "No mail sent."
Guido van Rossumec8cfd41995-05-01 20:06:44 +0000293
294 def report(self, files):
295 for e in self.whichentries(files):
296 e.report()
297
298 def diff(self, files, opts):
299 for e in self.whichentries(files):
300 e.diff(opts)
301
Guido van Rossuma79f5a31995-07-18 18:34:34 +0000302 def add(self, files):
303 if not files:
304 raise RuntimeError, "'cvs add' needs at least one file"
305 list = []
306 for e in self.whichentries(files, 1):
Guido van Rossuma79f5a31995-07-18 18:34:34 +0000307 e.add()
Guido van Rossuma79f5a31995-07-18 18:34:34 +0000308
309 def rm(self, files):
310 if not files:
311 raise RuntimeError, "'cvs rm' needs at least one file"
312 raise RuntimeError, "'cvs rm' not yet imlemented"
313
Guido van Rossum7bde92b1995-10-07 19:47:26 +0000314 def log(self, files, opts):
315 flags = ''
316 for o, a in opts:
317 flags = flags + ' ' + o + a
318 for e in self.whichentries(files):
319 e.log(flags)
320
Guido van Rossumec8cfd41995-05-01 20:06:44 +0000321 def whichentries(self, files, localfilestoo = 0):
322 if files:
323 list = []
Guido van Rossumae21ced1995-04-28 14:32:26 +0000324 for file in files:
Guido van Rossumec8cfd41995-05-01 20:06:44 +0000325 if self.entries.has_key(file):
326 e = self.entries[file]
327 else:
328 e = self.FileClass(file)
329 self.entries[file] = e
330 list.append(e)
331 else:
332 list = self.entries.values()
333 for file in self.proxy.listfiles():
334 if self.entries.has_key(file):
335 continue
336 e = self.FileClass(file)
337 self.entries[file] = e
338 list.append(e)
339 if localfilestoo:
340 for file in os.listdir(os.curdir):
341 if not self.entries.has_key(file) \
342 and not self.ignored(file):
343 e = self.FileClass(file)
344 self.entries[file] = e
345 list.append(e)
346 list.sort()
347 if self.proxy:
348 for e in list:
349 if e.proxy is None:
350 e.proxy = self.proxy
351 return list
Guido van Rossumc218a7e1995-04-27 23:33:43 +0000352
353
354class rcvs(CommandFrameWork):
355
Guido van Rossum903afd01995-06-23 22:33:57 +0000356 GlobalFlags = 'd:h:p:qvL'
Guido van Rossumc218a7e1995-04-27 23:33:43 +0000357 UsageMessage = \
Guido van Rossum2d2a60e1995-04-28 19:24:50 +0000358"usage: rcvs [-d directory] [-h host] [-p port] [-q] [-v] [subcommand arg ...]"
359 PostUsageMessage = \
360 "If no subcommand is given, the status of all files is listed"
Guido van Rossumc218a7e1995-04-27 23:33:43 +0000361
362 def __init__(self):
363 """Constructor."""
364 CommandFrameWork.__init__(self)
365 self.proxy = None
366 self.cvs = RCVS()
Guido van Rossum7bde92b1995-10-07 19:47:26 +0000367
368 def close(self):
Guido van Rossumd1972af1995-06-21 01:02:06 +0000369 if self.proxy:
370 self.proxy._close()
371 self.proxy = None
Guido van Rossum7bde92b1995-10-07 19:47:26 +0000372
373 def recurse(self):
374 self.close()
Guido van Rossumd1972af1995-06-21 01:02:06 +0000375 names = os.listdir(os.curdir)
376 for name in names:
377 if name == os.curdir or name == os.pardir:
378 continue
379 if name == "CVS":
380 continue
381 if not os.path.isdir(name):
382 continue
383 if os.path.islink(name):
384 continue
385 print "--- entering subdirectory", name, "---"
386 os.chdir(name)
387 try:
388 if os.path.isdir("CVS"):
389 self.__class__().run()
390 else:
391 self.recurse()
392 finally:
393 os.chdir(os.pardir)
394 print "--- left subdirectory", name, "---"
395
Guido van Rossumc218a7e1995-04-27 23:33:43 +0000396 def options(self, opts):
397 self.opts = opts
398
399 def ready(self):
400 import rcsclient
401 self.proxy = rcsclient.openrcsclient(self.opts)
402 self.cvs.setproxy(self.proxy)
403 self.cvs.getentries()
Guido van Rossumc218a7e1995-04-27 23:33:43 +0000404
405 def default(self):
Guido van Rossumec8cfd41995-05-01 20:06:44 +0000406 self.cvs.report([])
Guido van Rossumc218a7e1995-04-27 23:33:43 +0000407
Guido van Rossuma79f5a31995-07-18 18:34:34 +0000408 def do_report(self, opts, files):
409 self.cvs.report(files)
410
Guido van Rossumc218a7e1995-04-27 23:33:43 +0000411 def do_update(self, opts, files):
Guido van Rossumd1972af1995-06-21 01:02:06 +0000412 """update [-l] [-R] [file] ..."""
413 local = DEF_LOCAL
414 for o, a in opts:
415 if o == '-l': local = 1
416 if o == '-R': local = 0
Guido van Rossumec8cfd41995-05-01 20:06:44 +0000417 self.cvs.update(files)
Guido van Rossum6bb4a511995-04-28 15:26:37 +0000418 self.cvs.putentries()
Guido van Rossumd1972af1995-06-21 01:02:06 +0000419 if not local and not files:
420 self.recurse()
421 flags_update = '-lR'
Guido van Rossum330e8841995-04-28 17:56:32 +0000422 do_up = do_update
Guido van Rossumd1972af1995-06-21 01:02:06 +0000423 flags_up = flags_update
Guido van Rossum5f07b841995-04-26 22:57:11 +0000424
Guido van Rossumae21ced1995-04-28 14:32:26 +0000425 def do_commit(self, opts, files):
Guido van Rossumec8cfd41995-05-01 20:06:44 +0000426 """commit [-m message] [file] ..."""
427 message = ""
428 for o, a in opts:
429 if o == '-m': message = a
430 self.cvs.commit(files, message)
Guido van Rossum6bb4a511995-04-28 15:26:37 +0000431 self.cvs.putentries()
Guido van Rossumec8cfd41995-05-01 20:06:44 +0000432 flags_commit = 'm:'
Guido van Rossumd1972af1995-06-21 01:02:06 +0000433 do_com = do_commit
434 flags_com = flags_commit
Guido van Rossum330e8841995-04-28 17:56:32 +0000435
436 def do_diff(self, opts, files):
437 """diff [difflags] [file] ..."""
Guido van Rossumec8cfd41995-05-01 20:06:44 +0000438 self.cvs.diff(files, opts)
Guido van Rossumd1972af1995-06-21 01:02:06 +0000439 flags_diff = 'cbitwcefhnlr:sD:S:'
Guido van Rossum330e8841995-04-28 17:56:32 +0000440 do_dif = do_diff
Guido van Rossumd1972af1995-06-21 01:02:06 +0000441 flags_dif = flags_diff
Guido van Rossum330e8841995-04-28 17:56:32 +0000442
Guido van Rossuma79f5a31995-07-18 18:34:34 +0000443 def do_add(self, opts, files):
444 """add file ..."""
445 if not files:
446 print "'rcvs add' requires at least one file"
447 return
448 self.cvs.add(files)
449 self.cvs.putentries()
450
451 def do_remove(self, opts, files):
452 """remove file ..."""
453 if not files:
454 print "'rcvs remove' requires at least one file"
455 return
456 self.cvs.remove(files)
457 self.cvs.putentries()
458 do_rm = do_remove
459
Guido van Rossum7bde92b1995-10-07 19:47:26 +0000460 def do_log(self, opts, files):
461 """log [rlog-options] [file] ..."""
462 self.cvs.log(files, opts)
463 flags_log = 'bhLNRtd:s:V:'
Guido van Rossum330e8841995-04-28 17:56:32 +0000464
465
466def remove(fn):
467 try:
468 os.unlink(fn)
469 except os.error:
470 pass
Guido van Rossumae21ced1995-04-28 14:32:26 +0000471
Guido van Rossum5f07b841995-04-26 22:57:11 +0000472
Guido van Rossumdeb627c1995-04-27 21:28:53 +0000473def main():
Guido van Rossum7bde92b1995-10-07 19:47:26 +0000474 r = rcvs()
475 try:
476 r.run()
477 finally:
478 r.close()
Guido van Rossum097c55a1995-04-27 18:07:07 +0000479
Guido van Rossum5f07b841995-04-26 22:57:11 +0000480
481if __name__ == "__main__":
Guido van Rossumdeb627c1995-04-27 21:28:53 +0000482 main()