#7342: make sure that the datetime object in test_fraction always has a number of...
[python.git] / Demo / pdist / rcvs.py
blob8b8bae63b930599c7a1898effde4c757d0948f9e
1 #! /usr/bin/env 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 rm
12 # - descend into directories (alraedy done for update)
13 # - conflict resolution
14 # - other relevant commands?
15 # - branches
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
24 # Performance:
25 # - cache remote checksums (for every revision ever seen!)
26 # - translate symbolic revisions to numeric revisions
28 # Reliability:
29 # - remote locking
31 # Security:
32 # - Authenticated RPC?
35 from cvslib import CVS, File
36 import md5
37 import os
38 import string
39 import sys
40 from cmdfw import CommandFrameWork
43 DEF_LOCAL = 1 # Default -l
46 class MyFile(File):
48 def action(self):
49 """Return a code indicating the update status of this file.
51 The possible return values are:
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
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'
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 = ''
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
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
183 def commitcheck(self):
184 return self.action() != 'C'
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)
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)
203 def log(self, otherflags):
204 print self.proxy.log(self.file, otherflags)
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 = ''
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 = ''
227 SENDMAIL = "/usr/lib/sendmail -t"
228 MAILFORM = """To: %s
229 Subject: CVS changes: %s
231 ...Message from rcvs...
233 Committed files:
236 Log message:
241 class RCVS(CVS):
243 FileClass = MyFile
245 def __init__(self):
246 CVS.__init__(self)
248 def update(self, files):
249 for e in self.whichentries(files, 1):
250 e.update()
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)
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."
289 def report(self, files):
290 for e in self.whichentries(files):
291 e.report()
293 def diff(self, files, opts):
294 for e in self.whichentries(files):
295 e.diff(opts)
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()
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"
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)
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
349 class rcvs(CommandFrameWork):
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"
357 def __init__(self):
358 """Constructor."""
359 CommandFrameWork.__init__(self)
360 self.proxy = None
361 self.cvs = RCVS()
363 def close(self):
364 if self.proxy:
365 self.proxy._close()
366 self.proxy = None
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, "---"
391 def options(self, opts):
392 self.opts = opts
394 def ready(self):
395 import rcsclient
396 self.proxy = rcsclient.openrcsclient(self.opts)
397 self.cvs.setproxy(self.proxy)
398 self.cvs.getentries()
400 def default(self):
401 self.cvs.report([])
403 def do_report(self, opts, files):
404 self.cvs.report(files)
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
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
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
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()
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
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:'
461 def remove(fn):
462 try:
463 os.unlink(fn)
464 except os.error:
465 pass
468 def main():
469 r = rcvs()
470 try:
471 r.run()
472 finally:
473 r.close()
476 if __name__ == "__main__":
477 main()