import re, os, sys
from difflib import SequenceMatcher, context_diff
from datetime import datetime
from itertools import islice
vline = re.compile(r'\.V(?:_\S+)? (\S+) (\S+) ?(.*)').match
nline = re.compile(r'\.N (.*)').match
cline = re.compile(r'\.C (\d+) (\d+)').match
iline = re.compile(r'\.I (\d+)').match
def get_version(repo_fn, v=None):
# Return version *v* or the last version if *v* is None.
if not os.path.exists(repo_fn):
return ''
currver = 0
curr = []
f = iter(open(repo_fn, 'r'))
line = next(f, '')
while line and (v is None or currver < v):
# Start building up next version from the last
currver += 1
prev, curr = curr, []
# Process mandatory .V line and optional .N msg lines
assert line.startswith('.V')
for line in f:
if not line.startswith('.N'):
break
# Process the .I and .C instructions
while line and line.startswith(('.I', '.C')):
if line.startswith('.I'):
n = int(iline(line).group(1))
curr.extend(islice(f, n))
else:
m, n = map(int, cline(line).groups())
curr.extend(prev[m-1:n])
line = next(f, '')
return ''.join(curr)
def print_log(repo_fn, v=None):
# Print log entries. *v* is a specific version number or None to print all.
currver = 0
f = iter(open(repo_fn, 'r'))
line = next(f, '')
while line:
# Process mandatory .V line and optional .N msg lines
assert line.startswith('.V')
repo_fn, datetime, msg = vline(line).groups()
for line in f:
if line.startswith('.N'):
msg += '\n' + nline(line).group(1)
else:
break
currver += 1
if v is None:
print "%s %d %18s %s" % (repo_fn, currver, datetime, msg)
elif currver == v:
print "%s %d %18s %s" % (repo_fn, currver, datetime, msg)
return
# Skip through the .I and .C instructions
while line and line.startswith(('.I', '.C')):
if line.startswith('.I'):
n = int(iline(line).group(1))
for line in islice(f, n):
pass
line = next(f, '')
def diff(repo_fn, vnum1=None, vnum2=None, context=False):
# vnum1 or vnum2 can be None to indicate last version in repository
# vnum2 can be a filename to compare to
v1 = get_version(repo_fn, vnum1).splitlines(True)
if isinstance(vnum2, int) or vnum2 is None:
v2 = get_version(repo_fn, vnum2).splitlines(True)
else:
v2 = open(vnum2).readlines()
results = []
if context:
return ''.join(context_diff(v1, v2))
for tag, i1, i2, j1, j2 in SequenceMatcher(None, v1, v2).get_opcodes():
if tag in ('replace', 'insert'):
results.append('.I %d\n' % (j2-j1))
results.extend(v2[j1:j2])
elif tag == 'equal':
results.append('.C %d %d\n' % (i1+1, i2))
return ''.join(results)
def make_header(filename, msg, create):
first = '_,03000' if create else ''
datestring = datetime.now().strftime('%d-%b-%y,%H:%M:%S')
return '.V%(first)s %(filename)s %(datestring)s %(msg)s' % locals()
def get_repo_fn(filename):
path, fullname = os.path.split(filename)
base, ext = os.path.splitext(fullname)
ext = ext or '.'
newext = ext[:2] + '$' + ext[3:]
result = os.path.join(repo_dir, base + newext)
return result
repo_dir = os.environ.get('VCS', '.')
# ------- Command-line interface -------
help_msg = '''
Usage:
vcs add foo.bar "Checkin message"
vcs extract foo.bar [revnum]
vcs log foo.bar [revnum]
vcs diff foo.bar [revnum1 [revnum2]]
Repository:
%s
''' % repo_dir
def talkback(msg, help=False, code=1):
print >> sys.stderr, msg
if help:
print >> sys.stderr, '\n' + help_msg
sys.exit(code)
def main(argv):
# XXX add support for branching
# XXX support .N for output
if len(argv) <= 1:
talkback(help_msg, code=0)
if len(argv) < 3:
talkback('Not enough arguments. Need a command and filename.', help=True)
command = argv[1].lower()
if command not in 'log extract diff add update l e d a u'.split():
talkback('Unknown command: ' + command, help=True)
command = command[:1]
filename = argv[2]
repo_fn = get_repo_fn(filename)
if command in 'le':
if not os.path.exists(repo_fn):
talkback(repo_fn + ' not found')
v = int(argv[3]) if len(argv) >= 4 else None
if command == 'l':
print_log(repo_fn, v)
else:
print get_version(repo_fn, v),
elif command == 'd':
v1 = int(argv[3]) if len(argv) >= 4 else None
v2 = int(argv[4]) if len(argv) >= 5 else filename
print diff(repo_fn, v1, v2, context=True),
elif command in 'au':
if not os.path.exists(filename):
talkback('Cannot find file: ' + filename)
d = diff(repo_fn, None, filename)
if len(d.splitlines()) == 1:
talkback('File is already current. There are no changes.', code=0)
msg = ' '.join(argv[3:])
create = not os.path.exists(repo_fn)
repo_file = open(repo_fn, 'a+')
print >> repo_file, make_header(filename, msg, create)
print >> repo_file, d,
repo_file.close()
talkback('Added to ' + repo_fn, code=0)
else:
talkback('Unreachable')
if __name__ == '__main__':
main(sys.argv)