diff --git a/trac/versioncontrol/diff.py b/trac/versioncontrol/diff.py index 8034461..27fd525 100644 --- a/trac/versioncontrol/diff.py +++ b/trac/versioncontrol/diff.py @@ -14,12 +14,13 @@ # # Author: Christopher Lenz +import re +from difflib import SequenceMatcher + +from trac.util.compat import all from trac.util.html import escape, Markup from trac.util.text import expandtabs -from difflib import SequenceMatcher -import re - __all__ = ['get_diff_options', 'hdf_diff', 'diff_blocks', 'unified_diff'] @@ -50,47 +51,49 @@ def _get_opcodes(fromlines, tolines, ignore_blank_lines=False, 'equal' block. """ - 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(str): - if ignore_case: - str = str.lower() - if ignore_space_changes: - str = ' '.join(str.split()) - return str - for i in range(len(fromlines)): - if f(fromlines[i]) != f(tolines[i]): - return False - return True + if ignore_case or ignore_space_changes: + def normalize(value): + if ignore_case: + value = value.lower() + if ignore_space_changes: + value = ' '.join(value.split()) + return value + else: + normalize = None - matcher = SequenceMatcher(None, fromlines, tolines) - previous = None - for tag, i1, i2, j1, j2 in matcher.get_opcodes(): - if tag == 'equal': - if previous: - previous = (tag, previous[1], i2, previous[3], j2) - else: - previous = (tag, i1, i2, j1, j2) - else: - if is_ignorable(tag, fromlines[i1:i2], tolines[j1:j2]): - if previous: - previous = 'equal', previous[1], i2, previous[3], j2 - else: - previous = 'equal', i1, i2, j1, j2 + def filter_ignores(opcodes): + for tag, i1, i2, j1, j2 in opcodes: + if normalize and tag == 'replace': + matcher = SequenceMatcher(None, + map(normalize, fromlines[i1:i2]), + map(normalize, tolines[j1:j2])) + for tag, norm_i1, norm_i2, norm_j1, norm_j2 in \ + matcher.get_opcodes(): + yield tag, i1 + norm_i1, i1 + norm_i2, \ + j1 + norm_j1, j1 + norm_j2 continue - if previous: - yield previous + if ignore_blank_lines and tag == 'delete': + if all(fromlines[n] == '' for n in xrange(i1, i2)): + tag = 'equal' + if ignore_blank_lines and tag == 'insert': + if all(tolines[n] == '' for n in xrange(j1, j2)): + tag = 'equal' yield tag, i1, i2, j1, j2 - previous = None + matcher = SequenceMatcher(None, fromlines, tolines) + opcodes = matcher.get_opcodes() + if ignore_blank_lines or ignore_case or ignore_space_changes: + opcodes = filter_ignores(opcodes) + previous = None + for current in opcodes: + if previous is None: + previous = current + elif previous[0] == current[0]: + previous = (previous[0], previous[1], current[2], + previous[3], current[4]) + else: + yield previous + previous = current if previous: yield previous diff --git a/trac/versioncontrol/tests/diff.py b/trac/versioncontrol/tests/diff.py index cc0f02f..2977da6 100644 --- a/trac/versioncontrol/tests/diff.py +++ b/trac/versioncontrol/tests/diff.py @@ -56,6 +56,40 @@ class DiffTestCase(unittest.TestCase): ignore_space_changes=1) self.assertEqual(('equal', 0, 2, 0, 2), opcodes.next()) + def test_space_changes_2(self): + left = """\ +try: + try: + func() + commit() + except: + rollback() +finally: + cleanup() +""" + left = left.splitlines() + right = """\ +try: + func() + commit() +except: + rollback() +finally: + cleanup() +""" + right = right.splitlines() + opcodes = diff._get_opcodes(left, right, ignore_space_changes=0) + self.assertEqual(('equal', 0, 1, 0, 1), opcodes.next()) + self.assertEqual(('replace', 1, 6, 1, 5), opcodes.next()) + self.assertEqual(('equal', 6, 8, 5, 7), opcodes.next()) + self.assertRaises(StopIteration, opcodes.next) + + opcodes = diff._get_opcodes(left, right, ignore_space_changes=1) + self.assertEqual(('equal', 0, 1, 0, 1), opcodes.next()) + self.assertEqual(('delete', 1, 2, 1, 1), opcodes.next()) + self.assertEqual(('equal', 2, 8, 1, 7), opcodes.next()) + self.assertRaises(StopIteration, opcodes.next) + def test_case_changes(self): opcodes = diff._get_opcodes(['A', 'B b'], ['A', 'B B'], ignore_case=0)