Welcome, guest | Sign In | My Account | Store | Cart
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)

History

  • revision 2 (12 years ago)
  • previous revisions are not available