#!/usr/bin/env python
#
# scons-diff.py - diff-like utility for comparing SCons trees
#
# This supports most common diff options (with some quirks, like you can't
# just say -c and have it use a default value), but canonicalizes the
# various version strings within the file like __revision__, __build__,
# etc. so that you can diff trees without having to ignore changes in
# version lines.
#

import difflib
import getopt
import os.path
import re
import sys

Usage = """\
Usage: scons-diff.py [OPTIONS] dir1 dir2
Options:
    -c NUM, --context=NUM       Print NUM lines of copied context.
    -h, --help                  Print this message and exit.
    -n                          Don't canonicalize SCons lines.
    -q, --quiet                 Print only whether files differ.
    -r, --recursive             Recursively compare found subdirectories.
    -s                          Report when two files are the same.
    -u NUM, --unified=NUM       Print NUM lines of unified context.
"""

opts, args = getopt.getopt(sys.argv[1:],
                           'c:dhnqrsu:',
		           ['context=', 'help', 'recursive', 'unified='])

diff_type = None
edit_type = None
context = 2
recursive = False
report_same = False
diff_options = []

def diff_line(left, right):
    if diff_options:
        opts = ' ' + ' '.join(diff_options)
    else:
        opts = ''
    print 'diff%s %s %s' % (opts, left, right)

for o, a in opts:
    if o in ('-c', '-u'):
        diff_type = o
        context = int(a)
        diff_options.append(o)
    elif o in ('-h', '--help'):
        print Usage
	sys.exit(0)
    elif o in ('-n'):
        diff_options.append(o)
        edit_type = o
    elif o in ('-q'):
        diff_type = o
        diff_line = lambda l, r: None
    elif o in ('-r', '--recursive'):
        recursive = True
        diff_options.append(o)
    elif o in ('-s'):
        report_same = True

try:
    left, right = args
except ValueError:
    sys.stderr.write(Usage)
    sys.exit(1)

def quiet_diff(a, b, fromfile='', tofile='',
               fromfiledate='', tofiledate='', n=3, lineterm='\n'):
    """
    A function with the same calling signature as difflib.context_diff
    (diff -c) and difflib.unified_diff (diff -u) but which prints
    output like the simple, unadorned 'diff" command.
    """
    if a == b:
        return []
    else:
        return ['Files %s and %s differ\n' % (fromfile, tofile)]

def simple_diff(a, b, fromfile='', tofile='',
                fromfiledate='', tofiledate='', n=3, lineterm='\n'):
    """
    A function with the same calling signature as difflib.context_diff
    (diff -c) and difflib.unified_diff (diff -u) but which prints
    output like the simple, unadorned 'diff" command.
    """
    sm = difflib.SequenceMatcher(None, a, b)
    def comma(x1, x2):
        return x1+1 == x2 and str(x2) or '%s,%s' % (x1+1, x2)
    result = []
    for op, a1, a2, b1, b2 in sm.get_opcodes():
        if op == 'delete':
            result.append("%sd%d\n" % (comma(a1, a2), b1))
            result.extend(['< ' + l for l in a[a1:a2]])
        elif op == 'insert':
            result.append("%da%s\n" % (a1, comma(b1, b2)))
            result.extend(['> ' + l for l in b[b1:b2]])
        elif op == 'replace':
            result.append("%sc%s\n" % (comma(a1, a2), comma(b1, b2)))
            result.extend(['< ' + l for l in a[a1:a2]])
            result.append('---\n')
            result.extend(['> ' + l for l in b[b1:b2]])
    return result

diff_map = {
    '-c'        : difflib.context_diff,
    '-q'        : quiet_diff,
    '-u'        : difflib.unified_diff,
}

diff_function = diff_map.get(diff_type, simple_diff)

baseline_re = re.compile('(# |@REM )/home/\S+/baseline/')
comment_rev_re = re.compile('(# |@REM )(\S+) 0.96.[CD]\d+ \S+ \S+( knight)')
revision_re = re.compile('__revision__ = "[^"]*"')
build_re = re.compile('__build__ = "[^"]*"')
date_re = re.compile('__date__ = "[^"]*"')

def lines_read(file):
    return open(file).readlines()

def lines_massage(file):
    text = open(file).read()
    text = baseline_re.sub('\\1', text)
    text = comment_rev_re.sub('\\1\\2\\3', text)
    text = revision_re.sub('__revision__ = "bin/scons-diff.py"', text)
    text = build_re.sub('__build__ = "0.96.92.DXXX"', text)
    text = date_re.sub('__date__ = "2006/08/25 02:59:00"', text)
    return text.splitlines(1)

lines_map = {
    '-n'        : lines_read,
}

lines_function = lines_map.get(edit_type, lines_massage)

def do_diff(left, right, diff_subdirs):
    if os.path.isfile(left) and os.path.isfile(right):
        diff_file(left, right)
    elif not os.path.isdir(left):
        diff_file(left, os.path.join(right, os.path.split(left)[1]))
    elif not os.path.isdir(right):
        diff_file(os.path.join(left, os.path.split(right)[1]), right)
    elif diff_subdirs:
        diff_dir(left, right)

def diff_file(left, right):
    l = lines_function(left)
    r = lines_function(right)
    d = diff_function(l, r, left, right, context)
    try:
        text = ''.join(d)
    except IndexError:
        sys.stderr.write('IndexError diffing %s and %s\n' % (left, right))
    else:
        if text:
            diff_line(left, right)
            print text,
        elif report_same:
            print 'Files %s and %s are identical' % (left, right)

def diff_dir(left, right):
    llist = os.listdir(left)
    rlist = os.listdir(right)
    u = {}
    for l in llist:
        u[l] = 1
    for r in rlist:
        u[r] = 1
    for x in sorted([ x for x in u.keys() if x[-4:] != '.pyc' ]):
        if x in llist:
            if x in rlist:
                do_diff(os.path.join(left, x),
                        os.path.join(right, x),
                        recursive)
            else:
                print 'Only in %s: %s' % (left, x)
        else:
            print 'Only in %s: %s' % (right, x)

do_diff(left, right, True)

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:
