d1e48cf590ac86ba8c1b19e189fff457872e08e9
[scons.git] / bin / scons-diff.py
1 #!/usr/bin/env python
2 #
3 # scons-diff.py - diff-like utility for comparing SCons trees
4 #
5 # This supports most common diff options (with some quirks, like you can't
6 # just say -c and have it use a default value), but canonicalizes the
7 # various version strings within the file like __revision__, __build__,
8 # etc. so that you can diff trees without having to ignore changes in
9 # version lines.
10 #
11
12 import difflib
13 import getopt
14 import os.path
15 import re
16 import sys
17
18 Usage = """\
19 Usage: scons-diff.py [OPTIONS] dir1 dir2
20 Options:
21     -c NUM, --context=NUM       Print NUM lines of copied context.
22     -h, --help                  Print this message and exit.
23     -n                          Don't canonicalize SCons lines.
24     -q, --quiet                 Print only whether files differ.
25     -r, --recursive             Recursively compare found subdirectories.
26     -s                          Report when two files are the same.
27     -u NUM, --unified=NUM       Print NUM lines of unified context.
28 """
29
30 opts, args = getopt.getopt(sys.argv[1:],
31                            'c:dhnqrsu:',
32                            ['context=', 'help', 'recursive', 'unified='])
33
34 diff_type = None
35 edit_type = None
36 context = 2
37 recursive = False
38 report_same = False
39 diff_options = []
40
41 def diff_line(left, right):
42     if diff_options:
43         opts = ' ' + ' '.join(diff_options)
44     else:
45         opts = ''
46     print 'diff%s %s %s' % (opts, left, right)
47
48 for o, a in opts:
49     if o in ('-c', '-u'):
50         diff_type = o
51         context = int(a)
52         diff_options.append(o)
53     elif o in ('-h', '--help'):
54         print Usage
55         sys.exit(0)
56     elif o in ('-n'):
57         diff_options.append(o)
58         edit_type = o
59     elif o in ('-q'):
60         diff_type = o
61         diff_line = lambda l, r: None
62     elif o in ('-r', '--recursive'):
63         recursive = True
64         diff_options.append(o)
65     elif o in ('-s'):
66         report_same = True
67
68 try:
69     left, right = args
70 except ValueError:
71     sys.stderr.write(Usage)
72     sys.exit(1)
73
74 def quiet_diff(a, b, fromfile='', tofile='',
75                fromfiledate='', tofiledate='', n=3, lineterm='\n'):
76     """
77     A function with the same calling signature as difflib.context_diff
78     (diff -c) and difflib.unified_diff (diff -u) but which prints
79     output like the simple, unadorned 'diff" command.
80     """
81     if a == b:
82         return []
83     else:
84         return ['Files %s and %s differ\n' % (fromfile, tofile)]
85
86 def simple_diff(a, b, fromfile='', tofile='',
87                 fromfiledate='', tofiledate='', n=3, lineterm='\n'):
88     """
89     A function with the same calling signature as difflib.context_diff
90     (diff -c) and difflib.unified_diff (diff -u) but which prints
91     output like the simple, unadorned 'diff" command.
92     """
93     sm = difflib.SequenceMatcher(None, a, b)
94     def comma(x1, x2):
95         return x1+1 == x2 and str(x2) or '%s,%s' % (x1+1, x2)
96     result = []
97     for op, a1, a2, b1, b2 in sm.get_opcodes():
98         if op == 'delete':
99             result.append("%sd%d\n" % (comma(a1, a2), b1))
100             result.extend(['< ' + l for l in a[a1:a2]])
101         elif op == 'insert':
102             result.append("%da%s\n" % (a1, comma(b1, b2)))
103             result.extend(['> ' + l for l in b[b1:b2]])
104         elif op == 'replace':
105             result.append("%sc%s\n" % (comma(a1, a2), comma(b1, b2)))
106             result.extend(['< ' + l for l in a[a1:a2]])
107             result.append('---\n')
108             result.extend(['> ' + l for l in b[b1:b2]])
109     return result
110
111 diff_map = {
112     '-c'        : difflib.context_diff,
113     '-q'        : quiet_diff,
114     '-u'        : difflib.unified_diff,
115 }
116
117 diff_function = diff_map.get(diff_type, simple_diff)
118
119 baseline_re = re.compile('(# |@REM )/home/\S+/baseline/')
120 comment_rev_re = re.compile('(# |@REM )(\S+) 0.96.[CD]\d+ \S+ \S+( knight)')
121 revision_re = re.compile('__revision__ = "[^"]*"')
122 build_re = re.compile('__build__ = "[^"]*"')
123 date_re = re.compile('__date__ = "[^"]*"')
124
125 def lines_read(file):
126     return open(file).readlines()
127
128 def lines_massage(file):
129     text = open(file).read()
130     text = baseline_re.sub('\\1', text)
131     text = comment_rev_re.sub('\\1\\2\\3', text)
132     text = revision_re.sub('__revision__ = "__FILE__"', text)
133     text = build_re.sub('__build__ = "0.96.92.DXXX"', text)
134     text = date_re.sub('__date__ = "2006/08/25 02:59:00"', text)
135     return text.splitlines(1)
136
137 lines_map = {
138     '-n'        : lines_read,
139 }
140
141 lines_function = lines_map.get(edit_type, lines_massage)
142
143 def do_diff(left, right, diff_subdirs):
144     if os.path.isfile(left) and os.path.isfile(right):
145         diff_file(left, right)
146     elif not os.path.isdir(left):
147         diff_file(left, os.path.join(right, os.path.split(left)[1]))
148     elif not os.path.isdir(right):
149         diff_file(os.path.join(left, os.path.split(right)[1]), right)
150     elif diff_subdirs:
151         diff_dir(left, right)
152
153 def diff_file(left, right):
154     l = lines_function(left)
155     r = lines_function(right)
156     d = diff_function(l, r, left, right, context)
157     try:
158         text = ''.join(d)
159     except IndexError:
160         sys.stderr.write('IndexError diffing %s and %s\n' % (left, right))
161     else:
162         if text:
163             diff_line(left, right)
164             print text,
165         elif report_same:
166             print 'Files %s and %s are identical' % (left, right)
167
168 def diff_dir(left, right):
169     llist = os.listdir(left)
170     rlist = os.listdir(right)
171     u = {}
172     for l in llist:
173         u[l] = 1
174     for r in rlist:
175         u[r] = 1
176     clist = [ x for x in u.keys() if x[-4:] != '.pyc' ]
177     clist.sort()
178     for x in clist:
179         if x in llist:
180             if x in rlist:
181                 do_diff(os.path.join(left, x),
182                         os.path.join(right, x),
183                         recursive)
184             else:
185                 print 'Only in %s: %s' % (left, x)
186         else:
187             print 'Only in %s: %s' % (right, x)
188
189 do_diff(left, right, True)
190
191 # Local Variables:
192 # tab-width:4
193 # indent-tabs-mode:nil
194 # End:
195 # vim: set expandtab tabstop=4 shiftwidth=4: