cvslib.py 8.27 KB
Newer Older
1
"""Utilities for CVS administration."""
Guido van Rossum's avatar
Guido van Rossum committed
2 3 4 5

import string
import os
import time
6 7
import md5
import fnmatch
Guido van Rossum's avatar
Guido van Rossum committed
8

9 10
if not hasattr(time, 'timezone'):
	time.timezone = 0
Guido van Rossum's avatar
Guido van Rossum committed
11

12
class File:
Guido van Rossum's avatar
Guido van Rossum committed
13

14 15 16 17 18 19 20 21 22
	"""Represent a file's status.

	Instance variables:

	file -- the filename (no slashes), None if uninitialized
	lseen -- true if the data for the local file is up to date
	eseen -- true if the data from the CVS/Entries entry is up to date
	         (this implies that the entry must be written back)
	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 26 27 28 29 30

	Note that lseen and rseen don't necessary mean that a local
	or remote file *exists* -- they indicate that we've checked it.
	However, eseen means that this instance corresponds to an
	entry in the CVS/Entries file.

	If lseen is true:
Guido van Rossum's avatar
Guido van Rossum committed
31
	
32 33 34 35 36 37 38 39 40 41 42
	lsum -- checksum of the local file, None if no local file
	lctime -- ctime of the local file, None if no local file
	lmtime -- mtime of the local file, None if no local file

	If eseen is true:

	erev -- revision, None if this is a no revision (not '0')
	enew -- true if this is an uncommitted added file
	edeleted -- true if this is an uncommitted removed file
	ectime -- ctime of last local file corresponding to erev
	emtime -- mtime of last local file corresponding to erev
43
	extra -- 5th string from CVS/Entries file
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61

	If rseen is true:

	rrev -- revision of head, None if non-existent
	rsum -- checksum of that revision, Non if non-existent

	If eseen and rseen are both true:
	
	esum -- checksum of revision erev, None if no revision

	Note
	"""

	def __init__(self, file = None):
		if file and '/' in file:
			raise ValueError, "no slash allowed in file"
		self.file = file
		self.lseen = self.eseen = self.rseen = 0
62 63 64 65
		self.proxy = None

	def __cmp__(self, other):
		return cmp(self.file, other.file)
66 67 68 69 70

	def getlocal(self):
		try:
			self.lmtime, self.lctime = os.stat(self.file)[-2:]
		except os.error:
71
			self.lmtime = self.lctime = self.lsum = None
72
		else:
73
			self.lsum = md5.md5(open(self.file).read()).digest()
74 75 76
		self.lseen = 1

	def getentry(self, line):
Guido van Rossum's avatar
Guido van Rossum committed
77
		words = string.splitfields(line, '/')
78 79
		if self.file and words[1] != self.file:
			raise ValueError, "file name mismatch"
Guido van Rossum's avatar
Guido van Rossum committed
80
		self.file = words[1]
81 82 83 84 85 86 87 88 89 90
		self.erev = words[2]
		self.edeleted = 0
		self.enew = 0
		self.ectime = self.emtime = None
		if self.erev[:1] == '-':
			self.edeleted = 1
			self.erev = self.erev[1:]
		if self.erev == '0':
			self.erev = None
			self.enew = 1
Guido van Rossum's avatar
Guido van Rossum committed
91
		else:
92 93 94
			dates = words[3]
			self.ectime = unctime(dates[:24])
			self.emtime = unctime(dates[25:])
Guido van Rossum's avatar
Guido van Rossum committed
95
		self.extra = words[4]
96 97 98 99
		if self.rseen:
			self.getesum()
		self.eseen = 1

100 101 102
	def getremote(self, proxy = None):
		if proxy:
			self.proxy = proxy
103 104 105 106 107 108
		try:
			self.rrev = self.proxy.head(self.file)
		except (os.error, IOError):
			self.rrev = None
		if self.rrev:
			self.rsum = self.proxy.sum(self.file)
Guido van Rossum's avatar
Guido van Rossum committed
109
		else:
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
			self.rsum = None
		if self.eseen:
			self.getesum()
		self.rseen = 1

	def getesum(self):
		if self.erev == self.rrev:
			self.esum = self.rsum
		elif self.erev:
			name = (self.file, self.erev)
			self.esum = self.proxy.sum(name)
		else:
			self.esum = None

	def putentry(self):
		"""Return a line suitable for inclusion in CVS/Entries.

		The returned line is terminated by a newline.
		If no entry should be written for this file,
		return "".
		"""
		if not self.eseen:
			return ""

		rev = self.erev or '0'
		if self.edeleted:
			rev = '-' + rev
		if self.enew:
			dates = 'Initial ' + self.file
		else:
			dates = gmctime(self.ectime) + ' ' + \
				gmctime(self.emtime)
Guido van Rossum's avatar
Guido van Rossum committed
142 143
		return "/%s/%s/%s/%s/\n" % (
			self.file,
144
			rev,
Guido van Rossum's avatar
Guido van Rossum committed
145 146
			dates,
			self.extra)
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162

	def report(self):
		print '-'*50
		def r(key, repr=repr, self=self):
			try:
				value = repr(getattr(self, key))
			except AttributeError:
				value = "?"
			print "%-15s:" % key, value
		r("file")
		if self.lseen:
			r("lsum", hexify)
			r("lctime", gmctime)
			r("lmtime", gmctime)
		if self.eseen:
			r("erev")
163 164
			r("enew")
			r("edeleted")
165 166 167 168 169 170 171
			r("ectime", gmctime)
			r("emtime", gmctime)
		if self.rseen:
			r("rrev")
			r("rsum", hexify)
			if self.eseen:
				r("esum", hexify)
Guido van Rossum's avatar
Guido van Rossum committed
172 173 174


class CVS:
175 176
	
	"""Represent the contents of a CVS admin file (and more).
Guido van Rossum's avatar
Guido van Rossum committed
177

178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
	Class variables:

	FileClass -- the class to be instantiated for entries
	             (this should be derived from class File above)
	IgnoreList -- shell patterns for local files to be ignored

	Instance variables:

	entries -- a dictionary containing File instances keyed by
	           their file name
	proxy -- an RCSProxy instance, or None
	"""
	
	FileClass = File

	IgnoreList = ['.*', '@*', ',*', '*~', '*.o', '*.a', '*.so', '*.pyc']
Guido van Rossum's avatar
Guido van Rossum committed
194 195
	
	def __init__(self):
196 197 198 199 200 201 202 203 204
		self.entries = {}
		self.proxy = None
	
	def setproxy(self, proxy):
		if proxy is self.proxy:
			return
		self.proxy = proxy
		for e in self.entries.values():
			e.rseen = 0
Guido van Rossum's avatar
Guido van Rossum committed
205
	
206 207
	def getentries(self):
		"""Read the contents of CVS/Entries"""
Guido van Rossum's avatar
Guido van Rossum committed
208 209 210 211 212
		self.entries = {}
		f = self.cvsopen("Entries")
		while 1:
			line = f.readline()
			if not line: break
213 214
			e = self.FileClass()
			e.getentry(line)
Guido van Rossum's avatar
Guido van Rossum committed
215 216 217
			self.entries[e.file] = e
		f.close()
	
218 219
	def putentries(self):
		"""Write CVS/Entries back"""
Guido van Rossum's avatar
Guido van Rossum committed
220
		f = self.cvsopen("Entries", 'w')
221
		for e in self.values():
222
			f.write(e.putentry())
Guido van Rossum's avatar
Guido van Rossum committed
223
		f.close()
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257

	def getlocalfiles(self):
		list = self.entries.keys()
		addlist = os.listdir(os.curdir)
		for name in addlist:
			if name in list:
				continue
			if not self.ignored(name):
				list.append(name)
		list.sort()
		for file in list:
			try:
				e = self.entries[file]
			except KeyError:
				e = self.entries[file] = self.FileClass(file)
			e.getlocal()

	def getremotefiles(self, proxy = None):
		if proxy:
			self.proxy = proxy
		if not self.proxy:
			raise RuntimeError, "no RCS proxy"
		addlist = self.proxy.listfiles()
		for file in addlist:
			try:
				e = self.entries[file]
			except KeyError:
				e = self.entries[file] = self.FileClass(file)
			e.getremote(self.proxy)

	def report(self):
		for e in self.values():
			e.report()
		print '-'*50
Guido van Rossum's avatar
Guido van Rossum committed
258 259 260 261 262 263
	
	def keys(self):
		keys = self.entries.keys()
		keys.sort()
		return keys

264 265 266 267 268 269 270 271 272 273
	def values(self):
		def value(key, self=self):
			return self.entries[key]
		return map(value, self.keys())

	def items(self):
		def item(key, self=self):
			return (key, self.entries[key])
		return map(item, self.keys())

Guido van Rossum's avatar
Guido van Rossum committed
274 275 276 277 278 279 280 281 282 283 284 285 286
	def cvsexists(self, file):
		file = os.path.join("CVS", file)
		return os.path.exists(file)
	
	def cvsopen(self, file, mode = 'r'):
		file = os.path.join("CVS", file)
		if 'r' not in mode:
			self.backup(file)
		return open(file, mode)
	
	def backup(self, file):
		if os.path.isfile(file):
			bfile = file + '~'
287 288
			try: os.unlink(bfile)
			except os.error: pass
Guido van Rossum's avatar
Guido van Rossum committed
289 290
			os.rename(file, bfile)

291 292 293 294 295 296 297 298
	def ignored(self, file):
		if os.path.isdir(file): return 1
		for pat in self.IgnoreList:
			if fnmatch.fnmatch(file, pat): return 1
		return 0


# hexify and unhexify are useful to print MD5 checksums in hex format
Guido van Rossum's avatar
Guido van Rossum committed
299 300 301 302

hexify_format = '%02x' * 16
def hexify(sum):
	"Return a hex representation of a 16-byte string (e.g. an MD5 digest)"
303 304
	if sum is None:
		return "None"
Guido van Rossum's avatar
Guido van Rossum committed
305 306 307 308
	return hexify_format % tuple(map(ord, sum))

def unhexify(hexsum):
	"Return the original from a hexified string"
309 310
	if hexsum == "None":
		return None
Guido van Rossum's avatar
Guido van Rossum committed
311 312 313 314 315 316 317 318
	sum = ''
	for i in range(0, len(hexsum), 2):
		sum = sum + chr(string.atoi(hexsum[i:i+2], 16))
	return sum


unctime_monthmap = {}
def unctime(date):
319
	if date == "None": return None
Guido van Rossum's avatar
Guido van Rossum committed
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
	if not unctime_monthmap:
		months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
			  'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
		i = 0
		for m in months:
			i = i+1
			unctime_monthmap[m] = i
	words = string.split(date) # Day Mon DD HH:MM:SS YEAR
	year = string.atoi(words[4])
	month = unctime_monthmap[words[1]]
	day = string.atoi(words[2])
	[hh, mm, ss] = map(string.atoi, string.splitfields(words[3], ':'))
	ss = ss - time.timezone
	return time.mktime((year, month, day, hh, mm, ss, 0, 0, 0))

def gmctime(t):
336
	if t is None: return "None"
Guido van Rossum's avatar
Guido van Rossum committed
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
	return time.asctime(time.gmtime(t))

def test_unctime():
	now = int(time.time())
	t = time.gmtime(now)
	at = time.asctime(t)
	print 'GMT', now, at
	print 'timezone', time.timezone
	print 'local', time.ctime(now)
	u = unctime(at)
	print 'unctime()', u
	gu = time.gmtime(u)
	print '->', gu
	print time.asctime(gu)

def test():
	x = CVS()
354 355 356 357 358 359 360
	x.getentries()
	x.getlocalfiles()
##	x.report()
	import rcsclient
	proxy = rcsclient.openrcsclient()
	x.getremotefiles(proxy)
	x.report()
Guido van Rossum's avatar
Guido van Rossum committed
361 362 363 364


if __name__ == "__main__":
	test()