diff options
-rwxr-xr-x | Demo/pdist/rcslib.py | 428 |
1 files changed, 258 insertions, 170 deletions
diff --git a/Demo/pdist/rcslib.py b/Demo/pdist/rcslib.py index 5e4ecc0..7c68543 100755 --- a/Demo/pdist/rcslib.py +++ b/Demo/pdist/rcslib.py @@ -1,168 +1,82 @@ -"""Base local RCS interface module. +"""RCS interface module. -Provides simplified interface to RCS controlled files. Unabashedly -ripped most of this from RCSProxy.py which had in some respects, more -than I needed, and in others, not enough. I think RCSProxy.py could -be rewritten using this module as a base. -""" +Defines the class RCS, which represents a directory with rcs version +files and (possibly) corresponding work files. -__version__ = "guido's, based on: rcs.py,v 1.2 1995/06/22 21:11:37 bwarsaw Exp" +""" -import string +import fnmatch import os +import regsub +import string import tempfile -okchars = string.letters + string.digits + '-_=+.' +class RCS: -class RCSDirectory: + """RCS interface class (local filesystem version). - def __init__(self, directory=None): - self._dirstack = [] - if directory: - self.cd(directory) + An instance of this class represents a directory with rcs version + files and (possible) corresponding work files. - def _close(self): - while self._dirstack: - self.back() + Methods provide access to most rcs operations such as + checkin/checkout, access to the rcs metadata (revisions, logs, + branches etc.) as well as some filesystem operations such as + listing all rcs version files. - def pwd(self): - return os.getcwd() + XXX BUGS / PROBLEMS - def cd(self, name): - save = os.getcwd() - os.chdir(name) - self._dirstack.append(save) + - The instance always represents the current directory so it's not + very useful to have more than one instance around simultaneously - def back(self): - if not self._dirstack: - raise os.error, "empty directory stack" - dir = self._dirstack[-1] - os.chdir(dir) - del self._dirstack[-1] + """ - def _filter(self, files, pat = None): - if pat: - def keep(name, pat = pat): - return fnmatch.fnmatch(name, pat) - files = filter(keep, files) - files.sort() - return files + # Characters allowed in work file names + okchars = string.letters + string.digits + '-_=+.' - def isfile(self, name): - namev = self.rcsname(name) - return namev and (os.path.isfile(namev) or \ - os.path.isfile(os.path.join('RCS', namev))) + def __init__(self): + """Constructor.""" + pass - def isrcs(self, name): - return name[-2:] == ',v' + def __del__(self): + """Destructor.""" + pass - def rcsname(self, name): - if self.isrcs(name): namev = name - else: namev = name + ',v' - if os.path.isfile(namev): return namev - namev = os.path.join('RCS', namev) - if os.path.isfile(namev): return namev - return None + # --- Informational methods about a single file/revision --- - def realname(self, namev): - if self.isrcs(namev): name = namev[:-2] - else: name = namev - if os.path.isfile(name): return name - name = os.path.basename(name) - if os.path.isfile(name): return name - return None + def log(self, name_rev, otherflags = ''): + """Print the full log text for NAME_REV on stdout. - def islocked(self, name): - f = self._open(name, 'rlog -L -R') - line = f.readline() - self._closepipe(f) - if not line: return None - return not self.realname(name) == self.realname(line) + Optional OTHERFLAGS are passed to rlog. - def _unmangle(self, name): - if type(name) == type(''): - rev = '' - else: - name, rev = name - return name, rev + """ + name, rev = self.checkfile(name_rev) + cmd = "rlog -r%s %s %s" % (rev, name, otherflags) + return self._system(cmd) - def checkfile(self, name): - name, rev = self._unmangle(name) - if not self.isfile(name): - raise os.error, 'not an rcs file %s' % `name` - for c in rev: - if c not in okchars: - raise ValueError, "bad char in rev" - return name, rev - - def listfiles(self, pat = None): - files = os.listdir(os.curdir) - files = filter(self.isrcs, files) - if os.path.isdir('RCS'): - files2 = os.listdir('RCS') - files2 = filter(self.isrcs, files2) - files = files + files2 - files = map(self.realname, files) - return self._filter(files, pat) - - def listsubdirs(self, pat = None): - files = os.listdir(os.curdir) - files = filter(os.path.isdir, files) - return self._filter(files, pat) + def head(self, name_rev): + """Return the head revision for NAME_REV""" + dict = self.info(name_rev) + return dict['head'] - def isdir(self, name): - return os.path.isdir(name) + def info(self, name_rev): + """Return a dictionary of info (from rlog -h) for NAME_REV - def _open(self, name, cmd = 'co -p'): - name, rev = self.checkfile(name) - namev = self.rcsname(name) - if rev: - cmd = cmd + ' -r' + rev - return os.popen('%s %s' % (cmd, `namev`)) + The dictionary's keys are the keywords that rlog prints + (e.g. 'head' and its values are the corresponding data + (e.g. '1.3'). - def _closepipe(self, f): - sts = f.close() - if sts: - raise IOError, "Exit status %d" % sts - - def _remove(self, fn): - try: - os.unlink(fn) - except os.error: - pass - - def _list(self, function, list): - if list is None: - list = self.listfiles() - res = [] - for name in list: - try: - res.append((name, function(name))) - except (os.error, IOError): - res.append((name, None)) - return res - - def _dict(self, function, list): - if list is None: - list = self.listfiles() - dict = {} - for name in list: - try: - dict[name] = function(name) - except (os.error, IOError): - pass - return dict + XXX symbolic names and locks are not returned - def info(self, name): - f = self._open(name, 'rlog -h') + """ + f = self._open(name_rev, 'rlog -h') dict = {} while 1: line = f.readline() if not line: break if line[0] == '\t': - # TBD: could be a lock or symbolic name + # XXX could be a lock or symbolic name # Anything else? continue i = string.find(line, ':') @@ -172,59 +86,233 @@ class RCSDirectory: self._closepipe(f) return dict - def head(self, name): - dict = self.info(name) - return dict['head'] + # --- Methods that change files --- - def log(self, name, flags = ''): - f = self._open(name, 'rlog %s 2>&1' % flags) - log = f.read() - self._closepipe(f) - return log + def lock(self, name_rev): + """Set an rcs lock on NAME_REV.""" + name, rev = self.checkfile(name_rev) + cmd = "rcs -l%s %s" % (rev, name) + return self._system(cmd) - def get(self, name, withlock=None, otherflags=""): - """a.k.a. co""" - name, rev = self._unmangle(name) - if not self.isfile(name): - raise os.error, 'not an rcs file %s' % `name` + def unlock(self, name_rev): + """Clear an rcs lock on NAME_REV.""" + name, rev = self.checkfile(name_rev) + cmd = "rcs -u%s %s" % (rev, name) + return self._system(cmd) + + def checkout(self, name_rev, withlock=0, otherflags=""): + """Check out NAME_REV to its work file. + + If optional WITHLOCK is set, check out locked, else unlocked. + + The optional OTHERFLAGS is passed to co without + interpretation. + + Any output from co goes to directly to stdout. + + """ + name, rev = self.checkfile(name_rev) if withlock: lockflag = "-l" else: lockflag = "-u" - cmd = 'co %s%s %s %s 2>&1' % (lockflag, rev, otherflags, name) - sts = os.system(cmd) - if sts: raise IOError, "bad co exit status %d" % sts + cmd = 'co %s%s %s %s' % (lockflag, rev, otherflags, name) + return self._system(cmd) - def put(self, name, message=None, otherflags=""): - """a.k.a. ci""" + def checkin(self, name_rev, message=None, otherflags=""): + """Check in NAME_REV from its work file. + + The optional MESSAGE argument becomes the checkin message + (default "<none>" if None); or the file description if this is + a new file. + + The optional OTHERFLAGS argument is passed to ci without + interpretation. + + Any output from ci goes to directly to stdout. + + """ + name, rev = self._unmangle(name_rev) + new = not self.isvalid(name) if not message: message = "<none>" if message and message[-1] != '\n': message = message + '\n' - name, rev = self._unmangle(name) - new = not self.isfile(name) - # by default, check it in -u so we get an unlocked copy back - # in the current directory + lockflag = "-u" textfile = None try: if new: - for c in name: - if c not in okchars: - raise ValueError, "bad char in name" textfile = tempfile.mktemp() f = open(textfile, 'w') f.write(message) f.close() - cmd = 'ci -u%s -t%s %s %s 2>&1' % \ - (rev, textfile, otherflags, name) + cmd = 'ci %s%s -t%s %s %s' % \ + (lockflag, rev, textfile, otherflags, name) else: - cmd = 'ci -u%s -m"%s" %s %s 2>&1' % \ - (rev, message, otherflags, name) - - sts = os.system(cmd) - if sts: raise IOError, "bad ci exit status %d" % sts + message = regsub.gsub('\([\\"$`]\)', '\\\\\\1', message) + cmd = 'ci %s%s -m"%s" %s %s' % \ + (lockflag, rev, message, otherflags, name) + return self._system(cmd) finally: if textfile: self._remove(textfile) - def mkdir(self, name): - os.mkdir(name, 0777) + # --- Exported support methods --- + + def listfiles(self, pat = None): + """Return a list of all version files matching optional PATTERN.""" + files = os.listdir(os.curdir) + files = filter(self._isrcs, files) + if os.path.isdir('RCS'): + files2 = os.listdir('RCS') + files2 = filter(self._isrcs, files2) + files = files + files2 + files = map(self.realname, files) + return self._filter(files, pat) + + def isvalid(self, name): + """Test whether NAME has a version file associated.""" + namev = self.rcsname(name) + return (os.path.isfile(namev) or + os.path.isfile(os.path.join('RCS', namev))) + + def rcsname(self, name): + """Return the pathname of the version file for NAME. + + The argument can be a work file name or a version file name. + If the version file does not exist, the name of the version + file that would be created by "ci" is returned. + + """ + if self._isrcs(name): namev = name + else: namev = name + ',v' + if os.path.isfile(namev): return namev + namev = os.path.join('RCS', os.path.basename(namev)) + if os.path.isfile(namev): return namev + if os.path.isdir('RCS'): + return os.path.join('RCS', namev) + else: + return namev - def rmdir(self, name): - os.rmdir(name) + def realname(self, namev): + """Return the pathname of the work file for NAME. + + The argument can be a work file name or a version file name. + If the work file does not exist, the name of the work file + that would be created by "co" is returned. + + """ + if self._isrcs(namev): name = namev[:-2] + else: name = namev + if os.path.isfile(name): return name + name = os.path.basename(name) + return name + + def islocked(self, name_rev): + """Test whether FILE (which must have a version file) is locked. + + XXX This does not tell you which revision number is locked and + ignores any revision you may pass in (by virtue of using rlog + -L -R). + + """ + f = self._open(name_rev, 'rlog -L -R') + line = f.readline() + self._closepipe(f) + if not line: return None + return self.realname(name_rev) == self.realname(line) + + def checkfile(self, name_rev): + """Normalize NAME_REV into a (NAME, REV) tuple. + + Raise an exception if there is no corresponding version file. + + """ + name, rev = self._unmangle(name_rev) + if not self.isvalid(name): + raise os.error, 'not an rcs file %s' % `name` + return name, rev + + # --- Internal methods --- + + def _open(self, name_rev, cmd = 'co -p', rflag = '-r'): + """INTERNAL: open a read pipe to NAME_REV using optional COMMAND. + + Optional FLAG is used to indicate the revision (default -r). + + Default COMMAND is "co -p". + + Return a file object connected by a pipe to the command's + output. + + """ + name, rev = self.checkfile(name_rev) + namev = self.rcsname(name) + if rev: + cmd = cmd + ' ' + rflag + rev + return os.popen('%s %s' % (cmd, `namev`)) + + def _unmangle(self, name_rev): + """INTERNAL: Normalize NAME_REV argument to (NAME, REV) tuple. + + Raise an exception if NAME contains invalid characters. + + A NAME_REV argument is either NAME string (implying REV='') or + a tuple of the form (NAME, REV). + + """ + if type(name_rev) == type(''): + name_rev = name, rev = name_rev, '' + else: + name, rev = name_rev + for c in rev: + if c not in self.okchars: + raise ValueError, "bad char in rev" + return name_rev + + def _closepipe(self, f): + """INTERNAL: Close PIPE and print its exit status if nonzero.""" + sts = f.close() + if sts: + raise IOError, "Exit status %d" % sts + + def _system(self, cmd): + """INTERNAL: run COMMAND in a subshell. + + Standard input for the command is taken fron /dev/null. + + Raise IOError when the exit status is not zero. + + Return whatever the calling method should return; normally + None. + + A derived class may override this method and redefine it to + capture stdout/stderr of the command and return it. + + """ + cmd = cmd + " </dev/null" + sts = os.system(cmd) + if sts: raise IOError, "command exit status %d" % sts + + def _filter(self, files, pat = None): + """INTERNAL: Return a sorted copy of the given list of FILES. + + If a second PATTERN argument is given, only files matching it + are kept. No check for valid filenames is made. + + """ + if pat: + def keep(name, pat = pat): + return fnmatch.fnmatch(name, pat) + files = filter(keep, files) + else: + files = files[:] + files.sort() + return files + + def _remove(self, fn): + """INTERNAL: remove FILE without complaints.""" + try: + os.unlink(fn) + except os.error: + pass + + def _isrcs(self, name): + """INTERNAL: Test whether NAME ends in ',v'.""" + return name[-2:] == ',v' |