blob: 4e2532aa54fedd9d8126f5cc84c64df2f89ac2a2 [file] [log] [blame]
Guido van Rossumf06ee5f1996-11-27 19:52:01 +00001#! /usr/bin/env python
Guido van Rossum097c55a1995-04-27 18:07:07 +00002
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 Rossuma79f5a31995-07-18 18:34:34 +000011# - cvs rm
12# - descend into directories (alraedy done for update)
13# - conflict resolution
Guido van Rossumd1972af1995-06-21 01:02:06 +000014# - 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
Guido van Rossumc218a7e1995-04-27 23:33:43 +000035from cvslib import CVS, File
Guido van Rossum5f07b841995-04-26 22:57:11 +000036import md5
37import os
Guido van Rossum5f07b841995-04-26 22:57:11 +000038import sys
Guido van Rossumc218a7e1995-04-27 23:33:43 +000039from cmdfw import CommandFrameWork
Guido van Rossum5f07b841995-04-26 22:57:11 +000040
41
Tim Peterse6ddc8b2004-07-18 05:56:09 +000042DEF_LOCAL = 1 # Default -l
Guido van Rossumd1972af1995-06-21 01:02:06 +000043
44
Guido van Rossumc218a7e1995-04-27 23:33:43 +000045class MyFile(File):
Guido van Rossum5f07b841995-04-26 22:57:11 +000046
Tim Peterse6ddc8b2004-07-18 05:56:09 +000047 def action(self):
48 """Return a code indicating the update status of this file.
Guido van Rossumae21ced1995-04-28 14:32:26 +000049
Tim Peterse6ddc8b2004-07-18 05:56:09 +000050 The possible return values are:
Guido van Rossum2f7ef911995-05-01 20:22:01 +000051
Tim Peterse6ddc8b2004-07-18 05:56:09 +000052 '=' -- everything's fine
53 '0' -- file doesn't exist anywhere
54 '?' -- exists locally only
55 'A' -- new locally
56 'R' -- deleted locally
57 'U' -- changed remotely, no changes locally
58 (includes new remotely or deleted remotely)
59 'M' -- changed locally, no changes remotely
60 'C' -- conflict: changed locally as well as remotely
61 (includes cases where the file has been added
62 or removed locally and remotely)
63 'D' -- deleted remotely
64 'N' -- new remotely
65 'r' -- get rid of entry
66 'c' -- create entry
67 'u' -- update entry
Guido van Rossumae21ced1995-04-28 14:32:26 +000068
Tim Peterse6ddc8b2004-07-18 05:56:09 +000069 (and probably others :-)
70 """
71 if not self.lseen:
72 self.getlocal()
73 if not self.rseen:
74 self.getremote()
75 if not self.eseen:
76 if not self.lsum:
77 if not self.rsum: return '0' # Never heard of
78 else:
79 return 'N' # New remotely
80 else: # self.lsum
81 if not self.rsum: return '?' # Local only
82 # Local and remote, but no entry
83 if self.lsum == self.rsum:
84 return 'c' # Restore entry only
85 else: return 'C' # Real conflict
86 else: # self.eseen
87 if not self.lsum:
88 if self.edeleted:
89 if self.rsum: return 'R' # Removed
90 else: return 'r' # Get rid of entry
91 else: # not self.edeleted
92 if self.rsum:
Collin Winter6f2df4d2007-07-17 20:59:35 +000093 print("warning:", end=' ')
94 print(self.file, end=' ')
95 print("was lost")
Tim Peterse6ddc8b2004-07-18 05:56:09 +000096 return 'U'
97 else: return 'r' # Get rid of entry
98 else: # self.lsum
99 if not self.rsum:
100 if self.enew: return 'A' # New locally
101 else: return 'D' # Deleted remotely
102 else: # self.rsum
103 if self.enew:
104 if self.lsum == self.rsum:
105 return 'u'
106 else:
107 return 'C'
108 if self.lsum == self.esum:
109 if self.esum == self.rsum:
110 return '='
111 else:
112 return 'U'
113 elif self.esum == self.rsum:
114 return 'M'
115 elif self.lsum == self.rsum:
116 return 'u'
117 else:
118 return 'C'
Guido van Rossumae21ced1995-04-28 14:32:26 +0000119
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000120 def update(self):
121 code = self.action()
122 if code == '=': return
Collin Winter6f2df4d2007-07-17 20:59:35 +0000123 print(code, self.file)
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000124 if code in ('U', 'N'):
125 self.get()
126 elif code == 'C':
Collin Winter6f2df4d2007-07-17 20:59:35 +0000127 print("%s: conflict resolution not yet implemented" % \
128 self.file)
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000129 elif code == 'D':
130 remove(self.file)
131 self.eseen = 0
132 elif code == 'r':
133 self.eseen = 0
134 elif code in ('c', 'u'):
135 self.eseen = 1
136 self.erev = self.rrev
137 self.enew = 0
138 self.edeleted = 0
139 self.esum = self.rsum
140 self.emtime, self.ectime = os.stat(self.file)[-2:]
141 self.extra = ''
Guido van Rossumae21ced1995-04-28 14:32:26 +0000142
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000143 def commit(self, message = ""):
144 code = self.action()
145 if code in ('A', 'M'):
146 self.put(message)
147 return 1
148 elif code == 'R':
Collin Winter6f2df4d2007-07-17 20:59:35 +0000149 print("%s: committing removes not yet implemented" % \
150 self.file)
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000151 elif code == 'C':
Collin Winter6f2df4d2007-07-17 20:59:35 +0000152 print("%s: conflict resolution not yet implemented" % \
153 self.file)
Guido van Rossum330e8841995-04-28 17:56:32 +0000154
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000155 def diff(self, opts = []):
156 self.action() # To update lseen, rseen
157 flags = ''
158 rev = self.rrev
159 # XXX should support two rev options too!
160 for o, a in opts:
161 if o == '-r':
162 rev = a
163 else:
164 flags = flags + ' ' + o + a
165 if rev == self.rrev and self.lsum == self.rsum:
166 return
167 flags = flags[1:]
168 fn = self.file
169 data = self.proxy.get((fn, rev))
170 sum = md5.new(data).digest()
171 if self.lsum == sum:
172 return
173 import tempfile
174 tf = tempfile.NamedTemporaryFile()
175 tf.write(data)
176 tf.flush()
Collin Winter6f2df4d2007-07-17 20:59:35 +0000177 print('diff %s -r%s %s' % (flags, rev, fn))
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000178 sts = os.system('diff %s %s %s' % (flags, tf.name, fn))
179 if sts:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000180 print('='*70)
Guido van Rossumae21ced1995-04-28 14:32:26 +0000181
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000182 def commitcheck(self):
183 return self.action() != 'C'
Guido van Rossum8b5e0fa1995-04-28 21:48:16 +0000184
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000185 def put(self, message = ""):
Collin Winter6f2df4d2007-07-17 20:59:35 +0000186 print("Checking in", self.file, "...")
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000187 data = open(self.file).read()
188 if not self.enew:
189 self.proxy.lock(self.file)
190 messages = self.proxy.put(self.file, data, message)
191 if messages:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000192 print(messages)
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000193 self.setentry(self.proxy.head(self.file), self.lsum)
Guido van Rossum7bde92b1995-10-07 19:47:26 +0000194
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000195 def get(self):
196 data = self.proxy.get(self.file)
197 f = open(self.file, 'w')
198 f.write(data)
199 f.close()
200 self.setentry(self.rrev, self.rsum)
Guido van Rossuma79f5a31995-07-18 18:34:34 +0000201
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000202 def log(self, otherflags):
Collin Winter6f2df4d2007-07-17 20:59:35 +0000203 print(self.proxy.log(self.file, otherflags))
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000204
205 def add(self):
206 self.eseen = 0 # While we're hacking...
207 self.esum = self.lsum
208 self.emtime, self.ectime = 0, 0
209 self.erev = ''
210 self.enew = 1
211 self.edeleted = 0
212 self.eseen = 1 # Done
213 self.extra = ''
214
215 def setentry(self, erev, esum):
216 self.eseen = 0 # While we're hacking...
217 self.esum = esum
218 self.emtime, self.ectime = os.stat(self.file)[-2:]
219 self.erev = erev
220 self.enew = 0
221 self.edeleted = 0
222 self.eseen = 1 # Done
223 self.extra = ''
Guido van Rossumd1972af1995-06-21 01:02:06 +0000224
225
226SENDMAIL = "/usr/lib/sendmail -t"
227MAILFORM = """To: %s
228Subject: CVS changes: %s
229
230...Message from rcvs...
231
232Committed files:
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000233 %s
Guido van Rossumd1972af1995-06-21 01:02:06 +0000234
235Log message:
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000236 %s
Guido van Rossumd1972af1995-06-21 01:02:06 +0000237"""
Guido van Rossum5f07b841995-04-26 22:57:11 +0000238
239
Guido van Rossumc218a7e1995-04-27 23:33:43 +0000240class RCVS(CVS):
241
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000242 FileClass = MyFile
Guido van Rossumc218a7e1995-04-27 23:33:43 +0000243
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000244 def __init__(self):
245 CVS.__init__(self)
Guido van Rossumc218a7e1995-04-27 23:33:43 +0000246
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000247 def update(self, files):
248 for e in self.whichentries(files, 1):
249 e.update()
Guido van Rossumec8cfd41995-05-01 20:06:44 +0000250
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000251 def commit(self, files, message = ""):
252 list = self.whichentries(files)
253 if not list: return
254 ok = 1
255 for e in list:
256 if not e.commitcheck():
257 ok = 0
258 if not ok:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000259 print("correct above errors first")
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000260 return
261 if not message:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000262 message = input("One-liner: ")
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000263 committed = []
264 for e in list:
265 if e.commit(message):
266 committed.append(e.file)
267 self.mailinfo(committed, message)
Guido van Rossumd1972af1995-06-21 01:02:06 +0000268
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000269 def mailinfo(self, files, message = ""):
270 towhom = "sjoerd@cwi.nl, jack@cwi.nl" # XXX
Neal Norwitzce96f692006-03-17 06:49:51 +0000271 mailtext = MAILFORM % (towhom, ' '.join(files),
272 ' '.join(files), message)
Collin Winter6f2df4d2007-07-17 20:59:35 +0000273 print('-'*70)
274 print(mailtext)
275 print('-'*70)
276 ok = input("OK to mail to %s? " % towhom)
Neal Norwitzce96f692006-03-17 06:49:51 +0000277 if ok.lower().strip() in ('y', 'ye', 'yes'):
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000278 p = os.popen(SENDMAIL, "w")
279 p.write(mailtext)
280 sts = p.close()
281 if sts:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000282 print("Sendmail exit status %s" % str(sts))
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000283 else:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000284 print("Mail sent.")
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000285 else:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000286 print("No mail sent.")
Guido van Rossumec8cfd41995-05-01 20:06:44 +0000287
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000288 def report(self, files):
289 for e in self.whichentries(files):
290 e.report()
Guido van Rossumec8cfd41995-05-01 20:06:44 +0000291
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000292 def diff(self, files, opts):
293 for e in self.whichentries(files):
294 e.diff(opts)
Guido van Rossumec8cfd41995-05-01 20:06:44 +0000295
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000296 def add(self, files):
297 if not files:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000298 raise RuntimeError("'cvs add' needs at least one file")
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000299 list = []
300 for e in self.whichentries(files, 1):
301 e.add()
Guido van Rossuma79f5a31995-07-18 18:34:34 +0000302
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000303 def rm(self, files):
304 if not files:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000305 raise RuntimeError("'cvs rm' needs at least one file")
306 raise RuntimeError("'cvs rm' not yet imlemented")
Guido van Rossuma79f5a31995-07-18 18:34:34 +0000307
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000308 def log(self, files, opts):
309 flags = ''
310 for o, a in opts:
311 flags = flags + ' ' + o + a
312 for e in self.whichentries(files):
313 e.log(flags)
Guido van Rossum7bde92b1995-10-07 19:47:26 +0000314
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000315 def whichentries(self, files, localfilestoo = 0):
316 if files:
317 list = []
318 for file in files:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000319 if file in self.entries:
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000320 e = self.entries[file]
321 else:
322 e = self.FileClass(file)
323 self.entries[file] = e
324 list.append(e)
325 else:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000326 list = list(self.entries.values())
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000327 for file in self.proxy.listfiles():
Collin Winter6f2df4d2007-07-17 20:59:35 +0000328 if file in self.entries:
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000329 continue
330 e = self.FileClass(file)
331 self.entries[file] = e
332 list.append(e)
333 if localfilestoo:
334 for file in os.listdir(os.curdir):
Collin Winter6f2df4d2007-07-17 20:59:35 +0000335 if file not in self.entries \
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000336 and not self.ignored(file):
337 e = self.FileClass(file)
338 self.entries[file] = e
339 list.append(e)
340 list.sort()
341 if self.proxy:
342 for e in list:
343 if e.proxy is None:
344 e.proxy = self.proxy
345 return list
Guido van Rossumc218a7e1995-04-27 23:33:43 +0000346
347
348class rcvs(CommandFrameWork):
349
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000350 GlobalFlags = 'd:h:p:qvL'
351 UsageMessage = \
Guido van Rossum2d2a60e1995-04-28 19:24:50 +0000352"usage: rcvs [-d directory] [-h host] [-p port] [-q] [-v] [subcommand arg ...]"
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000353 PostUsageMessage = \
354 "If no subcommand is given, the status of all files is listed"
Guido van Rossumc218a7e1995-04-27 23:33:43 +0000355
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000356 def __init__(self):
357 """Constructor."""
358 CommandFrameWork.__init__(self)
359 self.proxy = None
360 self.cvs = RCVS()
Guido van Rossum7bde92b1995-10-07 19:47:26 +0000361
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000362 def close(self):
363 if self.proxy:
364 self.proxy._close()
365 self.proxy = None
Guido van Rossumd1972af1995-06-21 01:02:06 +0000366
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000367 def recurse(self):
368 self.close()
369 names = os.listdir(os.curdir)
370 for name in names:
371 if name == os.curdir or name == os.pardir:
372 continue
373 if name == "CVS":
374 continue
375 if not os.path.isdir(name):
376 continue
377 if os.path.islink(name):
378 continue
Collin Winter6f2df4d2007-07-17 20:59:35 +0000379 print("--- entering subdirectory", name, "---")
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000380 os.chdir(name)
381 try:
382 if os.path.isdir("CVS"):
383 self.__class__().run()
384 else:
385 self.recurse()
386 finally:
387 os.chdir(os.pardir)
Collin Winter6f2df4d2007-07-17 20:59:35 +0000388 print("--- left subdirectory", name, "---")
Guido van Rossumc218a7e1995-04-27 23:33:43 +0000389
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000390 def options(self, opts):
391 self.opts = opts
Guido van Rossumc218a7e1995-04-27 23:33:43 +0000392
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000393 def ready(self):
394 import rcsclient
395 self.proxy = rcsclient.openrcsclient(self.opts)
396 self.cvs.setproxy(self.proxy)
397 self.cvs.getentries()
Guido van Rossumc218a7e1995-04-27 23:33:43 +0000398
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000399 def default(self):
400 self.cvs.report([])
Guido van Rossuma79f5a31995-07-18 18:34:34 +0000401
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000402 def do_report(self, opts, files):
403 self.cvs.report(files)
Guido van Rossum5f07b841995-04-26 22:57:11 +0000404
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000405 def do_update(self, opts, files):
406 """update [-l] [-R] [file] ..."""
407 local = DEF_LOCAL
408 for o, a in opts:
409 if o == '-l': local = 1
410 if o == '-R': local = 0
411 self.cvs.update(files)
412 self.cvs.putentries()
413 if not local and not files:
414 self.recurse()
415 flags_update = '-lR'
416 do_up = do_update
417 flags_up = flags_update
Guido van Rossum330e8841995-04-28 17:56:32 +0000418
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000419 def do_commit(self, opts, files):
420 """commit [-m message] [file] ..."""
421 message = ""
422 for o, a in opts:
423 if o == '-m': message = a
424 self.cvs.commit(files, message)
425 self.cvs.putentries()
426 flags_commit = 'm:'
427 do_com = do_commit
428 flags_com = flags_commit
Guido van Rossum330e8841995-04-28 17:56:32 +0000429
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000430 def do_diff(self, opts, files):
431 """diff [difflags] [file] ..."""
432 self.cvs.diff(files, opts)
433 flags_diff = 'cbitwcefhnlr:sD:S:'
434 do_dif = do_diff
435 flags_dif = flags_diff
Guido van Rossuma79f5a31995-07-18 18:34:34 +0000436
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000437 def do_add(self, opts, files):
438 """add file ..."""
439 if not files:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000440 print("'rcvs add' requires at least one file")
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000441 return
442 self.cvs.add(files)
443 self.cvs.putentries()
Guido van Rossuma79f5a31995-07-18 18:34:34 +0000444
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000445 def do_remove(self, opts, files):
446 """remove file ..."""
447 if not files:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000448 print("'rcvs remove' requires at least one file")
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000449 return
450 self.cvs.remove(files)
451 self.cvs.putentries()
452 do_rm = do_remove
453
454 def do_log(self, opts, files):
455 """log [rlog-options] [file] ..."""
456 self.cvs.log(files, opts)
457 flags_log = 'bhLNRtd:s:V:r:'
Guido van Rossum330e8841995-04-28 17:56:32 +0000458
459
460def remove(fn):
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000461 try:
462 os.unlink(fn)
463 except os.error:
464 pass
Guido van Rossumae21ced1995-04-28 14:32:26 +0000465
Guido van Rossum5f07b841995-04-26 22:57:11 +0000466
Guido van Rossumdeb627c1995-04-27 21:28:53 +0000467def main():
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000468 r = rcvs()
469 try:
470 r.run()
471 finally:
472 r.close()
Guido van Rossum097c55a1995-04-27 18:07:07 +0000473
Guido van Rossum5f07b841995-04-26 22:57:11 +0000474
475if __name__ == "__main__":
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000476 main()