]>
Commit | Line | Data |
---|---|---|
4710c53d | 1 | """RCS interface module.\r |
2 | \r | |
3 | Defines the class RCS, which represents a directory with rcs version\r | |
4 | files and (possibly) corresponding work files.\r | |
5 | \r | |
6 | """\r | |
7 | \r | |
8 | \r | |
9 | import fnmatch\r | |
10 | import os\r | |
11 | import re\r | |
12 | import string\r | |
13 | import tempfile\r | |
14 | \r | |
15 | \r | |
16 | class RCS:\r | |
17 | \r | |
18 | """RCS interface class (local filesystem version).\r | |
19 | \r | |
20 | An instance of this class represents a directory with rcs version\r | |
21 | files and (possible) corresponding work files.\r | |
22 | \r | |
23 | Methods provide access to most rcs operations such as\r | |
24 | checkin/checkout, access to the rcs metadata (revisions, logs,\r | |
25 | branches etc.) as well as some filesystem operations such as\r | |
26 | listing all rcs version files.\r | |
27 | \r | |
28 | XXX BUGS / PROBLEMS\r | |
29 | \r | |
30 | - The instance always represents the current directory so it's not\r | |
31 | very useful to have more than one instance around simultaneously\r | |
32 | \r | |
33 | """\r | |
34 | \r | |
35 | # Characters allowed in work file names\r | |
36 | okchars = string.ascii_letters + string.digits + '-_=+'\r | |
37 | \r | |
38 | def __init__(self):\r | |
39 | """Constructor."""\r | |
40 | pass\r | |
41 | \r | |
42 | def __del__(self):\r | |
43 | """Destructor."""\r | |
44 | pass\r | |
45 | \r | |
46 | # --- Informational methods about a single file/revision ---\r | |
47 | \r | |
48 | def log(self, name_rev, otherflags = ''):\r | |
49 | """Return the full log text for NAME_REV as a string.\r | |
50 | \r | |
51 | Optional OTHERFLAGS are passed to rlog.\r | |
52 | \r | |
53 | """\r | |
54 | f = self._open(name_rev, 'rlog ' + otherflags)\r | |
55 | data = f.read()\r | |
56 | status = self._closepipe(f)\r | |
57 | if status:\r | |
58 | data = data + "%s: %s" % status\r | |
59 | elif data[-1] == '\n':\r | |
60 | data = data[:-1]\r | |
61 | return data\r | |
62 | \r | |
63 | def head(self, name_rev):\r | |
64 | """Return the head revision for NAME_REV"""\r | |
65 | dict = self.info(name_rev)\r | |
66 | return dict['head']\r | |
67 | \r | |
68 | def info(self, name_rev):\r | |
69 | """Return a dictionary of info (from rlog -h) for NAME_REV\r | |
70 | \r | |
71 | The dictionary's keys are the keywords that rlog prints\r | |
72 | (e.g. 'head' and its values are the corresponding data\r | |
73 | (e.g. '1.3').\r | |
74 | \r | |
75 | XXX symbolic names and locks are not returned\r | |
76 | \r | |
77 | """\r | |
78 | f = self._open(name_rev, 'rlog -h')\r | |
79 | dict = {}\r | |
80 | while 1:\r | |
81 | line = f.readline()\r | |
82 | if not line: break\r | |
83 | if line[0] == '\t':\r | |
84 | # XXX could be a lock or symbolic name\r | |
85 | # Anything else?\r | |
86 | continue\r | |
87 | i = string.find(line, ':')\r | |
88 | if i > 0:\r | |
89 | key, value = line[:i], string.strip(line[i+1:])\r | |
90 | dict[key] = value\r | |
91 | status = self._closepipe(f)\r | |
92 | if status:\r | |
93 | raise IOError, status\r | |
94 | return dict\r | |
95 | \r | |
96 | # --- Methods that change files ---\r | |
97 | \r | |
98 | def lock(self, name_rev):\r | |
99 | """Set an rcs lock on NAME_REV."""\r | |
100 | name, rev = self.checkfile(name_rev)\r | |
101 | cmd = "rcs -l%s %s" % (rev, name)\r | |
102 | return self._system(cmd)\r | |
103 | \r | |
104 | def unlock(self, name_rev):\r | |
105 | """Clear an rcs lock on NAME_REV."""\r | |
106 | name, rev = self.checkfile(name_rev)\r | |
107 | cmd = "rcs -u%s %s" % (rev, name)\r | |
108 | return self._system(cmd)\r | |
109 | \r | |
110 | def checkout(self, name_rev, withlock=0, otherflags=""):\r | |
111 | """Check out NAME_REV to its work file.\r | |
112 | \r | |
113 | If optional WITHLOCK is set, check out locked, else unlocked.\r | |
114 | \r | |
115 | The optional OTHERFLAGS is passed to co without\r | |
116 | interpretation.\r | |
117 | \r | |
118 | Any output from co goes to directly to stdout.\r | |
119 | \r | |
120 | """\r | |
121 | name, rev = self.checkfile(name_rev)\r | |
122 | if withlock: lockflag = "-l"\r | |
123 | else: lockflag = "-u"\r | |
124 | cmd = 'co %s%s %s %s' % (lockflag, rev, otherflags, name)\r | |
125 | return self._system(cmd)\r | |
126 | \r | |
127 | def checkin(self, name_rev, message=None, otherflags=""):\r | |
128 | """Check in NAME_REV from its work file.\r | |
129 | \r | |
130 | The optional MESSAGE argument becomes the checkin message\r | |
131 | (default "<none>" if None); or the file description if this is\r | |
132 | a new file.\r | |
133 | \r | |
134 | The optional OTHERFLAGS argument is passed to ci without\r | |
135 | interpretation.\r | |
136 | \r | |
137 | Any output from ci goes to directly to stdout.\r | |
138 | \r | |
139 | """\r | |
140 | name, rev = self._unmangle(name_rev)\r | |
141 | new = not self.isvalid(name)\r | |
142 | if not message: message = "<none>"\r | |
143 | if message and message[-1] != '\n':\r | |
144 | message = message + '\n'\r | |
145 | lockflag = "-u"\r | |
146 | if new:\r | |
147 | f = tempfile.NamedTemporaryFile()\r | |
148 | f.write(message)\r | |
149 | f.flush()\r | |
150 | cmd = 'ci %s%s -t%s %s %s' % \\r | |
151 | (lockflag, rev, f.name, otherflags, name)\r | |
152 | else:\r | |
153 | message = re.sub(r'([\"$`])', r'\\\1', message)\r | |
154 | cmd = 'ci %s%s -m"%s" %s %s' % \\r | |
155 | (lockflag, rev, message, otherflags, name)\r | |
156 | return self._system(cmd)\r | |
157 | \r | |
158 | # --- Exported support methods ---\r | |
159 | \r | |
160 | def listfiles(self, pat = None):\r | |
161 | """Return a list of all version files matching optional PATTERN."""\r | |
162 | files = os.listdir(os.curdir)\r | |
163 | files = filter(self._isrcs, files)\r | |
164 | if os.path.isdir('RCS'):\r | |
165 | files2 = os.listdir('RCS')\r | |
166 | files2 = filter(self._isrcs, files2)\r | |
167 | files = files + files2\r | |
168 | files = map(self.realname, files)\r | |
169 | return self._filter(files, pat)\r | |
170 | \r | |
171 | def isvalid(self, name):\r | |
172 | """Test whether NAME has a version file associated."""\r | |
173 | namev = self.rcsname(name)\r | |
174 | return (os.path.isfile(namev) or\r | |
175 | os.path.isfile(os.path.join('RCS', namev)))\r | |
176 | \r | |
177 | def rcsname(self, name):\r | |
178 | """Return the pathname of the version file for NAME.\r | |
179 | \r | |
180 | The argument can be a work file name or a version file name.\r | |
181 | If the version file does not exist, the name of the version\r | |
182 | file that would be created by "ci" is returned.\r | |
183 | \r | |
184 | """\r | |
185 | if self._isrcs(name): namev = name\r | |
186 | else: namev = name + ',v'\r | |
187 | if os.path.isfile(namev): return namev\r | |
188 | namev = os.path.join('RCS', os.path.basename(namev))\r | |
189 | if os.path.isfile(namev): return namev\r | |
190 | if os.path.isdir('RCS'):\r | |
191 | return os.path.join('RCS', namev)\r | |
192 | else:\r | |
193 | return namev\r | |
194 | \r | |
195 | def realname(self, namev):\r | |
196 | """Return the pathname of the work file for NAME.\r | |
197 | \r | |
198 | The argument can be a work file name or a version file name.\r | |
199 | If the work file does not exist, the name of the work file\r | |
200 | that would be created by "co" is returned.\r | |
201 | \r | |
202 | """\r | |
203 | if self._isrcs(namev): name = namev[:-2]\r | |
204 | else: name = namev\r | |
205 | if os.path.isfile(name): return name\r | |
206 | name = os.path.basename(name)\r | |
207 | return name\r | |
208 | \r | |
209 | def islocked(self, name_rev):\r | |
210 | """Test whether FILE (which must have a version file) is locked.\r | |
211 | \r | |
212 | XXX This does not tell you which revision number is locked and\r | |
213 | ignores any revision you may pass in (by virtue of using rlog\r | |
214 | -L -R).\r | |
215 | \r | |
216 | """\r | |
217 | f = self._open(name_rev, 'rlog -L -R')\r | |
218 | line = f.readline()\r | |
219 | status = self._closepipe(f)\r | |
220 | if status:\r | |
221 | raise IOError, status\r | |
222 | if not line: return None\r | |
223 | if line[-1] == '\n':\r | |
224 | line = line[:-1]\r | |
225 | return self.realname(name_rev) == self.realname(line)\r | |
226 | \r | |
227 | def checkfile(self, name_rev):\r | |
228 | """Normalize NAME_REV into a (NAME, REV) tuple.\r | |
229 | \r | |
230 | Raise an exception if there is no corresponding version file.\r | |
231 | \r | |
232 | """\r | |
233 | name, rev = self._unmangle(name_rev)\r | |
234 | if not self.isvalid(name):\r | |
235 | raise os.error, 'not an rcs file %r' % (name,)\r | |
236 | return name, rev\r | |
237 | \r | |
238 | # --- Internal methods ---\r | |
239 | \r | |
240 | def _open(self, name_rev, cmd = 'co -p', rflag = '-r'):\r | |
241 | """INTERNAL: open a read pipe to NAME_REV using optional COMMAND.\r | |
242 | \r | |
243 | Optional FLAG is used to indicate the revision (default -r).\r | |
244 | \r | |
245 | Default COMMAND is "co -p".\r | |
246 | \r | |
247 | Return a file object connected by a pipe to the command's\r | |
248 | output.\r | |
249 | \r | |
250 | """\r | |
251 | name, rev = self.checkfile(name_rev)\r | |
252 | namev = self.rcsname(name)\r | |
253 | if rev:\r | |
254 | cmd = cmd + ' ' + rflag + rev\r | |
255 | return os.popen("%s %r" % (cmd, namev))\r | |
256 | \r | |
257 | def _unmangle(self, name_rev):\r | |
258 | """INTERNAL: Normalize NAME_REV argument to (NAME, REV) tuple.\r | |
259 | \r | |
260 | Raise an exception if NAME contains invalid characters.\r | |
261 | \r | |
262 | A NAME_REV argument is either NAME string (implying REV='') or\r | |
263 | a tuple of the form (NAME, REV).\r | |
264 | \r | |
265 | """\r | |
266 | if type(name_rev) == type(''):\r | |
267 | name_rev = name, rev = name_rev, ''\r | |
268 | else:\r | |
269 | name, rev = name_rev\r | |
270 | for c in rev:\r | |
271 | if c not in self.okchars:\r | |
272 | raise ValueError, "bad char in rev"\r | |
273 | return name_rev\r | |
274 | \r | |
275 | def _closepipe(self, f):\r | |
276 | """INTERNAL: Close PIPE and print its exit status if nonzero."""\r | |
277 | sts = f.close()\r | |
278 | if not sts: return None\r | |
279 | detail, reason = divmod(sts, 256)\r | |
280 | if reason == 0: return 'exit', detail # Exit status\r | |
281 | signal = reason&0x7F\r | |
282 | if signal == 0x7F:\r | |
283 | code = 'stopped'\r | |
284 | signal = detail\r | |
285 | else:\r | |
286 | code = 'killed'\r | |
287 | if reason&0x80:\r | |
288 | code = code + '(coredump)'\r | |
289 | return code, signal\r | |
290 | \r | |
291 | def _system(self, cmd):\r | |
292 | """INTERNAL: run COMMAND in a subshell.\r | |
293 | \r | |
294 | Standard input for the command is taken from /dev/null.\r | |
295 | \r | |
296 | Raise IOError when the exit status is not zero.\r | |
297 | \r | |
298 | Return whatever the calling method should return; normally\r | |
299 | None.\r | |
300 | \r | |
301 | A derived class may override this method and redefine it to\r | |
302 | capture stdout/stderr of the command and return it.\r | |
303 | \r | |
304 | """\r | |
305 | cmd = cmd + " </dev/null"\r | |
306 | sts = os.system(cmd)\r | |
307 | if sts: raise IOError, "command exit status %d" % sts\r | |
308 | \r | |
309 | def _filter(self, files, pat = None):\r | |
310 | """INTERNAL: Return a sorted copy of the given list of FILES.\r | |
311 | \r | |
312 | If a second PATTERN argument is given, only files matching it\r | |
313 | are kept. No check for valid filenames is made.\r | |
314 | \r | |
315 | """\r | |
316 | if pat:\r | |
317 | def keep(name, pat = pat):\r | |
318 | return fnmatch.fnmatch(name, pat)\r | |
319 | files = filter(keep, files)\r | |
320 | else:\r | |
321 | files = files[:]\r | |
322 | files.sort()\r | |
323 | return files\r | |
324 | \r | |
325 | def _remove(self, fn):\r | |
326 | """INTERNAL: remove FILE without complaints."""\r | |
327 | try:\r | |
328 | os.unlink(fn)\r | |
329 | except os.error:\r | |
330 | pass\r | |
331 | \r | |
332 | def _isrcs(self, name):\r | |
333 | """INTERNAL: Test whether NAME ends in ',v'."""\r | |
334 | return name[-2:] == ',v'\r |