1"""RCS interface module. 2 3Defines the class RCS, which represents a directory with rcs version 4files and (possibly) corresponding work files. 5 6""" 7 8 9import fnmatch 10import os 11import re 12import string 13import tempfile 14 15 16class 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