|
1 """Utilities for CVS administration.""" |
|
2 |
|
3 import string |
|
4 import os |
|
5 import time |
|
6 import md5 |
|
7 import fnmatch |
|
8 |
|
9 if not hasattr(time, 'timezone'): |
|
10 time.timezone = 0 |
|
11 |
|
12 class File: |
|
13 |
|
14 """Represent a file's status. |
|
15 |
|
16 Instance variables: |
|
17 |
|
18 file -- the filename (no slashes), None if uninitialized |
|
19 lseen -- true if the data for the local file is up to date |
|
20 eseen -- true if the data from the CVS/Entries entry is up to date |
|
21 (this implies that the entry must be written back) |
|
22 rseen -- true if the data for the remote file is up to date |
|
23 proxy -- RCSProxy instance used to contact the server, or None |
|
24 |
|
25 Note that lseen and rseen don't necessary mean that a local |
|
26 or remote file *exists* -- they indicate that we've checked it. |
|
27 However, eseen means that this instance corresponds to an |
|
28 entry in the CVS/Entries file. |
|
29 |
|
30 If lseen is true: |
|
31 |
|
32 lsum -- checksum of the local file, None if no local file |
|
33 lctime -- ctime of the local file, None if no local file |
|
34 lmtime -- mtime of the local file, None if no local file |
|
35 |
|
36 If eseen is true: |
|
37 |
|
38 erev -- revision, None if this is a no revision (not '0') |
|
39 enew -- true if this is an uncommitted added file |
|
40 edeleted -- true if this is an uncommitted removed file |
|
41 ectime -- ctime of last local file corresponding to erev |
|
42 emtime -- mtime of last local file corresponding to erev |
|
43 extra -- 5th string from CVS/Entries file |
|
44 |
|
45 If rseen is true: |
|
46 |
|
47 rrev -- revision of head, None if non-existent |
|
48 rsum -- checksum of that revision, Non if non-existent |
|
49 |
|
50 If eseen and rseen are both true: |
|
51 |
|
52 esum -- checksum of revision erev, None if no revision |
|
53 |
|
54 Note |
|
55 """ |
|
56 |
|
57 def __init__(self, file = None): |
|
58 if file and '/' in file: |
|
59 raise ValueError, "no slash allowed in file" |
|
60 self.file = file |
|
61 self.lseen = self.eseen = self.rseen = 0 |
|
62 self.proxy = None |
|
63 |
|
64 def __cmp__(self, other): |
|
65 return cmp(self.file, other.file) |
|
66 |
|
67 def getlocal(self): |
|
68 try: |
|
69 self.lmtime, self.lctime = os.stat(self.file)[-2:] |
|
70 except os.error: |
|
71 self.lmtime = self.lctime = self.lsum = None |
|
72 else: |
|
73 self.lsum = md5.new(open(self.file).read()).digest() |
|
74 self.lseen = 1 |
|
75 |
|
76 def getentry(self, line): |
|
77 words = string.splitfields(line, '/') |
|
78 if self.file and words[1] != self.file: |
|
79 raise ValueError, "file name mismatch" |
|
80 self.file = words[1] |
|
81 self.erev = words[2] |
|
82 self.edeleted = 0 |
|
83 self.enew = 0 |
|
84 self.ectime = self.emtime = None |
|
85 if self.erev[:1] == '-': |
|
86 self.edeleted = 1 |
|
87 self.erev = self.erev[1:] |
|
88 if self.erev == '0': |
|
89 self.erev = None |
|
90 self.enew = 1 |
|
91 else: |
|
92 dates = words[3] |
|
93 self.ectime = unctime(dates[:24]) |
|
94 self.emtime = unctime(dates[25:]) |
|
95 self.extra = words[4] |
|
96 if self.rseen: |
|
97 self.getesum() |
|
98 self.eseen = 1 |
|
99 |
|
100 def getremote(self, proxy = None): |
|
101 if proxy: |
|
102 self.proxy = proxy |
|
103 try: |
|
104 self.rrev = self.proxy.head(self.file) |
|
105 except (os.error, IOError): |
|
106 self.rrev = None |
|
107 if self.rrev: |
|
108 self.rsum = self.proxy.sum(self.file) |
|
109 else: |
|
110 self.rsum = None |
|
111 if self.eseen: |
|
112 self.getesum() |
|
113 self.rseen = 1 |
|
114 |
|
115 def getesum(self): |
|
116 if self.erev == self.rrev: |
|
117 self.esum = self.rsum |
|
118 elif self.erev: |
|
119 name = (self.file, self.erev) |
|
120 self.esum = self.proxy.sum(name) |
|
121 else: |
|
122 self.esum = None |
|
123 |
|
124 def putentry(self): |
|
125 """Return a line suitable for inclusion in CVS/Entries. |
|
126 |
|
127 The returned line is terminated by a newline. |
|
128 If no entry should be written for this file, |
|
129 return "". |
|
130 """ |
|
131 if not self.eseen: |
|
132 return "" |
|
133 |
|
134 rev = self.erev or '0' |
|
135 if self.edeleted: |
|
136 rev = '-' + rev |
|
137 if self.enew: |
|
138 dates = 'Initial ' + self.file |
|
139 else: |
|
140 dates = gmctime(self.ectime) + ' ' + \ |
|
141 gmctime(self.emtime) |
|
142 return "/%s/%s/%s/%s/\n" % ( |
|
143 self.file, |
|
144 rev, |
|
145 dates, |
|
146 self.extra) |
|
147 |
|
148 def report(self): |
|
149 print '-'*50 |
|
150 def r(key, repr=repr, self=self): |
|
151 try: |
|
152 value = repr(getattr(self, key)) |
|
153 except AttributeError: |
|
154 value = "?" |
|
155 print "%-15s:" % key, value |
|
156 r("file") |
|
157 if self.lseen: |
|
158 r("lsum", hexify) |
|
159 r("lctime", gmctime) |
|
160 r("lmtime", gmctime) |
|
161 if self.eseen: |
|
162 r("erev") |
|
163 r("enew") |
|
164 r("edeleted") |
|
165 r("ectime", gmctime) |
|
166 r("emtime", gmctime) |
|
167 if self.rseen: |
|
168 r("rrev") |
|
169 r("rsum", hexify) |
|
170 if self.eseen: |
|
171 r("esum", hexify) |
|
172 |
|
173 |
|
174 class CVS: |
|
175 |
|
176 """Represent the contents of a CVS admin file (and more). |
|
177 |
|
178 Class variables: |
|
179 |
|
180 FileClass -- the class to be instantiated for entries |
|
181 (this should be derived from class File above) |
|
182 IgnoreList -- shell patterns for local files to be ignored |
|
183 |
|
184 Instance variables: |
|
185 |
|
186 entries -- a dictionary containing File instances keyed by |
|
187 their file name |
|
188 proxy -- an RCSProxy instance, or None |
|
189 """ |
|
190 |
|
191 FileClass = File |
|
192 |
|
193 IgnoreList = ['.*', '@*', ',*', '*~', '*.o', '*.a', '*.so', '*.pyc'] |
|
194 |
|
195 def __init__(self): |
|
196 self.entries = {} |
|
197 self.proxy = None |
|
198 |
|
199 def setproxy(self, proxy): |
|
200 if proxy is self.proxy: |
|
201 return |
|
202 self.proxy = proxy |
|
203 for e in self.entries.values(): |
|
204 e.rseen = 0 |
|
205 |
|
206 def getentries(self): |
|
207 """Read the contents of CVS/Entries""" |
|
208 self.entries = {} |
|
209 f = self.cvsopen("Entries") |
|
210 while 1: |
|
211 line = f.readline() |
|
212 if not line: break |
|
213 e = self.FileClass() |
|
214 e.getentry(line) |
|
215 self.entries[e.file] = e |
|
216 f.close() |
|
217 |
|
218 def putentries(self): |
|
219 """Write CVS/Entries back""" |
|
220 f = self.cvsopen("Entries", 'w') |
|
221 for e in self.values(): |
|
222 f.write(e.putentry()) |
|
223 f.close() |
|
224 |
|
225 def getlocalfiles(self): |
|
226 list = self.entries.keys() |
|
227 addlist = os.listdir(os.curdir) |
|
228 for name in addlist: |
|
229 if name in list: |
|
230 continue |
|
231 if not self.ignored(name): |
|
232 list.append(name) |
|
233 list.sort() |
|
234 for file in list: |
|
235 try: |
|
236 e = self.entries[file] |
|
237 except KeyError: |
|
238 e = self.entries[file] = self.FileClass(file) |
|
239 e.getlocal() |
|
240 |
|
241 def getremotefiles(self, proxy = None): |
|
242 if proxy: |
|
243 self.proxy = proxy |
|
244 if not self.proxy: |
|
245 raise RuntimeError, "no RCS proxy" |
|
246 addlist = self.proxy.listfiles() |
|
247 for file in addlist: |
|
248 try: |
|
249 e = self.entries[file] |
|
250 except KeyError: |
|
251 e = self.entries[file] = self.FileClass(file) |
|
252 e.getremote(self.proxy) |
|
253 |
|
254 def report(self): |
|
255 for e in self.values(): |
|
256 e.report() |
|
257 print '-'*50 |
|
258 |
|
259 def keys(self): |
|
260 keys = self.entries.keys() |
|
261 keys.sort() |
|
262 return keys |
|
263 |
|
264 def values(self): |
|
265 def value(key, self=self): |
|
266 return self.entries[key] |
|
267 return map(value, self.keys()) |
|
268 |
|
269 def items(self): |
|
270 def item(key, self=self): |
|
271 return (key, self.entries[key]) |
|
272 return map(item, self.keys()) |
|
273 |
|
274 def cvsexists(self, file): |
|
275 file = os.path.join("CVS", file) |
|
276 return os.path.exists(file) |
|
277 |
|
278 def cvsopen(self, file, mode = 'r'): |
|
279 file = os.path.join("CVS", file) |
|
280 if 'r' not in mode: |
|
281 self.backup(file) |
|
282 return open(file, mode) |
|
283 |
|
284 def backup(self, file): |
|
285 if os.path.isfile(file): |
|
286 bfile = file + '~' |
|
287 try: os.unlink(bfile) |
|
288 except os.error: pass |
|
289 os.rename(file, bfile) |
|
290 |
|
291 def ignored(self, file): |
|
292 if os.path.isdir(file): return True |
|
293 for pat in self.IgnoreList: |
|
294 if fnmatch.fnmatch(file, pat): return True |
|
295 return False |
|
296 |
|
297 |
|
298 # hexify and unhexify are useful to print MD5 checksums in hex format |
|
299 |
|
300 hexify_format = '%02x' * 16 |
|
301 def hexify(sum): |
|
302 "Return a hex representation of a 16-byte string (e.g. an MD5 digest)" |
|
303 if sum is None: |
|
304 return "None" |
|
305 return hexify_format % tuple(map(ord, sum)) |
|
306 |
|
307 def unhexify(hexsum): |
|
308 "Return the original from a hexified string" |
|
309 if hexsum == "None": |
|
310 return None |
|
311 sum = '' |
|
312 for i in range(0, len(hexsum), 2): |
|
313 sum = sum + chr(string.atoi(hexsum[i:i+2], 16)) |
|
314 return sum |
|
315 |
|
316 |
|
317 unctime_monthmap = {} |
|
318 def unctime(date): |
|
319 if date == "None": return None |
|
320 if not unctime_monthmap: |
|
321 months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', |
|
322 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] |
|
323 i = 0 |
|
324 for m in months: |
|
325 i = i+1 |
|
326 unctime_monthmap[m] = i |
|
327 words = string.split(date) # Day Mon DD HH:MM:SS YEAR |
|
328 year = string.atoi(words[4]) |
|
329 month = unctime_monthmap[words[1]] |
|
330 day = string.atoi(words[2]) |
|
331 [hh, mm, ss] = map(string.atoi, string.splitfields(words[3], ':')) |
|
332 ss = ss - time.timezone |
|
333 return time.mktime((year, month, day, hh, mm, ss, 0, 0, 0)) |
|
334 |
|
335 def gmctime(t): |
|
336 if t is None: return "None" |
|
337 return time.asctime(time.gmtime(t)) |
|
338 |
|
339 def test_unctime(): |
|
340 now = int(time.time()) |
|
341 t = time.gmtime(now) |
|
342 at = time.asctime(t) |
|
343 print 'GMT', now, at |
|
344 print 'timezone', time.timezone |
|
345 print 'local', time.ctime(now) |
|
346 u = unctime(at) |
|
347 print 'unctime()', u |
|
348 gu = time.gmtime(u) |
|
349 print '->', gu |
|
350 print time.asctime(gu) |
|
351 |
|
352 def test(): |
|
353 x = CVS() |
|
354 x.getentries() |
|
355 x.getlocalfiles() |
|
356 ## x.report() |
|
357 import rcsclient |
|
358 proxy = rcsclient.openrcsclient() |
|
359 x.getremotefiles(proxy) |
|
360 x.report() |
|
361 |
|
362 |
|
363 if __name__ == "__main__": |
|
364 test() |