changes by Barry, e.g. font lock & email addresses
[python/dscho.git] / Demo / pdist / rcvs.py
blobf7cdb41a56434d6dc82a54f99223d002d6d7c964
1 #! /usr/local/bin/python
3 """Remote CVS -- command line interface"""
5 # XXX To do:
7 # Bugs:
8 # - if the remote file is deleted, "rcvs update" will fail
10 # Functionality:
11 # - cvs log
12 # - cvs rm
13 # - descend into directories (alraedy done for update)
14 # - conflict resolution
15 # - other relevant commands?
16 # - branches
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
25 # Performance:
26 # - cache remote checksums (for every revision ever seen!)
27 # - translate symbolic revisions to numeric revisions
29 # Reliability:
30 # - remote locking
32 # Security:
33 # - Authenticated RPC?
36 from cvslib import CVS, File
37 import md5
38 import os
39 import string
40 import sys
41 from cmdfw import CommandFrameWork
44 DEF_LOCAL = 1 # Default -l
47 class MyFile(File):
49 def action(self):
50 """Return a code indicating the update status of this file.
52 The possible return values are:
54 '=' -- 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
60 (includes new remotely or deleted remotely)
61 '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)
65 'D' -- deleted remotely
66 'N' -- new remotely
67 'r' -- get rid of entry
68 'c' -- create entry
69 'u' -- update entry
71 (and probably others :-)
72 """
73 if not self.lseen:
74 self.getlocal()
75 if not self.rseen:
76 self.getremote()
77 if not self.eseen:
78 if not self.lsum:
79 if not self.rsum: return '0' # Never heard of
80 else:
81 return 'N' # New remotely
82 else: # self.lsum
83 if not self.rsum: return '?' # Local only
84 # 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
89 if not self.lsum:
90 if self.edeleted:
91 if self.rsum: return 'R' # Removed
92 else: return 'r' # Get rid of entry
93 else: # not self.edeleted
94 if self.rsum:
95 print "warning:",
96 print self.file,
97 print "was lost"
98 return 'U'
99 else: return 'r' # Get rid of entry
100 else: # self.lsum
101 if not self.rsum:
102 if self.enew: return 'A' # New locally
103 else: return 'D' # Deleted remotely
104 else: # self.rsum
105 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'
122 def update(self):
123 code = self.action()
124 if code == '=': return
125 print code, self.file
126 if code in ('U', 'N'):
127 self.get()
128 elif code == 'C':
129 print "%s: conflict resolution not yet implemented" % \
130 self.file
131 elif code == 'D':
132 remove(self.file)
133 self.eseen = 0
134 elif code == 'r':
135 self.eseen = 0
136 elif code in ('c', 'u'):
137 self.eseen = 1
138 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:]
143 self.extra = ''
145 def commit(self, message = ""):
146 code = self.action()
147 if code in ('A', 'M'):
148 self.put(message)
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
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 tfn = tempfile.mktemp()
176 try:
177 tf = open(tfn, 'w')
178 tf.write(data)
179 tf.close()
180 print 'diff %s -r%s %s' % (flags, rev, fn)
181 sts = os.system('diff %s %s %s' % (flags, tfn, fn))
182 if sts:
183 print '='*70
184 finally:
185 remove(tfn)
187 def commitcheck(self):
188 return self.action() != 'C'
190 def put(self, message = ""):
191 print "Checking in", self.file, "..."
192 data = open(self.file).read()
193 if not self.enew:
194 self.proxy.lock(self.file)
195 messages = self.proxy.put(self.file, data, message)
196 if messages:
197 print messages
198 self.setentry(self.proxy.head(self.file), self.lsum)
200 def get(self):
201 data = self.proxy.get(self.file)
202 f = open(self.file, 'w')
203 f.write(data)
204 f.close()
205 self.setentry(self.rrev, self.rsum)
207 def add(self):
208 self.eseen = 0 # While we're hacking...
209 self.esum = self.lsum
210 self.emtime, self.ectime = 0, 0
211 self.erev = ''
212 self.enew = 1
213 self.edeleted = 0
214 self.eseen = 1 # Done
215 self.extra = ''
217 def setentry(self, erev, esum):
218 self.eseen = 0 # While we're hacking...
219 self.esum = esum
220 self.emtime, self.ectime = os.stat(self.file)[-2:]
221 self.erev = erev
222 self.enew = 0
223 self.edeleted = 0
224 self.eseen = 1 # Done
225 self.extra = ''
228 SENDMAIL = "/usr/lib/sendmail -t"
229 MAILFORM = """To: %s
230 Subject: CVS changes: %s
232 ...Message from rcvs...
234 Committed files:
237 Log message:
242 class RCVS(CVS):
244 FileClass = MyFile
246 def __init__(self):
247 CVS.__init__(self)
249 def update(self, files):
250 for e in self.whichentries(files, 1):
251 e.update()
253 def commit(self, files, message = ""):
254 list = self.whichentries(files)
255 if not list: return
256 ok = 1
257 for e in list:
258 if not e.commitcheck():
259 ok = 0
260 if not ok:
261 print "correct above errors first"
262 return
263 if not message:
264 message = raw_input("One-liner: ")
265 committed = []
266 for e in list:
267 committed.append(e.file)
268 e.commit(message)
269 self.mailinfo(committed, message)
271 def mailinfo(self, files, message = ""):
272 towhom = "sjoerd@cwi.nl, jack@cwi.nl" # XXX
273 mailtext = MAILFORM % (towhom, string.join(files),
274 string.join(files), message)
275 print '-'*70
276 print mailtext
277 print '-'*70
278 ok = raw_input("OK to mail to %s? " % towhom)
279 if string.lower(string.strip(ok)) in ('y', 'ye', 'yes'):
280 p = os.popen(SENDMAIL, "w")
281 p.write(mailtext)
282 sts = p.close()
283 if sts:
284 print "Sendmail exit status %s" % str(sts)
285 else:
286 print "Mail sent."
287 else:
288 print "No mail sent."
290 def report(self, files):
291 for e in self.whichentries(files):
292 e.report()
294 def diff(self, files, opts):
295 for e in self.whichentries(files):
296 e.diff(opts)
298 def add(self, files):
299 if not files:
300 raise RuntimeError, "'cvs add' needs at least one file"
301 list = []
302 for e in self.whichentries(files, 1):
303 code = e.action()
304 print code, e.file
305 e.report()
306 e.add()
307 code = e.action()
308 print code, e.file
309 e.report()
310 print '='*20
312 def rm(self, files):
313 if not files:
314 raise RuntimeError, "'cvs rm' needs at least one file"
315 raise RuntimeError, "'cvs rm' not yet imlemented"
317 def whichentries(self, files, localfilestoo = 0):
318 if files:
319 list = []
320 for file in files:
321 if self.entries.has_key(file):
322 e = self.entries[file]
323 else:
324 e = self.FileClass(file)
325 self.entries[file] = e
326 list.append(e)
327 else:
328 list = self.entries.values()
329 for file in self.proxy.listfiles():
330 if self.entries.has_key(file):
331 continue
332 e = self.FileClass(file)
333 self.entries[file] = e
334 list.append(e)
335 if localfilestoo:
336 for file in os.listdir(os.curdir):
337 if not self.entries.has_key(file) \
338 and not self.ignored(file):
339 e = self.FileClass(file)
340 self.entries[file] = e
341 list.append(e)
342 list.sort()
343 if self.proxy:
344 for e in list:
345 if e.proxy is None:
346 e.proxy = self.proxy
347 return list
350 class rcvs(CommandFrameWork):
352 GlobalFlags = 'd:h:p:qvL'
353 UsageMessage = \
354 "usage: rcvs [-d directory] [-h host] [-p port] [-q] [-v] [subcommand arg ...]"
355 PostUsageMessage = \
356 "If no subcommand is given, the status of all files is listed"
358 def __init__(self):
359 """Constructor."""
360 CommandFrameWork.__init__(self)
361 self.proxy = None
362 self.cvs = RCVS()
364 def recurse(self):
365 if self.proxy:
366 self.proxy._close()
367 self.proxy = None
368 names = os.listdir(os.curdir)
369 for name in names:
370 if name == os.curdir or name == os.pardir:
371 continue
372 if name == "CVS":
373 continue
374 if not os.path.isdir(name):
375 continue
376 if os.path.islink(name):
377 continue
378 print "--- entering subdirectory", name, "---"
379 os.chdir(name)
380 try:
381 if os.path.isdir("CVS"):
382 self.__class__().run()
383 else:
384 self.recurse()
385 finally:
386 os.chdir(os.pardir)
387 print "--- left subdirectory", name, "---"
389 def options(self, opts):
390 self.opts = opts
392 def ready(self):
393 import rcsclient
394 self.proxy = rcsclient.openrcsclient(self.opts)
395 self.cvs.setproxy(self.proxy)
396 self.cvs.getentries()
398 def default(self):
399 self.cvs.report([])
401 def do_report(self, opts, files):
402 self.cvs.report(files)
404 def do_update(self, opts, files):
405 """update [-l] [-R] [file] ..."""
406 local = DEF_LOCAL
407 for o, a in opts:
408 if o == '-l': local = 1
409 if o == '-R': local = 0
410 self.cvs.update(files)
411 self.cvs.putentries()
412 if not local and not files:
413 self.recurse()
414 flags_update = '-lR'
415 do_up = do_update
416 flags_up = flags_update
418 def do_commit(self, opts, files):
419 """commit [-m message] [file] ..."""
420 message = ""
421 for o, a in opts:
422 if o == '-m': message = a
423 self.cvs.commit(files, message)
424 self.cvs.putentries()
425 flags_commit = 'm:'
426 do_com = do_commit
427 flags_com = flags_commit
429 def do_diff(self, opts, files):
430 """diff [difflags] [file] ..."""
431 self.cvs.diff(files, opts)
432 flags_diff = 'cbitwcefhnlr:sD:S:'
433 do_dif = do_diff
434 flags_dif = flags_diff
436 def do_add(self, opts, files):
437 """add file ..."""
438 if not files:
439 print "'rcvs add' requires at least one file"
440 return
441 self.cvs.add(files)
442 self.cvs.putentries()
444 def do_remove(self, opts, files):
445 """remove file ..."""
446 if not files:
447 print "'rcvs remove' requires at least one file"
448 return
449 self.cvs.remove(files)
450 self.cvs.putentries()
451 do_rm = do_remove
456 def remove(fn):
457 try:
458 os.unlink(fn)
459 except os.error:
460 pass
463 def main():
464 rcvs().run()
467 if __name__ == "__main__":
468 main()