| #! /usr/bin/env python3 | 
 |  | 
 | """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:", end=' ') | 
 |                         print(self.file, end=' ') | 
 |                         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 = 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 = 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 file in self.entries: | 
 |                     e = self.entries[file] | 
 |                 else: | 
 |                     e = self.FileClass(file) | 
 |                     self.entries[file] = e | 
 |                 list.append(e) | 
 |         else: | 
 |             list = list(self.entries.values()) | 
 |             for file in self.proxy.listfiles(): | 
 |                 if file in self.entries: | 
 |                     continue | 
 |                 e = self.FileClass(file) | 
 |                 self.entries[file] = e | 
 |                 list.append(e) | 
 |             if localfilestoo: | 
 |                 for file in os.listdir(os.curdir): | 
 |                     if file not in self.entries \ | 
 |                        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() |