blob: 4b34fd76489d401a07cc8e92ecc3ebf954a53333 [file] [log] [blame]
Guido van Rossum802c4371995-06-23 21:58:18 +00001"""RCS interface module.
Guido van Rossum79ed32d1995-06-23 14:40:06 +00002
Guido van Rossum802c4371995-06-23 21:58:18 +00003Defines the class RCS, which represents a directory with rcs version
4files and (possibly) corresponding work files.
5
Guido van Rossum79ed32d1995-06-23 14:40:06 +00006"""
7
Guido van Rossum79ed32d1995-06-23 14:40:06 +00008
Guido van Rossum802c4371995-06-23 21:58:18 +00009import fnmatch
Guido van Rossum79ed32d1995-06-23 14:40:06 +000010import os
Guido van Rossum802c4371995-06-23 21:58:18 +000011import regsub
12import string
Guido van Rossum79ed32d1995-06-23 14:40:06 +000013import tempfile
14
Guido van Rossum79ed32d1995-06-23 14:40:06 +000015
Guido van Rossum802c4371995-06-23 21:58:18 +000016class RCS:
Guido van Rossum79ed32d1995-06-23 14:40:06 +000017
Guido van Rossum802c4371995-06-23 21:58:18 +000018 """RCS interface class (local filesystem version).
Guido van Rossum79ed32d1995-06-23 14:40:06 +000019
Guido van Rossum802c4371995-06-23 21:58:18 +000020 An instance of this class represents a directory with rcs version
21 files and (possible) corresponding work files.
Guido van Rossum79ed32d1995-06-23 14:40:06 +000022
Guido van Rossum802c4371995-06-23 21:58:18 +000023 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 Rossum79ed32d1995-06-23 14:40:06 +000027
Guido van Rossum802c4371995-06-23 21:58:18 +000028 XXX BUGS / PROBLEMS
Guido van Rossum79ed32d1995-06-23 14:40:06 +000029
Guido van Rossum802c4371995-06-23 21:58:18 +000030 - The instance always represents the current directory so it's not
31 very useful to have more than one instance around simultaneously
Guido van Rossum79ed32d1995-06-23 14:40:06 +000032
Guido van Rossum802c4371995-06-23 21:58:18 +000033 """
Guido van Rossum79ed32d1995-06-23 14:40:06 +000034
Guido van Rossum802c4371995-06-23 21:58:18 +000035 # Characters allowed in work file names
36 okchars = string.letters + string.digits + '-_=+.'
Guido van Rossum79ed32d1995-06-23 14:40:06 +000037
Guido van Rossum802c4371995-06-23 21:58:18 +000038 def __init__(self):
39 """Constructor."""
40 pass
Guido van Rossum79ed32d1995-06-23 14:40:06 +000041
Guido van Rossum802c4371995-06-23 21:58:18 +000042 def __del__(self):
43 """Destructor."""
44 pass
Guido van Rossum79ed32d1995-06-23 14:40:06 +000045
Guido van Rossum802c4371995-06-23 21:58:18 +000046 # --- Informational methods about a single file/revision ---
Guido van Rossum79ed32d1995-06-23 14:40:06 +000047
Guido van Rossum802c4371995-06-23 21:58:18 +000048 def log(self, name_rev, otherflags = ''):
Guido van Rossumbffda891995-10-07 19:46:08 +000049 """Return the full log text for NAME_REV as a string.
Guido van Rossum79ed32d1995-06-23 14:40:06 +000050
Guido van Rossum802c4371995-06-23 21:58:18 +000051 Optional OTHERFLAGS are passed to rlog.
Guido van Rossum79ed32d1995-06-23 14:40:06 +000052
Guido van Rossum802c4371995-06-23 21:58:18 +000053 """
Guido van Rossumbffda891995-10-07 19:46:08 +000054 f = self._open(name_rev, 'rlog ' + otherflags)
55 data = f.read()
56 self._closepipe(f)
57 return data
Guido van Rossum79ed32d1995-06-23 14:40:06 +000058
Guido van Rossum802c4371995-06-23 21:58:18 +000059 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 Rossum79ed32d1995-06-23 14:40:06 +000063
Guido van Rossum802c4371995-06-23 21:58:18 +000064 def info(self, name_rev):
65 """Return a dictionary of info (from rlog -h) for NAME_REV
Guido van Rossum79ed32d1995-06-23 14:40:06 +000066
Guido van Rossum802c4371995-06-23 21:58:18 +000067 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 Rossum79ed32d1995-06-23 14:40:06 +000070
Guido van Rossum802c4371995-06-23 21:58:18 +000071 XXX symbolic names and locks are not returned
Guido van Rossum79ed32d1995-06-23 14:40:06 +000072
Guido van Rossum802c4371995-06-23 21:58:18 +000073 """
74 f = self._open(name_rev, 'rlog -h')
Guido van Rossum79ed32d1995-06-23 14:40:06 +000075 dict = {}
76 while 1:
77 line = f.readline()
78 if not line: break
79 if line[0] == '\t':
Guido van Rossum802c4371995-06-23 21:58:18 +000080 # XXX could be a lock or symbolic name
Guido van Rossum79ed32d1995-06-23 14:40:06 +000081 # 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 Rossum802c4371995-06-23 21:58:18 +000090 # --- Methods that change files ---
Guido van Rossum79ed32d1995-06-23 14:40:06 +000091
Guido van Rossum802c4371995-06-23 21:58:18 +000092 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 Rossum79ed32d1995-06-23 14:40:06 +000097
Guido van Rossum802c4371995-06-23 21:58:18 +000098 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 Rossum79ed32d1995-06-23 14:40:06 +0000116 if withlock: lockflag = "-l"
117 else: lockflag = "-u"
Guido van Rossum802c4371995-06-23 21:58:18 +0000118 cmd = 'co %s%s %s %s' % (lockflag, rev, otherflags, name)
119 return self._system(cmd)
Guido van Rossum79ed32d1995-06-23 14:40:06 +0000120
Guido van Rossum802c4371995-06-23 21:58:18 +0000121 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 Rossum79ed32d1995-06-23 14:40:06 +0000136 if not message: message = "<none>"
137 if message and message[-1] != '\n':
138 message = message + '\n'
Guido van Rossum802c4371995-06-23 21:58:18 +0000139 lockflag = "-u"
Guido van Rossum79ed32d1995-06-23 14:40:06 +0000140 textfile = None
141 try:
142 if new:
Guido van Rossum79ed32d1995-06-23 14:40:06 +0000143 textfile = tempfile.mktemp()
144 f = open(textfile, 'w')
145 f.write(message)
146 f.close()
Guido van Rossum802c4371995-06-23 21:58:18 +0000147 cmd = 'ci %s%s -t%s %s %s' % \
148 (lockflag, rev, textfile, otherflags, name)
Guido van Rossum79ed32d1995-06-23 14:40:06 +0000149 else:
Guido van Rossum802c4371995-06-23 21:58:18 +0000150 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 Rossum79ed32d1995-06-23 14:40:06 +0000154 finally:
155 if textfile: self._remove(textfile)
156
Guido van Rossum802c4371995-06-23 21:58:18 +0000157 # --- Exported support methods ---
Guido van Rossum79ed32d1995-06-23 14:40:06 +0000158
Guido van Rossum802c4371995-06-23 21:58:18 +0000159 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'