| #! /usr/bin/env python | |
| """Remote CVS -- command line interface""" | |
| # XXX To do: | |
| # | |
| # Bugs: | |
| # - if the remote file is deleted, "rcvs update" will fail | |
| # | |
| # Functionality: | |
| # - cvs rm | |
| # - descend into directories (alraedy done for update) | |
| # - conflict resolution | |
| # - other relevant commands? | |
| # - branches | |
| # | |
| # - Finesses: | |
| # - retain file mode's x bits | |
| # - complain when "nothing known about filename" | |
| # - edit log message the way CVS lets you edit it | |
| # - cvs diff -rREVA -rREVB | |
| # - send mail the way CVS sends it | |
| # | |
| # Performance: | |
| # - cache remote checksums (for every revision ever seen!) | |
| # - translate symbolic revisions to numeric revisions | |
| # | |
| # Reliability: | |
| # - remote locking | |
| # | |
| # Security: | |
| # - Authenticated RPC? | |
| from cvslib import CVS, File | |
| import md5 | |
| import os | |
| import string | |
| import sys | |
| from cmdfw import CommandFrameWork | |
| DEF_LOCAL = 1 # Default -l | |
| class MyFile(File): | |
| def action(self): | |
| """Return a code indicating the update status of this file. | |
| The possible return values are: | |
| '=' -- everything's fine | |
| '0' -- file doesn't exist anywhere | |
| '?' -- exists locally only | |
| 'A' -- new locally | |
| 'R' -- deleted locally | |
| 'U' -- changed remotely, no changes locally | |
| (includes new remotely or deleted remotely) | |
| 'M' -- changed locally, no changes remotely | |
| 'C' -- conflict: changed locally as well as remotely | |
| (includes cases where the file has been added | |
| or removed locally and remotely) | |
| 'D' -- deleted remotely | |
| 'N' -- new remotely | |
| 'r' -- get rid of entry | |
| 'c' -- create entry | |
| 'u' -- update entry | |
| (and probably others :-) | |
| """ | |
| if not self.lseen: | |
| self.getlocal() | |
| if not self.rseen: | |
| self.getremote() | |
| if not self.eseen: | |
| if not self.lsum: | |
| if not self.rsum: return '0' # Never heard of | |
| else: | |
| return 'N' # New remotely | |
| else: # self.lsum | |
| if not self.rsum: return '?' # Local only | |
| # Local and remote, but no entry | |
| if self.lsum == self.rsum: | |
| return 'c' # Restore entry only | |
| else: return 'C' # Real conflict | |
| else: # self.eseen | |
| if not self.lsum: | |
| if self.edeleted: | |
| if self.rsum: return 'R' # Removed | |
| else: return 'r' # Get rid of entry | |
| else: # not self.edeleted | |
| if self.rsum: | |
| print "warning:", | |
| print self.file, | |
| print "was lost" | |
| return 'U' | |
| else: return 'r' # Get rid of entry | |
| else: # self.lsum | |
| if not self.rsum: | |
| if self.enew: return 'A' # New locally | |
| else: return 'D' # Deleted remotely | |
| else: # self.rsum | |
| if self.enew: | |
| if self.lsum == self.rsum: | |
| return 'u' | |
| else: | |
| return 'C' | |
| if self.lsum == self.esum: | |
| if self.esum == self.rsum: | |
| return '=' | |
| else: | |
| return 'U' | |
| elif self.esum == self.rsum: | |
| return 'M' | |
| elif self.lsum == self.rsum: | |
| return 'u' | |
| else: | |
| return 'C' | |
| def update(self): | |
| code = self.action() | |
| if code == '=': return | |
| print code, self.file | |
| if code in ('U', 'N'): | |
| self.get() | |
| elif code == 'C': | |
| print "%s: conflict resolution not yet implemented" % \ | |
| self.file | |
| elif code == 'D': | |
| remove(self.file) | |
| self.eseen = 0 | |
| elif code == 'r': | |
| self.eseen = 0 | |
| elif code in ('c', 'u'): | |
| self.eseen = 1 | |
| self.erev = self.rrev | |
| self.enew = 0 | |
| self.edeleted = 0 | |
| self.esum = self.rsum | |
| self.emtime, self.ectime = os.stat(self.file)[-2:] | |
| self.extra = '' | |
| def commit(self, message = ""): | |
| code = self.action() | |
| if code in ('A', 'M'): | |
| self.put(message) | |
| return 1 | |
| elif code == 'R': | |
| print "%s: committing removes not yet implemented" % \ | |
| self.file | |
| elif code == 'C': | |
| print "%s: conflict resolution not yet implemented" % \ | |
| self.file | |
| def diff(self, opts = []): | |
| self.action() # To update lseen, rseen | |
| flags = '' | |
| rev = self.rrev | |
| # XXX should support two rev options too! | |
| for o, a in opts: | |
| if o == '-r': | |
| rev = a | |
| else: | |
| flags = flags + ' ' + o + a | |
| if rev == self.rrev and self.lsum == self.rsum: | |
| return | |
| flags = flags[1:] | |
| fn = self.file | |
| data = self.proxy.get((fn, rev)) | |
| sum = md5.new(data).digest() | |
| if self.lsum == sum: | |
| return | |
| import tempfile | |
| tf = tempfile.NamedTemporaryFile() | |
| tf.write(data) | |
| tf.flush() | |
| print 'diff %s -r%s %s' % (flags, rev, fn) | |
| sts = os.system('diff %s %s %s' % (flags, tf.name, fn)) | |
| if sts: | |
| print '='*70 | |
| def commitcheck(self): | |
| return self.action() != 'C' | |
| def put(self, message = ""): | |
| print "Checking in", self.file, "..." | |
| data = open(self.file).read() | |
| if not self.enew: | |
| self.proxy.lock(self.file) | |
| messages = self.proxy.put(self.file, data, message) | |
| if messages: | |
| print messages | |
| self.setentry(self.proxy.head(self.file), self.lsum) | |
| def get(self): | |
| data = self.proxy.get(self.file) | |
| f = open(self.file, 'w') | |
| f.write(data) | |
| f.close() | |
| self.setentry(self.rrev, self.rsum) | |
| def log(self, otherflags): | |
| print self.proxy.log(self.file, otherflags) | |
| def add(self): | |
| self.eseen = 0 # While we're hacking... | |
| self.esum = self.lsum | |
| self.emtime, self.ectime = 0, 0 | |
| self.erev = '' | |
| self.enew = 1 | |
| self.edeleted = 0 | |
| self.eseen = 1 # Done | |
| self.extra = '' | |
| def setentry(self, erev, esum): | |
| self.eseen = 0 # While we're hacking... | |
| self.esum = esum | |
| self.emtime, self.ectime = os.stat(self.file)[-2:] | |
| self.erev = erev | |
| self.enew = 0 | |
| self.edeleted = 0 | |
| self.eseen = 1 # Done | |
| self.extra = '' | |
| SENDMAIL = "/usr/lib/sendmail -t" | |
| MAILFORM = """To: %s | |
| Subject: CVS changes: %s | |
| ...Message from rcvs... | |
| Committed files: | |
| %s | |
| Log message: | |
| %s | |
| """ | |
| class RCVS(CVS): | |
| FileClass = MyFile | |
| def __init__(self): | |
| CVS.__init__(self) | |
| def update(self, files): | |
| for e in self.whichentries(files, 1): | |
| e.update() | |
| def commit(self, files, message = ""): | |
| list = self.whichentries(files) | |
| if not list: return | |
| ok = 1 | |
| for e in list: | |
| if not e.commitcheck(): | |
| ok = 0 | |
| if not ok: | |
| print "correct above errors first" | |
| return | |
| if not message: | |
| message = raw_input("One-liner: ") | |
| committed = [] | |
| for e in list: | |
| if e.commit(message): | |
| committed.append(e.file) | |
| self.mailinfo(committed, message) | |
| def mailinfo(self, files, message = ""): | |
| towhom = "sjoerd@cwi.nl, jack@cwi.nl" # XXX | |
| mailtext = MAILFORM % (towhom, string.join(files), | |
| string.join(files), message) | |
| print '-'*70 | |
| print mailtext | |
| print '-'*70 | |
| ok = raw_input("OK to mail to %s? " % towhom) | |
| if string.lower(string.strip(ok)) in ('y', 'ye', 'yes'): | |
| p = os.popen(SENDMAIL, "w") | |
| p.write(mailtext) | |
| sts = p.close() | |
| if sts: | |
| print "Sendmail exit status %s" % str(sts) | |
| else: | |
| print "Mail sent." | |
| else: | |
| print "No mail sent." | |
| def report(self, files): | |
| for e in self.whichentries(files): | |
| e.report() | |
| def diff(self, files, opts): | |
| for e in self.whichentries(files): | |
| e.diff(opts) | |
| def add(self, files): | |
| if not files: | |
| raise RuntimeError, "'cvs add' needs at least one file" | |
| list = [] | |
| for e in self.whichentries(files, 1): | |
| e.add() | |
| def rm(self, files): | |
| if not files: | |
| raise RuntimeError, "'cvs rm' needs at least one file" | |
| raise RuntimeError, "'cvs rm' not yet imlemented" | |
| def log(self, files, opts): | |
| flags = '' | |
| for o, a in opts: | |
| flags = flags + ' ' + o + a | |
| for e in self.whichentries(files): | |
| e.log(flags) | |
| def whichentries(self, files, localfilestoo = 0): | |
| if files: | |
| list = [] | |
| for file in files: | |
| if self.entries.has_key(file): | |
| e = self.entries[file] | |
| else: | |
| e = self.FileClass(file) | |
| self.entries[file] = e | |
| list.append(e) | |
| else: | |
| list = self.entries.values() | |
| for file in self.proxy.listfiles(): | |
| if self.entries.has_key(file): | |
| continue | |
| e = self.FileClass(file) | |
| self.entries[file] = e | |
| list.append(e) | |
| if localfilestoo: | |
| for file in os.listdir(os.curdir): | |
| if not self.entries.has_key(file) \ | |
| and not self.ignored(file): | |
| e = self.FileClass(file) | |
| self.entries[file] = e | |
| list.append(e) | |
| list.sort() | |
| if self.proxy: | |
| for e in list: | |
| if e.proxy is None: | |
| e.proxy = self.proxy | |
| return list | |
| class rcvs(CommandFrameWork): | |
| GlobalFlags = 'd:h:p:qvL' | |
| UsageMessage = \ | |
| "usage: rcvs [-d directory] [-h host] [-p port] [-q] [-v] [subcommand arg ...]" | |
| PostUsageMessage = \ | |
| "If no subcommand is given, the status of all files is listed" | |
| def __init__(self): | |
| """Constructor.""" | |
| CommandFrameWork.__init__(self) | |
| self.proxy = None | |
| self.cvs = RCVS() | |
| def close(self): | |
| if self.proxy: | |
| self.proxy._close() | |
| self.proxy = None | |
| def recurse(self): | |
| self.close() | |
| names = os.listdir(os.curdir) | |
| for name in names: | |
| if name == os.curdir or name == os.pardir: | |
| continue | |
| if name == "CVS": | |
| continue | |
| if not os.path.isdir(name): | |
| continue | |
| if os.path.islink(name): | |
| continue | |
| print "--- entering subdirectory", name, "---" | |
| os.chdir(name) | |
| try: | |
| if os.path.isdir("CVS"): | |
| self.__class__().run() | |
| else: | |
| self.recurse() | |
| finally: | |
| os.chdir(os.pardir) | |
| print "--- left subdirectory", name, "---" | |
| def options(self, opts): | |
| self.opts = opts | |
| def ready(self): | |
| import rcsclient | |
| self.proxy = rcsclient.openrcsclient(self.opts) | |
| self.cvs.setproxy(self.proxy) | |
| self.cvs.getentries() | |
| def default(self): | |
| self.cvs.report([]) | |
| def do_report(self, opts, files): | |
| self.cvs.report(files) | |
| def do_update(self, opts, files): | |
| """update [-l] [-R] [file] ...""" | |
| local = DEF_LOCAL | |
| for o, a in opts: | |
| if o == '-l': local = 1 | |
| if o == '-R': local = 0 | |
| self.cvs.update(files) | |
| self.cvs.putentries() | |
| if not local and not files: | |
| self.recurse() | |
| flags_update = '-lR' | |
| do_up = do_update | |
| flags_up = flags_update | |
| def do_commit(self, opts, files): | |
| """commit [-m message] [file] ...""" | |
| message = "" | |
| for o, a in opts: | |
| if o == '-m': message = a | |
| self.cvs.commit(files, message) | |
| self.cvs.putentries() | |
| flags_commit = 'm:' | |
| do_com = do_commit | |
| flags_com = flags_commit | |
| def do_diff(self, opts, files): | |
| """diff [difflags] [file] ...""" | |
| self.cvs.diff(files, opts) | |
| flags_diff = 'cbitwcefhnlr:sD:S:' | |
| do_dif = do_diff | |
| flags_dif = flags_diff | |
| def do_add(self, opts, files): | |
| """add file ...""" | |
| if not files: | |
| print "'rcvs add' requires at least one file" | |
| return | |
| self.cvs.add(files) | |
| self.cvs.putentries() | |
| def do_remove(self, opts, files): | |
| """remove file ...""" | |
| if not files: | |
| print "'rcvs remove' requires at least one file" | |
| return | |
| self.cvs.remove(files) | |
| self.cvs.putentries() | |
| do_rm = do_remove | |
| def do_log(self, opts, files): | |
| """log [rlog-options] [file] ...""" | |
| self.cvs.log(files, opts) | |
| flags_log = 'bhLNRtd:s:V:r:' | |
| def remove(fn): | |
| try: | |
| os.unlink(fn) | |
| except os.error: | |
| pass | |
| def main(): | |
| r = rcvs() | |
| try: | |
| r.run() | |
| finally: | |
| r.close() | |
| if __name__ == "__main__": | |
| main() |