Home | History | Annotate | Download | only in pdist
      1 #! /usr/bin/env python

      2 
      3 """Remote CVS -- command line interface"""
      4 
      5 # XXX To do:

      6 #

      7 # Bugs:

      8 # - if the remote file is deleted, "rcvs update" will fail

      9 #

     10 # Functionality:

     11 # - cvs rm

     12 # - descend into directories (alraedy done for update)

     13 # - conflict resolution

     14 # - other relevant commands?

     15 # - branches

     16 #

     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

     23 #

     24 # Performance:

     25 # - cache remote checksums (for every revision ever seen!)

     26 # - translate symbolic revisions to numeric revisions

     27 #

     28 # Reliability:

     29 # - remote locking

     30 #

     31 # Security:

     32 # - Authenticated RPC?

     33 
     34 
     35 from cvslib import CVS, File
     36 import md5
     37 import os
     38 import string
     39 import sys
     40 from cmdfw import CommandFrameWork
     41 
     42 
     43 DEF_LOCAL = 1                           # Default -l

     44 
     45 
     46 class MyFile(File):
     47 
     48     def action(self):
     49         """Return a code indicating the update status of this file.
     50 
     51         The possible return values are:
     52 
     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
     69 
     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'
    120 
    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 = ''
    143 
    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
    155 
    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
    182 
    183     def commitcheck(self):
    184         return self.action() != 'C'
    185 
    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)
    195 
    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)
    202 
    203     def log(self, otherflags):
    204         print self.proxy.log(self.file, otherflags)
    205 
    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 = ''
    215 
    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 = ''
    225 
    226 
    227 SENDMAIL = "/usr/lib/sendmail -t"
    228 MAILFORM = """To: %s
    229 Subject: CVS changes: %s
    230 
    231 ...Message from rcvs...
    232 
    233 Committed files:
    234         %s
    235 
    236 Log message:
    237         %s
    238 """
    239 
    240 
    241 class RCVS(CVS):
    242 
    243     FileClass = MyFile
    244 
    245     def __init__(self):
    246         CVS.__init__(self)
    247 
    248     def update(self, files):
    249         for e in self.whichentries(files, 1):
    250             e.update()
    251 
    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)
    269 
    270     def mailinfo(self, files, message = ""):
    271         towhom = "sjoerd (at] cwi.nl, jack (at] 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."
    288 
    289     def report(self, files):
    290         for e in self.whichentries(files):
    291             e.report()
    292 
    293     def diff(self, files, opts):
    294         for e in self.whichentries(files):
    295             e.diff(opts)
    296 
    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()
    303 
    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"
    308 
    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)
    315 
    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
    347 
    348 
    349 class rcvs(CommandFrameWork):
    350 
    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"
    356 
    357     def __init__(self):
    358         """Constructor."""
    359         CommandFrameWork.__init__(self)
    360         self.proxy = None
    361         self.cvs = RCVS()
    362 
    363     def close(self):
    364         if self.proxy:
    365             self.proxy._close()
    366         self.proxy = None
    367 
    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, "---"
    390 
    391     def options(self, opts):
    392         self.opts = opts
    393 
    394     def ready(self):
    395         import rcsclient
    396         self.proxy = rcsclient.openrcsclient(self.opts)
    397         self.cvs.setproxy(self.proxy)
    398         self.cvs.getentries()
    399 
    400     def default(self):
    401         self.cvs.report([])
    402 
    403     def do_report(self, opts, files):
    404         self.cvs.report(files)
    405 
    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
    419 
    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
    430 
    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
    437 
    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()
    445 
    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
    454 
    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:'
    459 
    460 
    461 def remove(fn):
    462     try:
    463         os.unlink(fn)
    464     except os.error:
    465         pass
    466 
    467 
    468 def main():
    469     r = rcvs()
    470     try:
    471         r.run()
    472     finally:
    473         r.close()
    474 
    475 
    476 if __name__ == "__main__":
    477     main()
    478