|
0
|
1 |
# -*- coding: utf-8 -*- |
|
|
2 |
# |
|
|
3 |
# Copyright (C) 2004-2009 Edgewall Software |
|
|
4 |
# Copyright (C) 2004-2006 Christopher Lenz <cmlenz@gmx.de> |
|
|
5 |
# All rights reserved. |
|
|
6 |
# |
|
|
7 |
# This software is licensed as described in the file COPYING, which |
|
|
8 |
# you should have received as part of this distribution. The terms |
|
|
9 |
# are also available at http://trac.edgewall.org/wiki/TracLicense. |
|
|
10 |
# |
|
|
11 |
# This software consists of voluntary contributions made by many |
|
|
12 |
# individuals. For the exact contribution history, see the revision |
|
|
13 |
# history and logs, available at http://trac.edgewall.org/log/. |
|
|
14 |
# |
|
|
15 |
# Author: Christopher Lenz <cmlenz@gmx.de> |
|
|
16 |
|
|
|
17 |
from trac.util.html import escape, Markup |
|
|
18 |
from trac.util.text import expandtabs |
|
|
19 |
|
|
|
20 |
from difflib import SequenceMatcher |
|
|
21 |
import re |
|
|
22 |
|
|
|
23 |
__all__ = ['get_diff_options', 'hdf_diff', 'diff_blocks', 'unified_diff'] |
|
|
24 |
|
|
|
25 |
|
|
|
26 |
def _get_change_extent(str1, str2): |
|
|
27 |
""" |
|
|
28 |
Determines the extent of differences between two strings. Returns a tuple |
|
|
29 |
containing the offset at which the changes start, and the negative offset |
|
|
30 |
at which the changes end. If the two strings have neither a common prefix |
|
|
31 |
nor a common suffix, (0, 0) is returned. |
|
|
32 |
""" |
|
|
33 |
start = 0 |
|
|
34 |
limit = min(len(str1), len(str2)) |
|
|
35 |
while start < limit and str1[start] == str2[start]: |
|
|
36 |
start += 1 |
|
|
37 |
end = -1 |
|
|
38 |
limit = limit - start |
|
|
39 |
while -end <= limit and str1[end] == str2[end]: |
|
|
40 |
end -= 1 |
|
|
41 |
return (start, end + 1) |
|
|
42 |
|
|
|
43 |
def _get_opcodes(fromlines, tolines, ignore_blank_lines=False, |
|
|
44 |
ignore_case=False, ignore_space_changes=False): |
|
|
45 |
""" |
|
|
46 |
Generator built on top of SequenceMatcher.get_opcodes(). |
|
|
47 |
|
|
|
48 |
This function detects line changes that should be ignored and emits them |
|
|
49 |
as tagged as 'equal', possibly joined with the preceding and/or following |
|
|
50 |
'equal' block. |
|
|
51 |
""" |
|
|
52 |
|
|
|
53 |
def is_ignorable(tag, fromlines, tolines): |
|
|
54 |
if tag == 'delete' and ignore_blank_lines: |
|
|
55 |
if ''.join(fromlines) == '': |
|
|
56 |
return True |
|
|
57 |
elif tag == 'insert' and ignore_blank_lines: |
|
|
58 |
if ''.join(tolines) == '': |
|
|
59 |
return True |
|
|
60 |
elif tag == 'replace' and (ignore_case or ignore_space_changes): |
|
|
61 |
if len(fromlines) != len(tolines): |
|
|
62 |
return False |
|
|
63 |
def f(str): |
|
|
64 |
if ignore_case: |
|
|
65 |
str = str.lower() |
|
|
66 |
if ignore_space_changes: |
|
|
67 |
str = ' '.join(str.split()) |
|
|
68 |
return str |
|
|
69 |
for i in range(len(fromlines)): |
|
|
70 |
if f(fromlines[i]) != f(tolines[i]): |
|
|
71 |
return False |
|
|
72 |
return True |
|
|
73 |
|
|
|
74 |
matcher = SequenceMatcher(None, fromlines, tolines) |
|
|
75 |
previous = None |
|
|
76 |
for tag, i1, i2, j1, j2 in matcher.get_opcodes(): |
|
|
77 |
if tag == 'equal': |
|
|
78 |
if previous: |
|
|
79 |
previous = (tag, previous[1], i2, previous[3], j2) |
|
|
80 |
else: |
|
|
81 |
previous = (tag, i1, i2, j1, j2) |
|
|
82 |
else: |
|
|
83 |
if is_ignorable(tag, fromlines[i1:i2], tolines[j1:j2]): |
|
|
84 |
if previous: |
|
|
85 |
previous = 'equal', previous[1], i2, previous[3], j2 |
|
|
86 |
else: |
|
|
87 |
previous = 'equal', i1, i2, j1, j2 |
|
|
88 |
continue |
|
|
89 |
if previous: |
|
|
90 |
yield previous |
|
|
91 |
yield tag, i1, i2, j1, j2 |
|
|
92 |
previous = None |
|
|
93 |
|
|
|
94 |
if previous: |
|
|
95 |
yield previous |
|
|
96 |
|
|
|
97 |
def _group_opcodes(opcodes, n=3): |
|
|
98 |
""" |
|
|
99 |
Python 2.2 doesn't have SequenceMatcher.get_grouped_opcodes(), so let's |
|
|
100 |
provide equivalent here. The opcodes parameter can be any iterable or |
|
|
101 |
sequence. |
|
|
102 |
|
|
|
103 |
This function can also be used to generate full-context diffs by passing |
|
|
104 |
None for the parameter n. |
|
|
105 |
""" |
|
|
106 |
# Full context produces all the opcodes |
|
|
107 |
if n is None: |
|
|
108 |
yield list(opcodes) |
|
|
109 |
return |
|
|
110 |
|
|
|
111 |
# Otherwise we leave at most n lines with the tag 'equal' before and after |
|
|
112 |
# every change |
|
|
113 |
nn = n + n |
|
|
114 |
group = [] |
|
|
115 |
for idx, (tag, i1, i2, j1, j2) in enumerate(opcodes): |
|
|
116 |
if idx == 0 and tag == 'equal': # Fixup leading unchanged block |
|
|
117 |
i1, j1 = max(i1, i2 - n), max(j1, j2 - n) |
|
|
118 |
elif tag == 'equal' and i2 - i1 > nn: |
|
|
119 |
group.append((tag, i1, min(i2, i1 + n), j1, min(j2, j1 + n))) |
|
|
120 |
yield group |
|
|
121 |
group = [] |
|
|
122 |
i1, j1 = max(i1, i2 - n), max(j1, j2 - n) |
|
|
123 |
group.append((tag, i1, i2, j1 ,j2)) |
|
|
124 |
|
|
|
125 |
if group and not (len(group) == 1 and group[0][0] == 'equal'): |
|
|
126 |
if group[-1][0] == 'equal': # Fixup trailing unchanged block |
|
|
127 |
tag, i1, i2, j1, j2 = group[-1] |
|
|
128 |
group[-1] = tag, i1, min(i2, i1 + n), j1, min(j2, j1 + n) |
|
|
129 |
yield group |
|
|
130 |
|
|
|
131 |
def hdf_diff(*args, **kwargs): |
|
|
132 |
return diff_blocks(*args, **kwargs) |
|
|
133 |
|
|
|
134 |
def diff_blocks(fromlines, tolines, context=None, tabwidth=8, |
|
|
135 |
ignore_blank_lines=0, ignore_case=0, ignore_space_changes=0): |
|
|
136 |
"""Return an array that is adequate for adding to the data dictionary |
|
|
137 |
|
|
|
138 |
See the diff_div.html template. |
|
|
139 |
""" |
|
|
140 |
|
|
|
141 |
type_map = {'replace': 'mod', 'delete': 'rem', 'insert': 'add', |
|
|
142 |
'equal': 'unmod'} |
|
|
143 |
|
|
|
144 |
space_re = re.compile(' ( +)|^ ') |
|
|
145 |
def htmlify(match): |
|
|
146 |
div, mod = divmod(len(match.group(0)), 2) |
|
|
147 |
return div * ' ' + mod * ' ' |
|
|
148 |
|
|
|
149 |
def markup_intraline_changes(opcodes): |
|
|
150 |
for tag, i1, i2, j1, j2 in opcodes: |
|
|
151 |
if tag == 'replace' and i2 - i1 == j2 - j1: |
|
|
152 |
for i in range(i2 - i1): |
|
|
153 |
fromline, toline = fromlines[i1 + i], tolines[j1 + i] |
|
|
154 |
(start, end) = _get_change_extent(fromline, toline) |
|
|
155 |
if start != 0 or end != 0: |
|
|
156 |
last = end+len(fromline) |
|
|
157 |
fromlines[i1+i] = fromline[:start] + '\0' + fromline[start:last] + \ |
|
|
158 |
'\1' + fromline[last:] |
|
|
159 |
last = end+len(toline) |
|
|
160 |
tolines[j1+i] = toline[:start] + '\0' + toline[start:last] + \ |
|
|
161 |
'\1' + toline[last:] |
|
|
162 |
yield tag, i1, i2, j1, j2 |
|
|
163 |
|
|
|
164 |
changes = [] |
|
|
165 |
opcodes = _get_opcodes(fromlines, tolines, ignore_blank_lines, ignore_case, |
|
|
166 |
ignore_space_changes) |
|
|
167 |
for group in _group_opcodes(opcodes, context): |
|
|
168 |
blocks = [] |
|
|
169 |
last_tag = None |
|
|
170 |
for tag, i1, i2, j1, j2 in markup_intraline_changes(group): |
|
|
171 |
if tag != last_tag: |
|
|
172 |
blocks.append({'type': type_map[tag], |
|
|
173 |
'base': {'offset': i1, 'lines': []}, |
|
|
174 |
'changed': {'offset': j1, 'lines': []}}) |
|
|
175 |
if tag == 'equal': |
|
|
176 |
for line in fromlines[i1:i2]: |
|
|
177 |
line = line.expandtabs(tabwidth) |
|
|
178 |
line = space_re.sub(htmlify, escape(line, quotes=False)) |
|
|
179 |
blocks[-1]['base']['lines'].append(Markup(unicode(line))) |
|
|
180 |
for line in tolines[j1:j2]: |
|
|
181 |
line = line.expandtabs(tabwidth) |
|
|
182 |
line = space_re.sub(htmlify, escape(line, quotes=False)) |
|
|
183 |
blocks[-1]['changed']['lines'].append(Markup(unicode(line))) |
|
|
184 |
else: |
|
|
185 |
if tag in ('replace', 'delete'): |
|
|
186 |
for line in fromlines[i1:i2]: |
|
|
187 |
line = expandtabs(line, tabwidth, '\0\1') |
|
|
188 |
line = escape(line, quotes=False) |
|
|
189 |
line = '<del>'.join([space_re.sub(htmlify, seg) |
|
|
190 |
for seg in line.split('\0')]) |
|
|
191 |
line = line.replace('\1', '</del>') |
|
|
192 |
blocks[-1]['base']['lines'].append( |
|
|
193 |
Markup(unicode(line))) |
|
|
194 |
if tag in ('replace', 'insert'): |
|
|
195 |
for line in tolines[j1:j2]: |
|
|
196 |
line = expandtabs(line, tabwidth, '\0\1') |
|
|
197 |
line = escape(line, quotes=False) |
|
|
198 |
line = '<ins>'.join([space_re.sub(htmlify, seg) |
|
|
199 |
for seg in line.split('\0')]) |
|
|
200 |
line = line.replace('\1', '</ins>') |
|
|
201 |
blocks[-1]['changed']['lines'].append( |
|
|
202 |
Markup(unicode(line))) |
|
|
203 |
changes.append(blocks) |
|
|
204 |
return changes |
|
|
205 |
|
|
|
206 |
def unified_diff(fromlines, tolines, context=None, ignore_blank_lines=0, |
|
|
207 |
ignore_case=0, ignore_space_changes=0): |
|
|
208 |
opcodes = _get_opcodes(fromlines, tolines, ignore_blank_lines, ignore_case, |
|
|
209 |
ignore_space_changes) |
|
|
210 |
for group in _group_opcodes(opcodes, context): |
|
|
211 |
i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4] |
|
|
212 |
if i1 == 0 and i2 == 0: |
|
|
213 |
i1, i2 = -1, -1 # support for 'A'dd changes |
|
|
214 |
yield '@@ -%d,%d +%d,%d @@' % (i1 + 1, i2 - i1, j1 + 1, j2 - j1) |
|
|
215 |
for tag, i1, i2, j1, j2 in group: |
|
|
216 |
if tag == 'equal': |
|
|
217 |
for line in fromlines[i1:i2]: |
|
|
218 |
yield ' ' + line |
|
|
219 |
else: |
|
|
220 |
if tag in ('replace', 'delete'): |
|
|
221 |
for line in fromlines[i1:i2]: |
|
|
222 |
yield '-' + line |
|
|
223 |
if tag in ('replace', 'insert'): |
|
|
224 |
for line in tolines[j1:j2]: |
|
|
225 |
yield '+' + line |
|
|
226 |
|
|
|
227 |
def get_diff_options(req): |
|
|
228 |
options_data = {} |
|
|
229 |
data = {'options': options_data} |
|
|
230 |
|
|
|
231 |
def get_bool_option(name, default=0): |
|
|
232 |
pref = int(req.session.get('diff_' + name, default)) |
|
|
233 |
arg = int(req.args.has_key(name)) |
|
|
234 |
if req.args.has_key('update') and arg != pref: |
|
|
235 |
req.session['diff_' + name] = arg |
|
|
236 |
else: |
|
|
237 |
arg = pref |
|
|
238 |
return arg |
|
|
239 |
|
|
|
240 |
pref = req.session.get('diff_style', 'inline') |
|
|
241 |
style = req.args.get('style', pref) |
|
|
242 |
if req.args.has_key('update') and style != pref: |
|
|
243 |
req.session['diff_style'] = style |
|
|
244 |
data['style'] = style |
|
|
245 |
|
|
|
246 |
pref = int(req.session.get('diff_contextlines', 2)) |
|
|
247 |
try: |
|
|
248 |
arg = int(req.args.get('contextlines', pref)) |
|
|
249 |
except ValueError: |
|
|
250 |
arg = -1 |
|
|
251 |
if req.args.has_key('update') and arg != pref: |
|
|
252 |
req.session['diff_contextlines'] = arg |
|
|
253 |
options = ['-U%d' % arg] |
|
|
254 |
options_data['contextlines'] = arg |
|
|
255 |
|
|
|
256 |
arg = get_bool_option('ignoreblanklines') |
|
|
257 |
if arg: |
|
|
258 |
options.append('-B') |
|
|
259 |
options_data['ignoreblanklines'] = arg |
|
|
260 |
|
|
|
261 |
arg = get_bool_option('ignorecase') |
|
|
262 |
if arg: |
|
|
263 |
options.append('-i') |
|
|
264 |
options_data['ignorecase'] = arg |
|
|
265 |
|
|
|
266 |
arg = get_bool_option('ignorewhitespace') |
|
|
267 |
if arg: |
|
|
268 |
options.append('-b') |
|
|
269 |
options_data['ignorewhitespace'] = arg |
|
|
270 |
|
|
|
271 |
return (style, options, data) |