Initial revision
diff --git a/Demo/pdist/RCSProxy.py b/Demo/pdist/RCSProxy.py
new file mode 100755
index 0000000..0a12157
--- /dev/null
+++ b/Demo/pdist/RCSProxy.py
@@ -0,0 +1,285 @@
+#! /usr/local/bin/python
+
+"""RCS Proxy.
+
+Provide a simplified interface on RCS files, locally or remotely.
+The functionality is geared towards implementing some sort of
+remote CVS like utility.  It is modeled after the similar module
+FSProxy.
+
+The module defines three classes:
+
+RCSProxyLocal  -- used for local access
+RCSProxyServer -- used on the server side of remote access
+RCSProxyClient -- used on the client side of remote access
+
+The remote classes are instantiated with an IP address and an optional
+verbosity flag.
+"""
+
+import server
+import client
+import md5
+import os
+import fnmatch
+import string
+import tempfile
+
+
+okchars = string.letters + string.digits + '-_=+.'
+
+
+class RCSProxyLocal:
+	
+	def __init__(self):
+		self._dirstack = []
+	
+	def _close(self):
+		while self._dirstack:
+			self.back()
+	
+	def pwd(self):
+		return os.getcwd()
+	
+	def cd(self, name):
+		save = os.getcwd()
+		os.chdir(name)
+		self._dirstack.append(save)
+	
+	def back(self):
+		if not self._dirstack:
+			raise os.error, "empty directory stack"
+		dir = self._dirstack[-1]
+		os.chdir(dir)
+		del self._dirstack[-1]
+	
+	def _filter(self, files, pat = None):
+		if pat:
+			def keep(name, pat = pat):
+				return fnmatch.fnmatch(name, pat)
+			files = filter(keep, files)
+		files.sort()
+		return files
+
+	def isfile(self, name):
+		namev = name + ',v'
+		return os.path.isfile(namev) or \
+		       os.path.isfile(os.path.join('RCS', namev))
+	
+	def _unmangle(self, name):
+		if type(name) == type(''):
+			rev = ''
+		else:
+			name, rev = name
+		return name, rev
+	
+	def checkfile(self, name):
+		name, rev = self._unmangle(name)
+		if not self.isfile(name):
+			raise os.error, 'not an rcs file %s' % `name`
+		for c in rev:
+			if c not in okchars:
+				raise ValueError, "bad char in rev"
+		return name, rev
+	
+	def listfiles(self, pat = None):
+		def isrcs(name): return name[-2:] == ',v'
+		def striprcs(name): return name[:-2]
+		files = os.listdir(os.curdir)
+		files = filter(isrcs, files)
+		if os.path.isdir('RCS'):
+			files2 = os.listdir('RCS')
+			files2 = filter(isrcs, files2)
+			files = files + files2
+		files = map(striprcs, files)
+		return self._filter(files, pat)
+	
+	def listsubdirs(self, pat = None):
+		files = os.listdir(os.curdir)
+		files = filter(os.path.isdir, files)
+		return self._filter(files, pat)
+	
+	def isdir(self, name):
+		return os.path.isdir(name)
+
+	def _open(self, name, cmd = 'co -p'):
+		name, rev = self.checkfile(name)
+		namev = name + ',v'
+		if rev:
+			cmd = cmd + ' -r' + rev
+		return os.popen('%s %s' %  (cmd, `namev`))
+
+	def _closepipe(self, f):
+		sts = f.close()
+		if sts:
+			raise IOError, "Exit status %d" % sts
+	
+	def _remove(self, fn):
+		try:
+			os.unlink(fn)
+		except os.error:
+			pass
+	
+	def sum(self, name):
+		f = self._open(name)
+		BUFFERSIZE = 1024*8
+		sum = md5.new()
+		while 1:
+			buffer = f.read(BUFFERSIZE)
+			if not buffer:
+				break
+			sum.update(buffer)
+		self._closepipe(f)
+		return sum.digest()
+	
+	def _list(self, function, list):
+		if list is None:
+			list = self.listfiles()
+		res = []
+		for name in list:
+			try:
+				res.append((name, function(name)))
+			except (os.error, IOError):
+				res.append((name, None))
+		return res
+	
+	def sumlist(self, list = None):
+		return self.list(self.sum, list)
+	
+	def _dict(self, function, list):
+		if list is None:
+			list = self.listfiles()
+		dict = {}
+		for name in list:
+			try:
+				dict[name] = function(name)
+			except (os.error, IOError):
+				pass
+		return dict
+	
+	def sumdict(self, list = None):
+		return self.dict(self.sum, list)
+	
+	def get(self, name):
+		f = self._open(name)
+		data = f.read()
+		self._closepipe(f)
+		return data
+
+	def info(self, name):
+		f = self._open(name, 'rlog -h')
+		dict = {}
+		while 1:
+			line = f.readline()
+			if not line: break
+			if line[0] == '\t':
+				continue # XXX lock details, later
+			i = string.find(line, ':')
+			if i > 0:
+				key, value = line[:i], string.strip(line[i+1:])
+				dict[key] = value
+		self._closepipe(f)
+		return dict
+	
+	def head(self, name):
+		dict = self.info(name)
+		return dict['head']
+	
+	def log(self, name, flags = ''):
+		f = self._open(name, 'rlog %s 2>&1' % flags)
+		log = f.read()
+		self._closepipe(f)
+		return log
+
+	def put(self, fullname, data, message = ""):
+		if message and message[-1] != '\n':
+			message = message + '\n'
+		name, rev = self._unmangle(fullname)
+		new = not self.isfile(name)
+		if new:
+			for c in name:
+				if c not in okchars:
+					raise ValueError, "bad char in name"
+		else:
+			self._remove(name)
+		f = open(name, 'w')
+		f.write(data)
+		f.close()
+		tf = tempfile.mktemp()
+		try:
+			if not new:
+			    cmd = "rcs -l%s %s >>%s 2>&1" % (rev, name, tf)
+			    sts = os.system(cmd)
+			    if sts:
+				raise IOError, "rcs -l exit status %d" % sts
+			cmd = "ci -r%s %s >>%s 2>&1" % (rev, name, tf)
+			p = os.popen(cmd, 'w')
+			p.write(message)
+			sts = p.close()
+			if sts:
+				raise IOError, "ci exit status %d" % sts
+			messages = open(tf).read()
+			return messages or None
+		finally:
+			self._remove(tf)
+	
+	def mkdir(self, name):
+		os.mkdir(name, 0777)
+	
+	def rmdir(self, name):
+		os.rmdir(name)
+
+
+class RCSProxyServer(RCSProxyLocal, server.Server):
+	
+	def __init__(self, address, verbose = server.VERBOSE):
+		RCSProxyLocal.__init__(self)
+		server.Server.__init__(self, address, verbose)
+	
+	def _close(self):
+		server.Server._close(self)
+		RCSProxyLocal._close(self)
+	
+	def _serve(self):
+		server.Server._serve(self)
+		# Retreat into start directory
+		while self._dirstack: self.back()
+
+
+class RCSProxyClient(client.Client):
+	
+	def __init__(self, address, verbose = client.VERBOSE):
+		client.Client.__init__(self, address, verbose)
+
+
+def test_server():
+	import string
+	import sys
+	if sys.argv[1:]:
+		port = string.atoi(sys.argv[1])
+	else:
+		port = 4127
+	proxy = RCSProxyServer(('', port))
+	proxy._serverloop()
+
+
+def test():
+	import sys
+	if not sys.argv[1:] or sys.argv[1] and sys.argv[1][0] in '0123456789':
+		test_server()
+		sys.exit(0)
+	proxy = RCSProxyLocal()
+	what = sys.argv[1]
+	if hasattr(proxy, what):
+		attr = getattr(proxy, what)
+		if callable(attr):
+			print apply(attr, tuple(sys.argv[2:]))
+		else:
+			print `attr`
+	else:
+		print "%s: no such attribute" % what
+		sys.exit(2)
+
+
+if __name__ == '__main__':
+	test()
diff --git a/Demo/pdist/README b/Demo/pdist/README
new file mode 100644
index 0000000..738126d
--- /dev/null
+++ b/Demo/pdist/README
@@ -0,0 +1,2 @@
+This directory contains various modules and classes that support
+remote file system operations
diff --git a/Demo/pdist/cvslib.py b/Demo/pdist/cvslib.py
new file mode 100755
index 0000000..c98f0ba
--- /dev/null
+++ b/Demo/pdist/cvslib.py
@@ -0,0 +1,186 @@
+"""Utilities to read and write CVS admin files (esp. CVS/Entries)"""
+
+import string
+import os
+import time
+
+
+class Entry:
+
+	"""Class representing one (parsed) line from CVS/Entries"""
+	
+	def __init__(self, line):
+		words = string.splitfields(line, '/')
+		self.file = words[1]
+		self.rev = words[2]
+		dates = words[3] # ctime, mtime
+		if dates[:7] == 'Initial':
+			self.ctime = None
+			self.mtime = None
+			self.new = 1
+		else:
+			self.ctime = unctime(dates[:24])
+			self.mtime = unctime(dates[25:])
+			self.new = 0
+		self.extra = words[4]
+		self.sum = None
+	
+	def unparse(self):
+		if self.new:
+			dates = "Initial %s" % self.file
+		else:
+			dates = gmctime(self.ctime) + ' ' + gmctime(self.mtime)
+		return "/%s/%s/%s/%s/\n" % (
+			self.file,
+			self.rev,
+			dates,
+			self.extra)
+	
+	def setsum(self, sum):
+		self.sum = sum
+	
+	def getsum(self):
+		return self.sum
+	
+	def sethexsum(self, hexsum):
+		self.setsum(unhexify(hexsum))
+	
+	def gethexsum(self):
+		if self.sum:
+			return hexify(self.sum)
+		else:
+			return None
+
+
+class CVS:
+
+	"""Class representing the contents of CVS/Entries (and CVS/Sums)"""
+	
+	def __init__(self):
+		self.readentries()
+	
+	def readentries(self):
+		self.entries = {}
+		f = self.cvsopen("Entries")
+		while 1:
+			line = f.readline()
+			if not line: break
+			e = Entry(line)
+			self.entries[e.file] = e
+		f.close()
+	
+	def readsums(self):
+		try:
+			f = self.cvsopen("Sums")
+		except IOError:
+			return
+		while 1:
+			line = f.readline()
+			if not line: break
+			words = string.split(line)
+			[file, rev, hexsum] = words
+			e = self.entries[file]
+			if e.rev == rev:
+				e.sethexsum(hexsum)
+		f.close()
+	
+	def writeentries(self):
+		f = self.cvsopen("Entries", 'w')
+		for file in self.keys():
+			f.write(self.entries[file].unparse())
+		f.close()
+	
+	def writesums(self):
+		if self.cvsexists("Sums"):
+			f = self.cvsopen("Sums", 'w')
+		else:
+			f = None
+		for file in self.keys():
+			e = self.entries[file]
+			hexsum = e.gethexsum()
+			if hexsum:
+				if not f:
+					f = self.cvsopen("Sums", 'w')
+				f.write("%s %s %s\n" % (file, e.rev, hexsum))
+		if f:
+			f.close()
+	
+	def keys(self):
+		keys = self.entries.keys()
+		keys.sort()
+		return keys
+
+	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 + '~'
+			os.rename(file, bfile)
+
+
+hexify_format = '%02x' * 16
+def hexify(sum):
+	"Return a hex representation of a 16-byte string (e.g. an MD5 digest)"
+	return hexify_format % tuple(map(ord, sum))
+
+def unhexify(hexsum):
+	"Return the original from a hexified string"
+	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):
+	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):
+	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()
+	keys = x.entries.keys()
+	keys.sort()
+	for file in keys:
+		e = x.entries[file]
+		print file, e.rev, gmctime(e.ctime), gmctime(e.mtime), e.extra,
+		print e.gethexsum()
+
+
+if __name__ == "__main__":
+	test()
diff --git a/Demo/pdist/rcvs.py b/Demo/pdist/rcvs.py
new file mode 100755
index 0000000..eb43bb8
--- /dev/null
+++ b/Demo/pdist/rcvs.py
@@ -0,0 +1,192 @@
+from cvslib import CVS, Entry
+import RCSProxy
+import client
+import md5
+import os
+import string
+import sys
+import time
+import fnmatch
+
+
+ignored_patterns = ['*.pyc', '.*', '*~', '@*']
+def ignored(file):
+	if os.path.isdir(file): return 1
+	for pat in ignored_patterns:
+		if fnmatch.fnmatch(file, pat): return 1
+	return 0
+
+
+class PCVS(CVS):
+	
+	def __init__(self, proxy):
+		CVS.__init__(self)
+		self.proxy = proxy
+		self.readsums()
+		self.calcsums()
+	
+	def calcsums(self):
+		for file in self.keys():
+			e = self.entries[file]
+			if not e.new and e.sum is None:
+				sum = self.proxy.sum((file, e.rev))
+				e.setsum(sum)
+	
+	def fullcheck(self):
+		ok = 1
+		for file in self.keys():
+			e = self.entries[file]
+			if e.new:
+				if self.proxy.isfile(file):
+					print "%s: created by someone else!"
+					ok = 0
+				continue
+			rrev = self.proxy.head(file)
+			if rrev != e.rev:
+				print "%s: out of date (%s vs. %s)" % \
+				      (file, e.rev, rrev)
+				ok = 0
+		return ok
+	
+	def update(self):
+		for file in self.keys():
+			e = self.entries[file]
+			if e.new:
+				print 'A', file
+				continue
+			rrev = self.proxy.head(file)
+			lsum = sumfile(file)
+			if rrev == e.rev:
+				if lsum == e.sum:
+					print '=', file
+				else:
+					print 'M', file
+				continue
+			if e.sum != lsum:
+				print "%s: conflict -- not updated" % file
+				continue
+			print "%s: getting ..." % file
+			data = self.proxy.get(file)
+			f = open(file, 'w')
+			f.write(data)
+			f.close()
+			nsum = md5.new(data).digest()
+			e.setsum(nsum)
+			e.rev = rrev
+			print 'U', file
+		self.writeentries()
+		self.writesums()
+	
+	def commit(self):
+		if not self.fullcheck():
+			print "correct above errors first"
+			return
+		needed = []
+		for file in self.keys():
+			e = self.entries[file]
+			if e.new:
+				needed.append(file)
+				continue
+			lsum = sumfile(file)
+			if lsum != e.sum:
+				needed.append(file)
+				continue
+		if not needed:
+			print "no changes need committing"
+			return
+		message = raw_input("One-liner: ")
+		for file in needed:
+			print "%s: putting ..." % file
+			e = self.entries[file]
+			data = open(file).read()
+			self.proxy.put(file, data, message)
+			e.rev = self.proxy.head(file)
+			e.setsum(self.proxy.sum(file))
+			# XXX get it?
+			mtime, ctime = os.stat(file)[-2:]
+			e.mtime = mtime
+			e.ctime = ctime
+		self.writeentries()
+		self.writesums()
+	
+	def report(self):
+		keys = self.keys()
+		files = os.listdir(os.curdir)
+		allfiles = files
+		for file in keys:
+			if file not in allfiles:
+				allfiles.append(file)
+		allfiles.sort()
+		for file in allfiles:
+			if file not in keys:
+				if not ignored(file):
+					print '?', file
+				continue
+			if file not in files:
+				print file, ': lost'
+				continue
+			e = self.entries[file]
+			if not os.path.exists(file):
+				print "%s: lost" % file
+				continue
+			if e.new:
+				print 'A', file
+				continue
+			lsum = sumfile(file)
+			rrev = self.proxy.head(file)
+			if rrev == e.rev:
+				if lsum == e.sum:
+					print '=', file
+				else:
+					print 'M', file
+			else:
+				if lsum == e.sum:
+					print 'U', file
+				else:
+					print 'C', file
+	
+	def add(self, file):
+		if self.entries.has_key(file):
+			print "%s: already known"
+		else:
+			self.entries[file] = Entry('/%s/0/Initial %s//\n' %
+						   (file, file))
+
+
+def sumfile(file):
+	return md5.new(open(file).read()).digest()
+
+
+def test():
+	proxy = RCSProxy.RCSProxyClient(('voorn.cwi.nl', 4127))
+	proxy.cd('/ufs/guido/voorn/python-RCS/Demo/pdist')
+	x = PCVS(proxy)
+	args = sys.argv[1:]
+	if args:
+		cmd = args[0]
+		files = args[1:]
+		if cmd == 'add':
+			if not files:
+				print "add needs at least one file argument"
+			else:
+				for file in files:
+					x.add(file)
+				x.writeentries()
+		elif cmd in ('update', 'up'):
+			if files:
+				print "updates wants no file arguments"
+			else:
+				x.update()
+		elif cmd in ('commit', 'com'):
+			if files:
+				print "commit wants no file arguments"
+			else:
+				x.commit()
+		else:
+			print "Unknown command", cmd
+	else:
+		x.report()
+		if sys.argv[1:]: x.writesums()
+
+if __name__ == "__main__":
+	test()
diff --git a/Demo/pdist/rrcs.py b/Demo/pdist/rrcs.py
new file mode 100755
index 0000000..74bce56
--- /dev/null
+++ b/Demo/pdist/rrcs.py
@@ -0,0 +1,174 @@
+#! /usr/local/bin/python
+
+import sys
+import os
+import getopt
+import string
+import md5
+import tempfile
+
+def main():
+	sys.stdout = sys.stderr
+	try:
+		opts, rest = getopt.getopt(sys.argv[1:], 'h:p:qv')
+		if not rest:
+			raise getopt.error, "missing command"
+		cmd, rest = rest[0], rest[1:]
+		if not commands.has_key(cmd):
+			raise getopt.error, "unknown command"
+		coptset, func = commands[cmd]
+		copts, files = getopt.getopt(rest, coptset)
+	except getopt.error, msg:
+		print msg
+		print "usage: rrcs [options] command [options] [file] ..."
+		print "where command can be:"
+		print "      ci|put      # checkin the given files"
+		print "      co|get      # checkout"
+		print "      info        # print header info"
+		print "      head        # print revision of head branch"
+		print "      list        # list filename if valid"
+		print "      log         # print full log"
+		print "      diff        # diff rcs file and work file"
+		print "if no files are given, all remote rcs files are assumed"
+		sys.exit(2)
+	x = openclient(opts)
+	if not files:
+		files = x.listfiles()
+	for fn in files:
+		try:
+			func(x, copts, fn)
+		except (IOError, os.error), msg:
+			print "%s: %s" % (fn, msg)
+
+def openclient(opts):
+	import client
+	import RCSProxy
+	host = 'spam'
+	port = 4127
+	verbose = client.VERBOSE
+	for o, a in opts:
+		if o == '-h':
+			host = a
+			if ':' in host:
+				i = string.find(host, ':')
+				host, p = host[:i], host[i+1:]
+				if p:
+					port = string.atoi(p)
+		if o == '-p':
+			port = string.atoi(a)
+		if o == '-v':
+			verbose = verbose + 1
+		if o == '-q':
+			verbose = 0
+	address = (host, port)
+	x = RCSProxy.RCSProxyClient(address, verbose)
+	return x
+
+def checkin(x, copts, fn):
+	f = open(fn)
+	data = f.read()
+	f.close()
+	new = not x.isfile(fn)
+	if not new and same(x, copts, fn, data):
+		print "%s: unchanged since last checkin" % fn
+		return
+	message = asklogmessage(new)
+	messages = x.put(fn, data, message)
+	if messages:
+		print messages
+
+def checkout(x, copts, fn):
+	data = x.get(fn)
+	f = open(fn, 'w')
+	f.write(data)
+	f.close()
+
+def info(x, copts, fn):
+	dict = x.info(fn)
+	keys = dict.keys()
+	keys.sort()
+	for key in keys:
+		print key + ':', dict[key]
+	print '='*70
+
+def head(x, copts, fn):
+	head = x.head(fn)
+	print fn, head
+
+def list(x, copts, fn):
+	if x.isfile(fn):
+		print fn
+
+def log(x, copts, fn):
+	flags = ''
+	for o, a in copts:
+		flags = flags + ' ' + o + a
+	flags = flags[1:]
+	messages = x.log(fn, flags)
+	print messages
+
+def diff(x, copts, fn):
+	if same(x, copts, fn):
+		return
+	flags = ''
+	for o, a in copts:
+		flags = flags + ' ' + o + a
+	flags = flags[1:]
+	data = x.get(fn)
+	tfn = tempfile.mktemp()
+	try:
+		tf = open(tfn, 'w')
+		tf.write(data)
+		tf.close()
+		print 'diff %s -r%s %s' % (flags, x.head(fn), fn)
+		sts = os.system('diff %s %s %s' % (flags, tfn, fn))
+		if sts:
+			print '='*70
+	finally:
+		remove(tfn)
+
+def same(x, copts, fn, data = None):
+	if data is None:
+		f = open(fn)
+		data = f.read()
+		f.close()
+	lsum = md5.new(data).digest()
+	rsum = x.sum(fn)
+	return lsum == rsum
+
+def asklogmessage(new):
+	if new:
+		print "enter description,",
+	else:
+		print "enter log message,",
+	print "terminate with single '.' or end of file:"
+	if new:
+		print "NOTE: This is NOT the log message!"
+	message = ""
+	while 1:
+		sys.stderr.write(">> ")
+		sys.stderr.flush()
+		line = sys.stdin.readline()
+		if not line or line == '.\n': break
+		message = message + line
+	return message
+
+def remove(fn):
+	try:
+		os.unlink(fn)
+	except os.error:
+		pass
+
+commands = {
+	'ci': ('', checkin),
+	'put': ('', checkin),
+	'co': ('', checkout),
+	'get': ('', checkout),
+	'info': ('', info),
+	'head': ('', head),
+	'list': ('', list),
+	'log': ('bhLRtd:l:r:s:w:V:', log),
+	'diff': ('c', diff),
+	}
+
+main()