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