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() |
Guido van Rossum | c0c01f7 | 1995-10-07 20:48:17 +0000 | [diff] [blame] | 56 | status = self._closepipe(f) |
| 57 | if status: |
| 58 | data = data + "%s: %s" % status |
| 59 | elif data[-1] == '\n': |
| 60 | data = data[:-1] |
Guido van Rossum | bffda89 | 1995-10-07 19:46:08 +0000 | [diff] [blame] | 61 | return data |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 62 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 63 | def head(self, name_rev): |
| 64 | """Return the head revision for NAME_REV""" |
| 65 | dict = self.info(name_rev) |
| 66 | return dict['head'] |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 67 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 68 | def info(self, name_rev): |
| 69 | """Return a dictionary of info (from rlog -h) for NAME_REV |
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 | 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'). |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 74 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 75 | XXX symbolic names and locks are not returned |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 76 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 77 | """ |
| 78 | f = self._open(name_rev, 'rlog -h') |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 79 | dict = {} |
| 80 | while 1: |
| 81 | line = f.readline() |
| 82 | if not line: break |
| 83 | if line[0] == '\t': |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 84 | # XXX could be a lock or symbolic name |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 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 |
Guido van Rossum | c0c01f7 | 1995-10-07 20:48:17 +0000 | [diff] [blame] | 91 | status = self._closepipe(f) |
| 92 | if status: |
| 93 | raise IOError, status |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 94 | return dict |
| 95 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 96 | # --- Methods that change files --- |
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 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) |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 103 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 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) |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 122 | if withlock: lockflag = "-l" |
| 123 | else: lockflag = "-u" |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 124 | cmd = 'co %s%s %s %s' % (lockflag, rev, otherflags, name) |
| 125 | return self._system(cmd) |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 126 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 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) |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 142 | if not message: message = "<none>" |
| 143 | if message and message[-1] != '\n': |
| 144 | message = message + '\n' |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 145 | lockflag = "-u" |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 146 | textfile = None |
| 147 | try: |
| 148 | if new: |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 149 | textfile = tempfile.mktemp() |
| 150 | f = open(textfile, 'w') |
| 151 | f.write(message) |
| 152 | f.close() |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 153 | cmd = 'ci %s%s -t%s %s %s' % \ |
| 154 | (lockflag, rev, textfile, otherflags, name) |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 155 | else: |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 156 | message = regsub.gsub('\([\\"$`]\)', '\\\\\\1', message) |
| 157 | cmd = 'ci %s%s -m"%s" %s %s' % \ |
| 158 | (lockflag, rev, message, otherflags, name) |
| 159 | return self._system(cmd) |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 160 | finally: |
| 161 | if textfile: self._remove(textfile) |
| 162 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 163 | # --- Exported support methods --- |
Guido van Rossum | 79ed32d | 1995-06-23 14:40:06 +0000 | [diff] [blame] | 164 | |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 165 | def listfiles(self, pat = None): |
| 166 | """Return a list of all version files matching optional PATTERN.""" |
| 167 | files = os.listdir(os.curdir) |
| 168 | files = filter(self._isrcs, files) |
| 169 | if os.path.isdir('RCS'): |
| 170 | files2 = os.listdir('RCS') |
| 171 | files2 = filter(self._isrcs, files2) |
| 172 | files = files + files2 |
| 173 | files = map(self.realname, files) |
| 174 | return self._filter(files, pat) |
| 175 | |
| 176 | def isvalid(self, name): |
| 177 | """Test whether NAME has a version file associated.""" |
| 178 | namev = self.rcsname(name) |
| 179 | return (os.path.isfile(namev) or |
| 180 | os.path.isfile(os.path.join('RCS', namev))) |
| 181 | |
| 182 | def rcsname(self, name): |
| 183 | """Return the pathname of the version file for NAME. |
| 184 | |
| 185 | The argument can be a work file name or a version file name. |
| 186 | If the version file does not exist, the name of the version |
| 187 | file that would be created by "ci" is returned. |
| 188 | |
| 189 | """ |
| 190 | if self._isrcs(name): namev = name |
| 191 | else: namev = name + ',v' |
| 192 | if os.path.isfile(namev): return namev |
| 193 | namev = os.path.join('RCS', os.path.basename(namev)) |
| 194 | if os.path.isfile(namev): return namev |
| 195 | if os.path.isdir('RCS'): |
| 196 | return os.path.join('RCS', namev) |
| 197 | else: |
| 198 | return namev |
| 199 | |
| 200 | def realname(self, namev): |
| 201 | """Return the pathname of the work file for NAME. |
| 202 | |
| 203 | The argument can be a work file name or a version file name. |
| 204 | If the work file does not exist, the name of the work file |
| 205 | that would be created by "co" is returned. |
| 206 | |
| 207 | """ |
| 208 | if self._isrcs(namev): name = namev[:-2] |
| 209 | else: name = namev |
| 210 | if os.path.isfile(name): return name |
| 211 | name = os.path.basename(name) |
| 212 | return name |
| 213 | |
| 214 | def islocked(self, name_rev): |
| 215 | """Test whether FILE (which must have a version file) is locked. |
| 216 | |
| 217 | XXX This does not tell you which revision number is locked and |
| 218 | ignores any revision you may pass in (by virtue of using rlog |
| 219 | -L -R). |
| 220 | |
| 221 | """ |
| 222 | f = self._open(name_rev, 'rlog -L -R') |
| 223 | line = f.readline() |
Guido van Rossum | c0c01f7 | 1995-10-07 20:48:17 +0000 | [diff] [blame] | 224 | status = self._closepipe(f) |
| 225 | if status: |
| 226 | raise IOError, status |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 227 | if not line: return None |
| 228 | return self.realname(name_rev) == self.realname(line) |
| 229 | |
| 230 | def checkfile(self, name_rev): |
| 231 | """Normalize NAME_REV into a (NAME, REV) tuple. |
| 232 | |
| 233 | Raise an exception if there is no corresponding version file. |
| 234 | |
| 235 | """ |
| 236 | name, rev = self._unmangle(name_rev) |
| 237 | if not self.isvalid(name): |
| 238 | raise os.error, 'not an rcs file %s' % `name` |
| 239 | return name, rev |
| 240 | |
| 241 | # --- Internal methods --- |
| 242 | |
| 243 | def _open(self, name_rev, cmd = 'co -p', rflag = '-r'): |
| 244 | """INTERNAL: open a read pipe to NAME_REV using optional COMMAND. |
| 245 | |
| 246 | Optional FLAG is used to indicate the revision (default -r). |
| 247 | |
| 248 | Default COMMAND is "co -p". |
| 249 | |
| 250 | Return a file object connected by a pipe to the command's |
| 251 | output. |
| 252 | |
| 253 | """ |
| 254 | name, rev = self.checkfile(name_rev) |
| 255 | namev = self.rcsname(name) |
| 256 | if rev: |
| 257 | cmd = cmd + ' ' + rflag + rev |
Guido van Rossum | c0c01f7 | 1995-10-07 20:48:17 +0000 | [diff] [blame] | 258 | return os.popen("%s %s" % (cmd, `namev`)) |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 259 | |
| 260 | def _unmangle(self, name_rev): |
| 261 | """INTERNAL: Normalize NAME_REV argument to (NAME, REV) tuple. |
| 262 | |
| 263 | Raise an exception if NAME contains invalid characters. |
| 264 | |
| 265 | A NAME_REV argument is either NAME string (implying REV='') or |
| 266 | a tuple of the form (NAME, REV). |
| 267 | |
| 268 | """ |
| 269 | if type(name_rev) == type(''): |
| 270 | name_rev = name, rev = name_rev, '' |
| 271 | else: |
| 272 | name, rev = name_rev |
| 273 | for c in rev: |
| 274 | if c not in self.okchars: |
| 275 | raise ValueError, "bad char in rev" |
| 276 | return name_rev |
| 277 | |
| 278 | def _closepipe(self, f): |
| 279 | """INTERNAL: Close PIPE and print its exit status if nonzero.""" |
| 280 | sts = f.close() |
Guido van Rossum | c0c01f7 | 1995-10-07 20:48:17 +0000 | [diff] [blame] | 281 | if not sts: return None |
| 282 | detail, reason = divmod(sts, 256) |
| 283 | if reason == 0: return 'exit', detail # Exit status |
| 284 | signal = reason&0x7F |
| 285 | if signal == 0x7F: |
| 286 | code = 'stopped' |
| 287 | signal = detail |
| 288 | else: |
| 289 | code = 'killed' |
| 290 | if reason&0x80: |
| 291 | code = code + '(coredump)' |
| 292 | return code, signal |
Guido van Rossum | 802c437 | 1995-06-23 21:58:18 +0000 | [diff] [blame] | 293 | |
| 294 | def _system(self, cmd): |
| 295 | """INTERNAL: run COMMAND in a subshell. |
| 296 | |
| 297 | Standard input for the command is taken fron /dev/null. |
| 298 | |
| 299 | Raise IOError when the exit status is not zero. |
| 300 | |
| 301 | Return whatever the calling method should return; normally |
| 302 | None. |
| 303 | |
| 304 | A derived class may override this method and redefine it to |
| 305 | capture stdout/stderr of the command and return it. |
| 306 | |
| 307 | """ |
| 308 | cmd = cmd + " </dev/null" |
| 309 | sts = os.system(cmd) |
| 310 | if sts: raise IOError, "command exit status %d" % sts |
| 311 | |
| 312 | def _filter(self, files, pat = None): |
| 313 | """INTERNAL: Return a sorted copy of the given list of FILES. |
| 314 | |
| 315 | If a second PATTERN argument is given, only files matching it |
| 316 | are kept. No check for valid filenames is made. |
| 317 | |
| 318 | """ |
| 319 | if pat: |
| 320 | def keep(name, pat = pat): |
| 321 | return fnmatch.fnmatch(name, pat) |
| 322 | files = filter(keep, files) |
| 323 | else: |
| 324 | files = files[:] |
| 325 | files.sort() |
| 326 | return files |
| 327 | |
| 328 | def _remove(self, fn): |
| 329 | """INTERNAL: remove FILE without complaints.""" |
| 330 | try: |
| 331 | os.unlink(fn) |
| 332 | except os.error: |
| 333 | pass |
| 334 | |
| 335 | def _isrcs(self, name): |
| 336 | """INTERNAL: Test whether NAME ends in ',v'.""" |
| 337 | return name[-2:] == ',v' |