Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 1 | """RCS interface module. |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 2 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 3 | Defines the class RCS, which represents a directory with rcs version |
| 4 | files and (possibly) corresponding work files. |
| 5 | |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 6 | """ |
| 7 | |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 8 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 9 | import fnmatch |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 10 | import os |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 11 | import regsub |
| 12 | import string |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 13 | import tempfile |
| 14 | |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 15 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 16 | class RCS: |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 17 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 18 | """RCS interface class (local filesystem version). |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 19 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 20 | An instance of this class represents a directory with rcs version |
| 21 | files and (possible) corresponding work files. |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 22 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 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. |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 27 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 28 | XXX BUGS / PROBLEMS |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 29 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 30 | - The instance always represents the current directory so it's not |
| 31 | very useful to have more than one instance around simultaneously |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 32 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 33 | """ |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 34 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 35 | # Characters allowed in work file names |
| 36 | okchars = string.letters + string.digits + '-_=+.' |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 37 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 38 | def __init__(self): |
| 39 | """Constructor.""" |
| 40 | pass |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 41 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 42 | def __del__(self): |
| 43 | """Destructor.""" |
| 44 | pass |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 45 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 46 | # --- Informational methods about a single file/revision --- |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 47 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 48 | def log(self, name_rev, otherflags = ''): |
Guido van Rossum | bffda89 | 1995-10-07 19:46:08 +0000 | [diff] [blame^] | 49 | """Return the full log text for NAME_REV as a string. |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 50 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 51 | Optional OTHERFLAGS are passed to rlog. |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 52 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 53 | """ |
Guido van Rossum | bffda89 | 1995-10-07 19:46:08 +0000 | [diff] [blame^] | 54 | f = self._open(name_rev, 'rlog ' + otherflags) |
| 55 | data = f.read() |
| 56 | self._closepipe(f) |
| 57 | return data |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 58 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 59 | def head(self, name_rev): |
| 60 | """Return the head revision for NAME_REV""" |
| 61 | dict = self.info(name_rev) |
| 62 | return dict['head'] |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 63 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 64 | def info(self, name_rev): |
| 65 | """Return a dictionary of info (from rlog -h) for NAME_REV |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 66 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 67 | The dictionary's keys are the keywords that rlog prints |
| 68 | (e.g. 'head' and its values are the corresponding data |
| 69 | (e.g. '1.3'). |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 70 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 71 | XXX symbolic names and locks are not returned |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 72 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 73 | """ |
| 74 | f = self._open(name_rev, 'rlog -h') |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 75 | dict = {} |
| 76 | while 1: |
| 77 | line = f.readline() |
| 78 | if not line: break |
| 79 | if line[0] == '\t': |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 80 | # XXX could be a lock or symbolic name |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 81 | # Anything else? |
| 82 | continue |
| 83 | i = string.find(line, ':') |
| 84 | if i > 0: |
| 85 | key, value = line[:i], string.strip(line[i+1:]) |
| 86 | dict[key] = value |
| 87 | self._closepipe(f) |
| 88 | return dict |
| 89 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 90 | # --- Methods that change files --- |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 91 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 92 | def lock(self, name_rev): |
| 93 | """Set an rcs lock on NAME_REV.""" |
| 94 | name, rev = self.checkfile(name_rev) |
| 95 | cmd = "rcs -l%s %s" % (rev, name) |
| 96 | return self._system(cmd) |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 97 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 98 | def unlock(self, name_rev): |
| 99 | """Clear an rcs lock on NAME_REV.""" |
| 100 | name, rev = self.checkfile(name_rev) |
| 101 | cmd = "rcs -u%s %s" % (rev, name) |
| 102 | return self._system(cmd) |
| 103 | |
| 104 | def checkout(self, name_rev, withlock=0, otherflags=""): |
| 105 | """Check out NAME_REV to its work file. |
| 106 | |
| 107 | If optional WITHLOCK is set, check out locked, else unlocked. |
| 108 | |
| 109 | The optional OTHERFLAGS is passed to co without |
| 110 | interpretation. |
| 111 | |
| 112 | Any output from co goes to directly to stdout. |
| 113 | |
| 114 | """ |
| 115 | name, rev = self.checkfile(name_rev) |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 116 | if withlock: lockflag = "-l" |
| 117 | else: lockflag = "-u" |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 118 | cmd = 'co %s%s %s %s' % (lockflag, rev, otherflags, name) |
| 119 | return self._system(cmd) |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 120 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 121 | def checkin(self, name_rev, message=None, otherflags=""): |
| 122 | """Check in NAME_REV from its work file. |
| 123 | |
| 124 | The optional MESSAGE argument becomes the checkin message |
| 125 | (default "<none>" if None); or the file description if this is |
| 126 | a new file. |
| 127 | |
| 128 | The optional OTHERFLAGS argument is passed to ci without |
| 129 | interpretation. |
| 130 | |
| 131 | Any output from ci goes to directly to stdout. |
| 132 | |
| 133 | """ |
| 134 | name, rev = self._unmangle(name_rev) |
| 135 | new = not self.isvalid(name) |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 136 | if not message: message = "<none>" |
| 137 | if message and message[-1] != '\n': |
| 138 | message = message + '\n' |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 139 | lockflag = "-u" |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 140 | textfile = None |
| 141 | try: |
| 142 | if new: |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 143 | textfile = tempfile.mktemp() |
| 144 | f = open(textfile, 'w') |
| 145 | f.write(message) |
| 146 | f.close() |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 147 | cmd = 'ci %s%s -t%s %s %s' % \ |
| 148 | (lockflag, rev, textfile, otherflags, name) |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 149 | else: |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 150 | message = regsub.gsub('\([\\"$`]\)', '\\\\\\1', message) |
| 151 | cmd = 'ci %s%s -m"%s" %s %s' % \ |
| 152 | (lockflag, rev, message, otherflags, name) |
| 153 | return self._system(cmd) |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 154 | finally: |
| 155 | if textfile: self._remove(textfile) |
| 156 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 157 | # --- Exported support methods --- |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 158 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 159 | def listfiles(self, pat = None): |
| 160 | """Return a list of all version files matching optional PATTERN.""" |
| 161 | files = os.listdir(os.curdir) |
| 162 | files = filter(self._isrcs, files) |
| 163 | if os.path.isdir('RCS'): |
| 164 | files2 = os.listdir('RCS') |
| 165 | files2 = filter(self._isrcs, files2) |
| 166 | files = files + files2 |
| 167 | files = map(self.realname, files) |
| 168 | return self._filter(files, pat) |
| 169 | |
| 170 | def isvalid(self, name): |
| 171 | """Test whether NAME has a version file associated.""" |
| 172 | namev = self.rcsname(name) |
| 173 | return (os.path.isfile(namev) or |
| 174 | os.path.isfile(os.path.join('RCS', namev))) |
| 175 | |
| 176 | def rcsname(self, name): |
| 177 | """Return the pathname of the version file for NAME. |
| 178 | |
| 179 | The argument can be a work file name or a version file name. |
| 180 | If the version file does not exist, the name of the version |
| 181 | file that would be created by "ci" is returned. |
| 182 | |
| 183 | """ |
| 184 | if self._isrcs(name): namev = name |
| 185 | else: namev = name + ',v' |
| 186 | if os.path.isfile(namev): return namev |
| 187 | namev = os.path.join('RCS', os.path.basename(namev)) |
| 188 | if os.path.isfile(namev): return namev |
| 189 | if os.path.isdir('RCS'): |
| 190 | return os.path.join('RCS', namev) |
| 191 | else: |
| 192 | return namev |
| 193 | |
| 194 | def realname(self, namev): |
| 195 | """Return the pathname of the work file for NAME. |
| 196 | |
| 197 | The argument can be a work file name or a version file name. |
| 198 | If the work file does not exist, the name of the work file |
| 199 | that would be created by "co" is returned. |
| 200 | |
| 201 | """ |
| 202 | if self._isrcs(namev): name = namev[:-2] |
| 203 | else: name = namev |
| 204 | if os.path.isfile(name): return name |
| 205 | name = os.path.basename(name) |
| 206 | return name |
| 207 | |
| 208 | def islocked(self, name_rev): |
| 209 | """Test whether FILE (which must have a version file) is locked. |
| 210 | |
| 211 | XXX This does not tell you which revision number is locked and |
| 212 | ignores any revision you may pass in (by virtue of using rlog |
| 213 | -L -R). |
| 214 | |
| 215 | """ |
| 216 | f = self._open(name_rev, 'rlog -L -R') |
| 217 | line = f.readline() |
| 218 | self._closepipe(f) |
| 219 | if not line: return None |
| 220 | return self.realname(name_rev) == self.realname(line) |
| 221 | |
| 222 | def checkfile(self, name_rev): |
| 223 | """Normalize NAME_REV into a (NAME, REV) tuple. |
| 224 | |
| 225 | Raise an exception if there is no corresponding version file. |
| 226 | |
| 227 | """ |
| 228 | name, rev = self._unmangle(name_rev) |
| 229 | if not self.isvalid(name): |
| 230 | raise os.error, 'not an rcs file %s' % `name` |
| 231 | return name, rev |
| 232 | |
| 233 | # --- Internal methods --- |
| 234 | |
| 235 | def _open(self, name_rev, cmd = 'co -p', rflag = '-r'): |
| 236 | """INTERNAL: open a read pipe to NAME_REV using optional COMMAND. |
| 237 | |
| 238 | Optional FLAG is used to indicate the revision (default -r). |
| 239 | |
| 240 | Default COMMAND is "co -p". |
| 241 | |
| 242 | Return a file object connected by a pipe to the command's |
| 243 | output. |
| 244 | |
| 245 | """ |
| 246 | name, rev = self.checkfile(name_rev) |
| 247 | namev = self.rcsname(name) |
| 248 | if rev: |
| 249 | cmd = cmd + ' ' + rflag + rev |
| 250 | return os.popen('%s %s' % (cmd, `namev`)) |
| 251 | |
| 252 | def _unmangle(self, name_rev): |
| 253 | """INTERNAL: Normalize NAME_REV argument to (NAME, REV) tuple. |
| 254 | |
| 255 | Raise an exception if NAME contains invalid characters. |
| 256 | |
| 257 | A NAME_REV argument is either NAME string (implying REV='') or |
| 258 | a tuple of the form (NAME, REV). |
| 259 | |
| 260 | """ |
| 261 | if type(name_rev) == type(''): |
| 262 | name_rev = name, rev = name_rev, '' |
| 263 | else: |
| 264 | name, rev = name_rev |
| 265 | for c in rev: |
| 266 | if c not in self.okchars: |
| 267 | raise ValueError, "bad char in rev" |
| 268 | return name_rev |
| 269 | |
| 270 | def _closepipe(self, f): |
| 271 | """INTERNAL: Close PIPE and print its exit status if nonzero.""" |
| 272 | sts = f.close() |
| 273 | if sts: |
| 274 | raise IOError, "Exit status %d" % sts |
| 275 | |
| 276 | def _system(self, cmd): |
| 277 | """INTERNAL: run COMMAND in a subshell. |
| 278 | |
| 279 | Standard input for the command is taken fron /dev/null. |
| 280 | |
| 281 | Raise IOError when the exit status is not zero. |
| 282 | |
| 283 | Return whatever the calling method should return; normally |
| 284 | None. |
| 285 | |
| 286 | A derived class may override this method and redefine it to |
| 287 | capture stdout/stderr of the command and return it. |
| 288 | |
| 289 | """ |
| 290 | cmd = cmd + " </dev/null" |
| 291 | sts = os.system(cmd) |
| 292 | if sts: raise IOError, "command exit status %d" % sts |
| 293 | |
| 294 | def _filter(self, files, pat = None): |
| 295 | """INTERNAL: Return a sorted copy of the given list of FILES. |
| 296 | |
| 297 | If a second PATTERN argument is given, only files matching it |
| 298 | are kept. No check for valid filenames is made. |
| 299 | |
| 300 | """ |
| 301 | if pat: |
| 302 | def keep(name, pat = pat): |
| 303 | return fnmatch.fnmatch(name, pat) |
| 304 | files = filter(keep, files) |
| 305 | else: |
| 306 | files = files[:] |
| 307 | files.sort() |
| 308 | return files |
| 309 | |
| 310 | def _remove(self, fn): |
| 311 | """INTERNAL: remove FILE without complaints.""" |
| 312 | try: |
| 313 | os.unlink(fn) |
| 314 | except os.error: |
| 315 | pass |
| 316 | |
| 317 | def _isrcs(self, name): |
| 318 | """INTERNAL: Test whether NAME ends in ',v'.""" |
| 319 | return name[-2:] == ',v' |