##// END OF EJS Templates
dependencies: bumped test dependencies:...
dependencies: bumped test dependencies: - pytest to 3.2.5 - pytest-runner to 3.0.0

File last commit:

r119:36ec6f9e default
r325:3c87307b default
Show More
svn_diff.py
209 lines | 8.0 KiB | text/x-python | PythonLexer
# -*- coding: utf-8 -*-
#
# Copyright (C) 2004-2009 Edgewall Software
# Copyright (C) 2004-2006 Christopher Lenz <cmlenz@gmx.de>
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://trac.edgewall.org/wiki/TracLicense.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at http://trac.edgewall.org/log/.
#
# Author: Christopher Lenz <cmlenz@gmx.de>
import difflib
def get_filtered_hunks(fromlines, tolines, context=None,
ignore_blank_lines=False, ignore_case=False,
ignore_space_changes=False):
"""Retrieve differences in the form of `difflib.SequenceMatcher`
opcodes, grouped according to the ``context`` and ``ignore_*``
parameters.
:param fromlines: list of lines corresponding to the old content
:param tolines: list of lines corresponding to the new content
:param ignore_blank_lines: differences about empty lines only are ignored
:param ignore_case: upper case / lower case only differences are ignored
:param ignore_space_changes: differences in amount of spaces are ignored
:param context: the number of "equal" lines kept for representing
the context of the change
:return: generator of grouped `difflib.SequenceMatcher` opcodes
If none of the ``ignore_*`` parameters is `True`, there's nothing
to filter out the results will come straight from the
SequenceMatcher.
"""
hunks = get_hunks(fromlines, tolines, context)
if ignore_space_changes or ignore_case or ignore_blank_lines:
hunks = filter_ignorable_lines(hunks, fromlines, tolines, context,
ignore_blank_lines, ignore_case,
ignore_space_changes)
return hunks
def get_hunks(fromlines, tolines, context=None):
"""Generator yielding grouped opcodes describing differences .
See `get_filtered_hunks` for the parameter descriptions.
"""
matcher = difflib.SequenceMatcher(None, fromlines, tolines)
if context is None:
return (hunk for hunk in [matcher.get_opcodes()])
else:
return matcher.get_grouped_opcodes(context)
def filter_ignorable_lines(hunks, fromlines, tolines, context,
ignore_blank_lines, ignore_case,
ignore_space_changes):
"""Detect line changes that should be ignored and emits them as
tagged as "equal", possibly joined with the preceding and/or
following "equal" block.
See `get_filtered_hunks` for the parameter descriptions.
"""
def is_ignorable(tag, fromlines, tolines):
if tag == 'delete' and ignore_blank_lines:
if ''.join(fromlines) == '':
return True
elif tag == 'insert' and ignore_blank_lines:
if ''.join(tolines) == '':
return True
elif tag == 'replace' and (ignore_case or ignore_space_changes):
if len(fromlines) != len(tolines):
return False
def f(input_str):
if ignore_case:
input_str = input_str.lower()
if ignore_space_changes:
input_str = ' '.join(input_str.split())
return input_str
for i in range(len(fromlines)):
if f(fromlines[i]) != f(tolines[i]):
return False
return True
hunks = list(hunks)
opcodes = []
ignored_lines = False
prev = None
for hunk in hunks:
for tag, i1, i2, j1, j2 in hunk:
if tag == 'equal':
if prev:
prev = (tag, prev[1], i2, prev[3], j2)
else:
prev = (tag, i1, i2, j1, j2)
else:
if is_ignorable(tag, fromlines[i1:i2], tolines[j1:j2]):
ignored_lines = True
if prev:
prev = 'equal', prev[1], i2, prev[3], j2
else:
prev = 'equal', i1, i2, j1, j2
continue
if prev:
opcodes.append(prev)
opcodes.append((tag, i1, i2, j1, j2))
prev = None
if prev:
opcodes.append(prev)
if ignored_lines:
if context is None:
yield opcodes
else:
# we leave at most n lines with the tag 'equal' before and after
# every change
n = context
nn = n + n
group = []
def all_equal():
all(op[0] == 'equal' for op in group)
for idx, (tag, i1, i2, j1, j2) in enumerate(opcodes):
if idx == 0 and tag == 'equal': # Fixup leading unchanged block
i1, j1 = max(i1, i2 - n), max(j1, j2 - n)
elif tag == 'equal' and i2 - i1 > nn:
group.append((tag, i1, min(i2, i1 + n), j1,
min(j2, j1 + n)))
if not all_equal():
yield group
group = []
i1, j1 = max(i1, i2 - n), max(j1, j2 - n)
group.append((tag, i1, i2, j1, j2))
if group and not (len(group) == 1 and group[0][0] == 'equal'):
if group[-1][0] == 'equal': # Fixup trailing unchanged block
tag, i1, i2, j1, j2 = group[-1]
group[-1] = tag, i1, min(i2, i1 + n), j1, min(j2, j1 + n)
if not all_equal():
yield group
else:
for hunk in hunks:
yield hunk
NO_NEWLINE_AT_END = '\\ No newline at end of file'
def unified_diff(fromlines, tolines, context=None, ignore_blank_lines=0,
ignore_case=0, ignore_space_changes=0, lineterm='\n'):
"""
Generator producing lines corresponding to a textual diff.
See `get_filtered_hunks` for the parameter descriptions.
"""
# TODO: johbo: Check if this can be nicely integrated into the matching
if ignore_space_changes:
fromlines = [l.strip() for l in fromlines]
tolines = [l.strip() for l in tolines]
for group in get_filtered_hunks(fromlines, tolines, context,
ignore_blank_lines, ignore_case,
ignore_space_changes):
i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
if i1 == 0 and i2 == 0:
i1, i2 = -1, -1 # support for Add changes
if j1 == 0 and j2 == 0:
j1, j2 = -1, -1 # support for Delete changes
yield '@@ -%s +%s @@%s' % (
_hunk_range(i1 + 1, i2 - i1),
_hunk_range(j1 + 1, j2 - j1),
lineterm)
for tag, i1, i2, j1, j2 in group:
if tag == 'equal':
for line in fromlines[i1:i2]:
if not line.endswith(lineterm):
yield ' ' + line + lineterm
yield NO_NEWLINE_AT_END + lineterm
else:
yield ' ' + line
else:
if tag in ('replace', 'delete'):
for line in fromlines[i1:i2]:
if not line.endswith(lineterm):
yield '-' + line + lineterm
yield NO_NEWLINE_AT_END + lineterm
else:
yield '-' + line
if tag in ('replace', 'insert'):
for line in tolines[j1:j2]:
if not line.endswith(lineterm):
yield '+' + line + lineterm
yield NO_NEWLINE_AT_END + lineterm
else:
yield '+' + line
def _hunk_range(start, length):
if length != 1:
return '%d,%d' % (start, length)
else:
return '%d' % (start, )