226 lines
7.4 KiB
Python
226 lines
7.4 KiB
Python
"""
|
|
lodgeit.lib.diff
|
|
~~~~~~~~~~~~~~~~
|
|
|
|
Render a nice diff between two things.
|
|
|
|
:copyright: 2007 by Armin Ronacher.
|
|
:license: BSD
|
|
"""
|
|
import re
|
|
import time
|
|
|
|
try:
|
|
from html import escape
|
|
except ImportError:
|
|
from cgi import escape
|
|
|
|
try:
|
|
all
|
|
except NameError:
|
|
def all(iterable):
|
|
for element in iterable:
|
|
if not element:
|
|
return False
|
|
return True
|
|
|
|
|
|
def prepare_udiff(udiff):
|
|
"""Prepare an udiff for a template."""
|
|
return DiffRenderer(udiff).prepare()
|
|
|
|
|
|
class DiffRenderer(object):
|
|
"""Give it a unified diff and it renders you a beautiful
|
|
html diff :-)
|
|
"""
|
|
_chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
|
|
|
|
def __init__(self, udiff):
|
|
""":param udiff: a text in udiff format"""
|
|
self.lines = [escape(line) for line in udiff.splitlines()]
|
|
|
|
def _extract_rev(self, line1, line2):
|
|
def _extract(line):
|
|
parts = line.split(None, 1)
|
|
return parts[0], (len(parts) == 2 and parts[1] or None)
|
|
try:
|
|
if line1.startswith('--- ') and line2.startswith('+++ '):
|
|
return _extract(line1[4:]), _extract(line2[4:])
|
|
except (ValueError, IndexError):
|
|
pass
|
|
return (None, None), (None, None)
|
|
|
|
def _highlight_line(self, line, nextline):
|
|
"""Highlight inline changes in both lines."""
|
|
start = 0
|
|
limit = min(len(line['line']), len(nextline['line']))
|
|
while start < limit and line['line'][start] == nextline['line'][start]:
|
|
start += 1
|
|
end = -1
|
|
limit -= start
|
|
while -end <= limit and line['line'][end] == nextline['line'][end]:
|
|
end -= 1
|
|
end += 1
|
|
if start or end:
|
|
def do(iterline):
|
|
last = end + len(iterline['line'])
|
|
if iterline['action'] == 'add':
|
|
tag = 'ins'
|
|
else:
|
|
tag = 'del'
|
|
iterline['line'] = u'%s<%s>%s</%s>%s' % (
|
|
iterline['line'][:start],
|
|
tag,
|
|
iterline['line'][start:last],
|
|
tag,
|
|
iterline['line'][last:]
|
|
)
|
|
do(line)
|
|
do(nextline)
|
|
|
|
def _parse_info(self):
|
|
"""Look for custom information preceding the diff."""
|
|
nlines = len(self.lines)
|
|
if not nlines:
|
|
return
|
|
firstline = self.lines[0]
|
|
info = []
|
|
|
|
# look for Hg export changeset
|
|
if firstline.startswith('# HG changeset patch'):
|
|
info.append(('Type', 'HG export changeset'))
|
|
i = 0
|
|
line = firstline
|
|
while line.startswith('#'):
|
|
if line.startswith('# User'):
|
|
info.append(('User', line[7:].strip()))
|
|
elif line.startswith('# Date'):
|
|
try:
|
|
t, tz = map(int, line[7:].split())
|
|
info.append(('Date', time.strftime(
|
|
'%b %d, %Y %H:%M:%S', time.gmtime(float(t) - tz))))
|
|
except Exception:
|
|
pass
|
|
elif line.startswith('# Branch'):
|
|
info.append(('Branch', line[9:].strip()))
|
|
i += 1
|
|
if i == nlines:
|
|
return info
|
|
line = self.lines[i]
|
|
commitmsg = ''
|
|
while not line.startswith('diff'):
|
|
commitmsg += line + '\n'
|
|
i += 1
|
|
if i == nlines:
|
|
return info
|
|
line = self.lines[i]
|
|
info.append(('Commit message', '\n' + commitmsg.strip()))
|
|
self.lines = self.lines[i:]
|
|
return info
|
|
|
|
def _parse_udiff(self):
|
|
"""Parse the diff an return data for the template."""
|
|
info = self._parse_info()
|
|
|
|
in_header = True
|
|
header = []
|
|
lineiter = iter(self.lines)
|
|
files = []
|
|
try:
|
|
line = next(lineiter)
|
|
while 1:
|
|
# continue until we found the old file
|
|
if not line.startswith('--- '):
|
|
if in_header:
|
|
header.append(line)
|
|
line = next(lineiter)
|
|
continue
|
|
|
|
if header and all(x.strip() for x in header):
|
|
files.append({'is_header': True, 'lines': header})
|
|
header = []
|
|
|
|
in_header = False
|
|
chunks = []
|
|
old, new = self._extract_rev(line, next(lineiter))
|
|
files.append({
|
|
'is_header': False,
|
|
'old_filename': old[0],
|
|
'old_revision': old[1],
|
|
'new_filename': new[0],
|
|
'new_revision': new[1],
|
|
'chunks': chunks
|
|
})
|
|
|
|
line = next(lineiter)
|
|
while line:
|
|
match = self._chunk_re.match(line)
|
|
if not match:
|
|
in_header = True
|
|
break
|
|
|
|
lines = []
|
|
chunks.append(lines)
|
|
|
|
old_line, old_end, new_line, new_end = \
|
|
[int(x or 1) for x in match.groups()]
|
|
old_line -= 1
|
|
new_line -= 1
|
|
old_end += old_line
|
|
new_end += new_line
|
|
line = next(lineiter)
|
|
|
|
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
|
|
|
|
if 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 u'',
|
|
'new_lineno': affects_new and new_line or u'',
|
|
'action': action,
|
|
'line': line
|
|
})
|
|
line = next(lineiter)
|
|
|
|
except StopIteration:
|
|
pass
|
|
|
|
# highlight inline changes
|
|
for file in files:
|
|
if file['is_header']:
|
|
continue
|
|
for chunk in file['chunks']:
|
|
lineiter = iter(chunk)
|
|
try:
|
|
while True:
|
|
line = next(lineiter)
|
|
if line['action'] != 'unmod':
|
|
nextline = next(lineiter)
|
|
if nextline['action'] == 'unmod' or \
|
|
nextline['action'] == line['action']:
|
|
continue
|
|
self._highlight_line(line, nextline)
|
|
except StopIteration:
|
|
pass
|
|
|
|
return files, info
|
|
|
|
def prepare(self):
|
|
return self._parse_udiff()
|