1"""Utilities for CVS administration.""" 2 3import string 4import os 5import time 6import md5 7import fnmatch 8 9if not hasattr(time, 'timezone'): 10 time.timezone = 0 11 12class 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 174class 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 300hexify_format = '%02x' * 16 301def 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 307def 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 317unctime_monthmap = {} 318def 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 335def gmctime(t): 336 if t is None: return "None" 337 return time.asctime(time.gmtime(t)) 338 339def 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 352def 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 363if __name__ == "__main__": 364 test() 365