source: python/vendor/Python-2.7.6/Demo/pdist/rcslib.py

Last change on this file was 2, checked in by Yuri Dario, 15 years ago

Initial import for vendor code.

  • Property svn:eol-style set to native
File size: 10.1 KB
Line 
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'
Note: See TracBrowser for help on using the repository browser.