Home | History | Annotate | Download | only in pdist
      1 """RCS interface module.
      2 
      3 Defines the class RCS, which represents a directory with rcs version
      4 files and (possibly) corresponding work files.
      5 
      6 """
      7 
      8 
      9 import fnmatch
     10 import os
     11 import re
     12 import string
     13 import tempfile
     14 
     15 
     16 class RCS:
     17 
     18     """RCS interface class (local filesystem version).
     19 
     20     An instance of this class represents a directory with rcs version
     21     files and (possible) corresponding work files.
     22 
     23     Methods provide access to most rcs operations such as
     24     checkin/checkout, access to the rcs metadata (revisions, logs,
     25     branches etc.) as well as some filesystem operations such as
     26     listing all rcs version files.
     27 
     28     XXX BUGS / PROBLEMS
     29 
     30     - The instance always represents the current directory so it's not
     31     very useful to have more than one instance around simultaneously
     32 
     33     """
     34 
     35     # Characters allowed in work file names

     36     okchars = string.ascii_letters + string.digits + '-_=+'
     37 
     38     def __init__(self):
     39         """Constructor."""
     40         pass
     41 
     42     def __del__(self):
     43         """Destructor."""
     44         pass
     45 
     46     # --- Informational methods about a single file/revision ---

     47 
     48     def log(self, name_rev, otherflags = ''):
     49         """Return the full log text for NAME_REV as a string.
     50 
     51         Optional OTHERFLAGS are passed to rlog.
     52 
     53         """
     54         f = self._open(name_rev, 'rlog ' + otherflags)
     55         data = f.read()
     56         status = self._closepipe(f)
     57         if status:
     58             data = data + "%s: %s" % status
     59         elif data[-1] == '\n':
     60             data = data[:-1]
     61         return data
     62 
     63     def head(self, name_rev):
     64         """Return the head revision for NAME_REV"""
     65         dict = self.info(name_rev)
     66         return dict['head']
     67 
     68     def info(self, name_rev):
     69         """Return a dictionary of info (from rlog -h) for NAME_REV
     70 
     71         The dictionary's keys are the keywords that rlog prints
     72         (e.g. 'head' and its values are the corresponding data
     73         (e.g. '1.3').
     74 
     75         XXX symbolic names and locks are not returned
     76 
     77         """
     78         f = self._open(name_rev, 'rlog -h')
     79         dict = {}
     80         while 1:
     81             line = f.readline()
     82             if not line: break
     83             if line[0] == '\t':
     84                 # XXX could be a lock or symbolic name

     85                 # Anything else?

     86                 continue
     87             i = string.find(line, ':')
     88             if i > 0:
     89                 key, value = line[:i], string.strip(line[i+1:])
     90                 dict[key] = value
     91         status = self._closepipe(f)
     92         if status:
     93             raise IOError, status
     94         return dict
     95 
     96     # --- Methods that change files ---

     97 
     98     def lock(self, name_rev):
     99         """Set an rcs lock on NAME_REV."""
    100         name, rev = self.checkfile(name_rev)
    101         cmd = "rcs -l%s %s" % (rev, name)
    102         return self._system(cmd)
    103 
    104     def unlock(self, name_rev):
    105         """Clear an rcs lock on NAME_REV."""
    106         name, rev = self.checkfile(name_rev)
    107         cmd = "rcs -u%s %s" % (rev, name)
    108         return self._system(cmd)
    109 
    110     def checkout(self, name_rev, withlock=0, otherflags=""):
    111         """Check out NAME_REV to its work file.
    112 
    113         If optional WITHLOCK is set, check out locked, else unlocked.
    114 
    115         The optional OTHERFLAGS is passed to co without
    116         interpretation.
    117 
    118         Any output from co goes to directly to stdout.
    119 
    120         """
    121         name, rev = self.checkfile(name_rev)
    122         if withlock: lockflag = "-l"
    123         else: lockflag = "-u"
    124         cmd = 'co %s%s %s %s' % (lockflag, rev, otherflags, name)
    125         return self._system(cmd)
    126 
    127     def checkin(self, name_rev, message=None, otherflags=""):
    128         """Check in NAME_REV from its work file.
    129 
    130         The optional MESSAGE argument becomes the checkin message
    131         (default "<none>" if None); or the file description if this is
    132         a new file.
    133 
    134         The optional OTHERFLAGS argument is passed to ci without
    135         interpretation.
    136 
    137         Any output from ci goes to directly to stdout.
    138 
    139         """
    140         name, rev = self._unmangle(name_rev)
    141         new = not self.isvalid(name)
    142         if not message: message = "<none>"
    143         if message and message[-1] != '\n':
    144             message = message + '\n'
    145         lockflag = "-u"
    146         if new:
    147             f = tempfile.NamedTemporaryFile()
    148             f.write(message)
    149             f.flush()
    150             cmd = 'ci %s%s -t%s %s %s' % \
    151                   (lockflag, rev, f.name, otherflags, name)
    152         else:
    153             message = re.sub(r'([\"$`])', r'\\\1', message)
    154             cmd = 'ci %s%s -m"%s" %s %s' % \
    155                   (lockflag, rev, message, otherflags, name)
    156         return self._system(cmd)
    157 
    158     # --- Exported support methods ---

    159 
    160     def listfiles(self, pat = None):
    161         """Return a list of all version files matching optional PATTERN."""
    162         files = os.listdir(os.curdir)
    163         files = filter(self._isrcs, files)
    164         if os.path.isdir('RCS'):
    165             files2 = os.listdir('RCS')
    166             files2 = filter(self._isrcs, files2)
    167             files = files + files2
    168         files = map(self.realname, files)
    169         return self._filter(files, pat)
    170 
    171     def isvalid(self, name):
    172         """Test whether NAME has a version file associated."""
    173         namev = self.rcsname(name)
    174         return (os.path.isfile(namev) or
    175                 os.path.isfile(os.path.join('RCS', namev)))
    176 
    177     def rcsname(self, name):
    178         """Return the pathname of the version file for NAME.
    179 
    180         The argument can be a work file name or a version file name.
    181         If the version file does not exist, the name of the version
    182         file that would be created by "ci" is returned.
    183 
    184         """
    185         if self._isrcs(name): namev = name
    186         else: namev = name + ',v'
    187         if os.path.isfile(namev): return namev
    188         namev = os.path.join('RCS', os.path.basename(namev))
    189         if os.path.isfile(namev): return namev
    190         if os.path.isdir('RCS'):
    191             return os.path.join('RCS', namev)
    192         else:
    193             return namev
    194 
    195     def realname(self, namev):
    196         """Return the pathname of the work file for NAME.
    197 
    198         The argument can be a work file name or a version file name.
    199         If the work file does not exist, the name of the work file
    200         that would be created by "co" is returned.
    201 
    202         """
    203         if self._isrcs(namev): name = namev[:-2]
    204         else: name = namev
    205         if os.path.isfile(name): return name
    206         name = os.path.basename(name)
    207         return name
    208 
    209     def islocked(self, name_rev):
    210         """Test whether FILE (which must have a version file) is locked.
    211 
    212         XXX This does not tell you which revision number is locked and
    213         ignores any revision you may pass in (by virtue of using rlog
    214         -L -R).
    215 
    216         """
    217         f = self._open(name_rev, 'rlog -L -R')
    218         line = f.readline()
    219         status = self._closepipe(f)
    220         if status:
    221             raise IOError, status
    222         if not line: return None
    223         if line[-1] == '\n':
    224             line = line[:-1]
    225         return self.realname(name_rev) == self.realname(line)
    226 
    227     def checkfile(self, name_rev):
    228         """Normalize NAME_REV into a (NAME, REV) tuple.
    229 
    230         Raise an exception if there is no corresponding version file.
    231 
    232         """
    233         name, rev = self._unmangle(name_rev)
    234         if not self.isvalid(name):
    235             raise os.error, 'not an rcs file %r' % (name,)
    236         return name, rev
    237 
    238     # --- Internal methods ---

    239 
    240     def _open(self, name_rev, cmd = 'co -p', rflag = '-r'):
    241         """INTERNAL: open a read pipe to NAME_REV using optional COMMAND.
    242 
    243         Optional FLAG is used to indicate the revision (default -r).
    244 
    245         Default COMMAND is "co -p".
    246 
    247         Return a file object connected by a pipe to the command's
    248         output.
    249 
    250         """
    251         name, rev = self.checkfile(name_rev)
    252         namev = self.rcsname(name)
    253         if rev:
    254             cmd = cmd + ' ' + rflag + rev
    255         return os.popen("%s %r" % (cmd, namev))
    256 
    257     def _unmangle(self, name_rev):
    258         """INTERNAL: Normalize NAME_REV argument to (NAME, REV) tuple.
    259 
    260         Raise an exception if NAME contains invalid characters.
    261 
    262         A NAME_REV argument is either NAME string (implying REV='') or
    263         a tuple of the form (NAME, REV).
    264 
    265         """
    266         if type(name_rev) == type(''):
    267             name_rev = name, rev = name_rev, ''
    268         else:
    269             name, rev = name_rev
    270         for c in rev:
    271             if c not in self.okchars:
    272                 raise ValueError, "bad char in rev"
    273         return name_rev
    274 
    275     def _closepipe(self, f):
    276         """INTERNAL: Close PIPE and print its exit status if nonzero."""
    277         sts = f.close()
    278         if not sts: return None
    279         detail, reason = divmod(sts, 256)
    280         if reason == 0: return 'exit', detail   # Exit status

    281         signal = reason&0x7F
    282         if signal == 0x7F:
    283             code = 'stopped'
    284             signal = detail
    285         else:
    286             code = 'killed'
    287         if reason&0x80:
    288             code = code + '(coredump)'
    289         return code, signal
    290 
    291     def _system(self, cmd):
    292         """INTERNAL: run COMMAND in a subshell.
    293 
    294         Standard input for the command is taken from /dev/null.
    295 
    296         Raise IOError when the exit status is not zero.
    297 
    298         Return whatever the calling method should return; normally
    299         None.
    300 
    301         A derived class may override this method and redefine it to
    302         capture stdout/stderr of the command and return it.
    303 
    304         """
    305         cmd = cmd + " </dev/null"
    306         sts = os.system(cmd)
    307         if sts: raise IOError, "command exit status %d" % sts
    308 
    309     def _filter(self, files, pat = None):
    310         """INTERNAL: Return a sorted copy of the given list of FILES.
    311 
    312         If a second PATTERN argument is given, only files matching it
    313         are kept.  No check for valid filenames is made.
    314 
    315         """
    316         if pat:
    317             def keep(name, pat = pat):
    318                 return fnmatch.fnmatch(name, pat)
    319             files = filter(keep, files)
    320         else:
    321             files = files[:]
    322         files.sort()
    323         return files
    324 
    325     def _remove(self, fn):
    326         """INTERNAL: remove FILE without complaints."""
    327         try:
    328             os.unlink(fn)
    329         except os.error:
    330             pass
    331 
    332     def _isrcs(self, name):
    333         """INTERNAL: Test whether NAME ends in ',v'."""
    334         return name[-2:] == ',v'
    335