commit 038b95dcaea8d5f92d552e1632723a7958b602af Author: jomae Date: Fri May 9 16:55:59 2014 +0900 1.0.2dev: improved diff with "ignore whitespace changes" diff --git a/trac/versioncontrol/diff.py b/trac/versioncontrol/diff.py index 8475b05..b6637f3 100644 --- a/trac/versioncontrol/diff.py +++ b/trac/versioncontrol/diff.py @@ -94,50 +94,61 @@ def filter_ignorable_lines(hunks, fromlines, tolines, context, 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(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 + + def filter_ignores(opcodes): + ignored = False + filtered = [] + for tag, i1, i2, j1, j2 in opcodes: + if normalize and tag == 'replace': + norm_fromlines = map(normalize, fromlines[i1:i2]) + norm_tolines = map(normalize, tolines[j1:j2]) + matcher = difflib.SequenceMatcher(None, norm_fromlines, + norm_tolines) + for tag, norm_i1, norm_i2, norm_j1, norm_j2 in \ + matcher.get_opcodes(): + filtered.append((tag, i1 + norm_i1, i1 + norm_i2, + j1 + norm_j1, j1 + norm_j2)) + if tag != 'replace': + ignored = True + continue + if ignore_blank_lines and tag == 'delete': + if all(fromlines[n] == '' for n in xrange(i1, i2)): + ignored = True + tag = 'equal' + if ignore_blank_lines and tag == 'insert': + if all(tolines[n] == '' for n in xrange(j1, j2)): + ignored = True + tag = 'equal' + filtered.append((tag, i1, i2, j1, j2)) + return ignored, filtered 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) + if ignore_blank_lines or ignore_case or ignore_space_changes: + ignored, hunk = filter_ignores(hunk) + if ignored: + ignored_lines = True + for curr in hunk: + if prev is None: + prev = curr + elif prev[0] == curr[0]: + prev = (prev[0], prev[1], curr[2], prev[3], curr[4]) 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 + opcodes.append(prev) + prev = curr if prev: opcodes.append(prev) diff --git a/trac/versioncontrol/tests/diff.py b/trac/versioncontrol/tests/diff.py index 68e6e47..b168f93 100644 --- a/trac/versioncontrol/tests/diff.py +++ b/trac/versioncontrol/tests/diff.py @@ -85,6 +85,40 @@ class DiffTestCase(unittest.TestCase): self.assertEqual(('equal', 0, 2, 0, 2), opcodes.next()) self.assertRaises(StopIteration, 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 = 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 = 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 = get_opcodes(['A', 'B b'], ['A', 'B B'], ignore_case=0) self.assertEqual(('equal', 0, 1, 0, 1), opcodes.next())