differ.py
216 lines
| 7.5 KiB
| text/x-python
|
PythonLexer
r130 | # -*- coding: utf-8 -*- | |||
# original copyright: 2007-2008 by Armin Ronacher | ||||
# licensed under the BSD license. | ||||
import re, difflib | ||||
def render_udiff(udiff, differ='udiff'): | ||||
"""Renders the udiff into multiple chunks of nice looking tables. | ||||
The return value is a list of those tables. | ||||
""" | ||||
return DiffProcessor(udiff, differ).prepare() | ||||
class DiffProcessor(object): | ||||
"""Give it a unified diff and it returns a list of the files that were | ||||
mentioned in the diff together with a dict of meta information that | ||||
can be used to render it in a HTML template. | ||||
""" | ||||
_chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)') | ||||
def __init__(self, udiff, differ): | ||||
""" | ||||
:param udiff: a text in udiff format | ||||
""" | ||||
if isinstance(udiff, basestring): | ||||
udiff = udiff.splitlines(1) | ||||
self.lines = map(self.escaper, udiff) | ||||
# Select a differ. | ||||
if differ == 'difflib': | ||||
self.differ = self._highlight_line_difflib | ||||
else: | ||||
self.differ = self._highlight_line_udiff | ||||
def escaper(self, string): | ||||
return string.replace('<', '<').replace('>', '>') | ||||
def _extract_rev(self, line1, line2): | ||||
"""Extract the filename and revision hint from a line.""" | ||||
try: | ||||
if line1.startswith('--- ') and line2.startswith('+++ '): | ||||
r152 | l1 = line1[4:].split(None, 1) | |||
old_filename = l1[0] if len(l1) >= 1 else None | ||||
old_rev = l1[1] if len(l1) == 2 else 'old' | ||||
l2 = line1[4:].split(None, 1) | ||||
new_filename = l2[0] if len(l2) >= 1 else None | ||||
new_rev = l2[1] if len(l2) == 2 else 'new' | ||||
return old_filename, new_rev, old_rev | ||||
r130 | except (ValueError, IndexError): | |||
pass | ||||
r152 | ||||
r130 | return None, None, None | |||
def _highlight_line_difflib(self, line, next): | ||||
"""Highlight inline changes in both lines.""" | ||||
if line['action'] == 'del': | ||||
old, new = line, next | ||||
else: | ||||
old, new = next, line | ||||
oldwords = re.split(r'(\W)', old['line']) | ||||
newwords = re.split(r'(\W)', new['line']) | ||||
sequence = difflib.SequenceMatcher(None, oldwords, newwords) | ||||
oldfragments, newfragments = [], [] | ||||
for tag, i1, i2, j1, j2 in sequence.get_opcodes(): | ||||
oldfrag = ''.join(oldwords[i1:i2]) | ||||
newfrag = ''.join(newwords[j1:j2]) | ||||
if tag != 'equal': | ||||
if oldfrag: | ||||
oldfrag = '<del>%s</del>' % oldfrag | ||||
if newfrag: | ||||
newfrag = '<ins>%s</ins>' % newfrag | ||||
oldfragments.append(oldfrag) | ||||
newfragments.append(newfrag) | ||||
old['line'] = "".join(oldfragments) | ||||
new['line'] = "".join(newfragments) | ||||
def _highlight_line_udiff(self, line, next): | ||||
"""Highlight inline changes in both lines.""" | ||||
start = 0 | ||||
limit = min(len(line['line']), len(next['line'])) | ||||
while start < limit and line['line'][start] == next['line'][start]: | ||||
start += 1 | ||||
end = -1 | ||||
limit -= start | ||||
while - end <= limit and line['line'][end] == next['line'][end]: | ||||
end -= 1 | ||||
end += 1 | ||||
if start or end: | ||||
def do(l): | ||||
last = end + len(l['line']) | ||||
if l['action'] == 'add': | ||||
tag = 'ins' | ||||
else: | ||||
tag = 'del' | ||||
l['line'] = '%s<%s>%s</%s>%s' % ( | ||||
l['line'][:start], | ||||
tag, | ||||
l['line'][start:last], | ||||
tag, | ||||
l['line'][last:] | ||||
) | ||||
do(line) | ||||
do(next) | ||||
def _parse_udiff(self): | ||||
"""Parse the diff an return data for the template.""" | ||||
lineiter = iter(self.lines) | ||||
files = [] | ||||
try: | ||||
line = lineiter.next() | ||||
while 1: | ||||
# continue until we found the old file | ||||
if not line.startswith('--- '): | ||||
line = lineiter.next() | ||||
continue | ||||
chunks = [] | ||||
filename, old_rev, new_rev = \ | ||||
self._extract_rev(line, lineiter.next()) | ||||
files.append({ | ||||
'filename': filename, | ||||
'old_revision': old_rev, | ||||
'new_revision': new_rev, | ||||
'chunks': chunks | ||||
}) | ||||
line = lineiter.next() | ||||
while line: | ||||
match = self._chunk_re.match(line) | ||||
if not match: | ||||
break | ||||
lines = [] | ||||
chunks.append(lines) | ||||
old_line, old_end, new_line, new_end = \ | ||||
[int(x or 1) for x in match.groups()[:-1]] | ||||
old_line -= 1 | ||||
new_line -= 1 | ||||
context = match.groups()[-1] | ||||
old_end += old_line | ||||
new_end += new_line | ||||
if context: | ||||
lines.append({ | ||||
'old_lineno': None, | ||||
'new_lineno': None, | ||||
'action': 'context', | ||||
'line': line, | ||||
}) | ||||
line = lineiter.next() | ||||
while old_line < old_end or new_line < new_end: | ||||
if line: | ||||
command, line = line[0], line[1:] | ||||
else: | ||||
command = ' ' | ||||
affects_old = affects_new = False | ||||
# ignore those if we don't expect them | ||||
if command in '#@': | ||||
continue | ||||
elif command == '+': | ||||
affects_new = True | ||||
action = 'add' | ||||
elif command == '-': | ||||
affects_old = True | ||||
action = 'del' | ||||
else: | ||||
affects_old = affects_new = True | ||||
action = 'unmod' | ||||
old_line += affects_old | ||||
new_line += affects_new | ||||
lines.append({ | ||||
'old_lineno': affects_old and old_line or '', | ||||
'new_lineno': affects_new and new_line or '', | ||||
'action': action, | ||||
'line': line | ||||
}) | ||||
line = lineiter.next() | ||||
except StopIteration: | ||||
pass | ||||
# highlight inline changes | ||||
for file in files: | ||||
for chunk in chunks: | ||||
lineiter = iter(chunk) | ||||
first = True | ||||
try: | ||||
while 1: | ||||
line = lineiter.next() | ||||
if line['action'] != 'unmod': | ||||
nextline = lineiter.next() | ||||
if nextline['action'] == 'unmod' or \ | ||||
nextline['action'] == line['action']: | ||||
continue | ||||
self.differ(line, nextline) | ||||
except StopIteration: | ||||
pass | ||||
return files | ||||
def prepare(self): | ||||
"""Prepare the passed udiff for HTML rendering.""" | ||||
return self._parse_udiff() | ||||