blob: 7c685430ee41b4cee993ed26535c4e9f7987c3b2 [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 = ''):
49 """Print the full log text for NAME_REV on stdout.
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 """
54 name, rev = self.checkfile(name_rev)
55 cmd = "rlog -r%s %s %s" % (rev, name, otherflags)
56 return self._system(cmd)
Guido van Rossum79ed32d1995-06-23 14:40:06 +000057
Guido van Rossum802c4371995-06-23 21:58:18 +000058 def head(self, name_rev):
59 """Return the head revision for NAME_REV"""
60 dict = self.info(name_rev)
61 return dict['head']
Guido van Rossum79ed32d1995-06-23 14:40:06 +000062
Guido van Rossum802c4371995-06-23 21:58:18 +000063 def info(self, name_rev):
64 """Return a dictionary of info (from rlog -h) for NAME_REV
Guido van Rossum79ed32d1995-06-23 14:40:06 +000065
Guido van Rossum802c4371995-06-23 21:58:18 +000066 The dictionary's keys are the keywords that rlog prints
67 (e.g. 'head' and its values are the corresponding data
68 (e.g. '1.3').
Guido van Rossum79ed32d1995-06-23 14:40:06 +000069
Guido van Rossum802c4371995-06-23 21:58:18 +000070 XXX symbolic names and locks are not returned
Guido van Rossum79ed32d1995-06-23 14:40:06 +000071
Guido van Rossum802c4371995-06-23 21:58:18 +000072 """
73 f = self._open(name_rev, 'rlog -h')
Guido van Rossum79ed32d1995-06-23 14:40:06 +000074 dict = {}
75 while 1:
76 line = f.readline()
77 if not line: break
78 if line[0] == '\t':
Guido van Rossum802c4371995-06-23 21:58:18 +000079 # XXX could be a lock or symbolic name
Guido van Rossum79ed32d1995-06-23 14:40:06 +000080 # Anything else?
81 continue
82 i = string.find(line, ':')
83 if i > 0:
84 key, value = line[:i], string.strip(line[i+1:])
85 dict[key] = value
86 self._closepipe(f)
87 return dict
88
Guido van Rossum802c4371995-06-23 21:58:18 +000089 # --- Methods that change files ---
Guido van Rossum79ed32d1995-06-23 14:40:06 +000090
Guido van Rossum802c4371995-06-23 21:58:18 +000091 def lock(self, name_rev):
92 """Set an rcs lock on NAME_REV."""
93 name, rev = self.checkfile(name_rev)
94 cmd = "rcs -l%s %s" % (rev, name)
95 return self._system(cmd)
Guido van Rossum79ed32d1995-06-23 14:40:06 +000096
Guido van Rossum802c4371995-06-23 21:58:18 +000097 def unlock(self, name_rev):
98 """Clear an rcs lock on NAME_REV."""
99 name, rev = self.checkfile(name_rev)
100 cmd = "rcs -u%s %s" % (rev, name)
101 return self._system(cmd)
102
103 def checkout(self, name_rev, withlock=0, otherflags=""):
104 """Check out NAME_REV to its work file.
105
106 If optional WITHLOCK is set, check out locked, else unlocked.
107
108 The optional OTHERFLAGS is passed to co without
109 interpretation.
110
111 Any output from co goes to directly to stdout.
112
113 """
114 name, rev = self.checkfile(name_rev)
Guido van Rossum79ed32d1995-06-23 14:40:06 +0000115 if withlock: lockflag = "-l"
116 else: lockflag = "-u"
Guido van Rossum802c4371995-06-23 21:58:18 +0000117 cmd = 'co %s%s %s %s' % (lockflag, rev, otherflags, name)
118 return self._system(cmd)
Guido van Rossum79ed32d1995-06-23 14:40:06 +0000119
Guido van Rossum802c4371995-06-23 21:58:18 +0000120 def checkin(self, name_rev, message=None, otherflags=""):
121 """Check in NAME_REV from its work file.
122
123 The optional MESSAGE argument becomes the checkin message
124 (default "<none>" if None); or the file description if this is
125 a new file.
126
127 The optional OTHERFLAGS argument is passed to ci without
128 interpretation.
129
130 Any output from ci goes to directly to stdout.
131
132 """
133 name, rev = self._unmangle(name_rev)
134 new = not self.isvalid(name)
Guido van Rossum79ed32d1995-06-23 14:40:06 +0000135 if not message: message = "<none>"
136 if message and message[-1] != '\n':
137 message = message + '\n'
Guido van Rossum802c4371995-06-23 21:58:18 +0000138 lockflag = "-u"
Guido van Rossum79ed32d1995-06-23 14:40:06 +0000139 textfile = None
140 try:
141 if new:
Guido van Rossum79ed32d1995-06-23 14:40:06 +0000142 textfile = tempfile.mktemp()
143 f = open(textfile, 'w')
144 f.write(message)
145 f.close()
Guido van Rossum802c4371995-06-23 21:58:18 +0000146 cmd = 'ci %s%s -t%s %s %s' % \
147 (lockflag, rev, textfile, otherflags, name)
Guido van Rossum79ed32d1995-06-23 14:40:06 +0000148 else:
Guido van Rossum802c4371995-06-23 21:58:18 +0000149 message = regsub.gsub('\([\\"$`]\)', '\\\\\\1', message)
150 cmd = 'ci %s%s -m"%s" %s %s' % \
151 (lockflag, rev, message, otherflags, name)
152 return self._system(cmd)
Guido van Rossum79ed32d1995-06-23 14:40:06 +0000153 finally:
154 if textfile: self._remove(textfile)
155
Guido van Rossum802c4371995-06-23 21:58:18 +0000156 # --- Exported support methods ---
Guido van Rossum79ed32d1995-06-23 14:40:06 +0000157
Guido van Rossum802c4371995-06-23 21:58:18 +0000158 def listfiles(self, pat = None):
159 """Return a list of all version files matching optional PATTERN."""
160 files = os.listdir(os.curdir)
161 files = filter(self._isrcs, files)
162 if os.path.isdir('RCS'):
163 files2 = os.listdir('RCS')
164 files2 = filter(self._isrcs, files2)
165 files = files + files2
166 files = map(self.realname, files)
167 return self._filter(files, pat)
168
169 def isvalid(self, name):
170 """Test whether NAME has a version file associated."""
171 namev = self.rcsname(name)
172 return (os.path.isfile(namev) or
173 os.path.isfile(os.path.join('RCS', namev)))
174
175 def rcsname(self, name):
176 """Return the pathname of the version file for NAME.
177
178 The argument can be a work file name or a version file name.
179 If the version file does not exist, the name of the version
180 file that would be created by "ci" is returned.
181
182 """
183 if self._isrcs(name): namev = name
184 else: namev = name + ',v'
185 if os.path.isfile(namev): return namev
186 namev = os.path.join('RCS', os.path.basename(namev))
187 if os.path.isfile(namev): return namev
188 if os.path.isdir('RCS'):
189 return os.path.join('RCS', namev)
190 else:
191 return namev
192
193 def realname(self, namev):
194 """Return the pathname of the work file for NAME.
195
196 The argument can be a work file name or a version file name.
197 If the work file does not exist, the name of the work file
198 that would be created by "co" is returned.
199
200 """
201 if self._isrcs(namev): name = namev[:-2]
202 else: name = namev
203 if os.path.isfile(name): return name
204 name = os.path.basename(name)
205 return name
206
207 def islocked(self, name_rev):
208 """Test whether FILE (which must have a version file) is locked.
209
210 XXX This does not tell you which revision number is locked and
211 ignores any revision you may pass in (by virtue of using rlog
212 -L -R).
213
214 """
215 f = self._open(name_rev, 'rlog -L -R')
216 line = f.readline()
217 self._closepipe(f)
218 if not line: return None
219 return self.realname(name_rev) == self.realname(line)
220
221 def checkfile(self, name_rev):
222 """Normalize NAME_REV into a (NAME, REV) tuple.
223
224 Raise an exception if there is no corresponding version file.
225
226 """
227 name, rev = self._unmangle(name_rev)
228 if not self.isvalid(name):
229 raise os.error, 'not an rcs file %s' % `name`
230 return name, rev
231
232 # --- Internal methods ---
233
234 def _open(self, name_rev, cmd = 'co -p', rflag = '-r'):
235 """INTERNAL: open a read pipe to NAME_REV using optional COMMAND.
236
237 Optional FLAG is used to indicate the revision (default -r).
238
239 Default COMMAND is "co -p".
240
241 Return a file object connected by a pipe to the command's
242 output.
243
244 """
245 name, rev = self.checkfile(name_rev)
246 namev = self.rcsname(name)
247 if rev:
248 cmd = cmd + ' ' + rflag + rev
249 return os.popen('%s %s' % (cmd, `namev`))
250
251 def _unmangle(self, name_rev):
252 """INTERNAL: Normalize NAME_REV argument to (NAME, REV) tuple.
253
254 Raise an exception if NAME contains invalid characters.
255
256 A NAME_REV argument is either NAME string (implying REV='') or
257 a tuple of the form (NAME, REV).
258
259 """
260 if type(name_rev) == type(''):
261 name_rev = name, rev = name_rev, ''
262 else:
263 name, rev = name_rev
264 for c in rev:
265 if c not in self.okchars:
266 raise ValueError, "bad char in rev"
267 return name_rev
268
269 def _closepipe(self, f):
270 """INTERNAL: Close PIPE and print its exit status if nonzero."""
271 sts = f.close()
272 if sts:
273 raise IOError, "Exit status %d" % sts
274
275 def _system(self, cmd):
276 """INTERNAL: run COMMAND in a subshell.
277
278 Standard input for the command is taken fron /dev/null.
279
280 Raise IOError when the exit status is not zero.
281
282 Return whatever the calling method should return; normally
283 None.
284
285 A derived class may override this method and redefine it to
286 capture stdout/stderr of the command and return it.
287
288 """
289 cmd = cmd + " </dev/null"
290 sts = os.system(cmd)
291 if sts: raise IOError, "command exit status %d" % sts
292
293 def _filter(self, files, pat = None):
294 """INTERNAL: Return a sorted copy of the given list of FILES.
295
296 If a second PATTERN argument is given, only files matching it
297 are kept. No check for valid filenames is made.
298
299 """
300 if pat:
301 def keep(name, pat = pat):
302 return fnmatch.fnmatch(name, pat)
303 files = filter(keep, files)
304 else:
305 files = files[:]
306 files.sort()
307 return files
308
309 def _remove(self, fn):
310 """INTERNAL: remove FILE without complaints."""
311 try:
312 os.unlink(fn)
313 except os.error:
314 pass
315
316 def _isrcs(self, name):
317 """INTERNAL: Test whether NAME ends in ',v'."""
318 return name[-2:] == ',v'