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