| #! /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 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, ' '.join(files), |
| ' '.join(files), message) |
| print '-'*70 |
| print mailtext |
| print '-'*70 |
| ok = raw_input("OK to mail to %s? " % towhom) |
| if ok.lower().strip() 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() |