diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py --- a/mercurial/cmdutil.py +++ b/mercurial/cmdutil.py @@ -1518,7 +1518,7 @@ def diffordiffstat(ui, repo, diffopts, n width = 80 if not ui.plain(): width = ui.termwidth() - chunks = patch.diff(repo, node1, node2, match, changes, diffopts, + chunks = patch.diff(repo, node1, node2, match, changes, opts=diffopts, prefix=prefix, relroot=relroot, hunksfilterfn=hunksfilterfn) for chunk, label in patch.diffstatui(util.iterlines(chunks), @@ -1526,7 +1526,7 @@ def diffordiffstat(ui, repo, diffopts, n write(chunk, label=label) else: for chunk, label in patch.diffui(repo, node1, node2, match, - changes, diffopts, prefix=prefix, + changes, opts=diffopts, prefix=prefix, relroot=relroot, hunksfilterfn=hunksfilterfn): write(chunk, label=label) diff --git a/mercurial/color.py b/mercurial/color.py --- a/mercurial/color.py +++ b/mercurial/color.py @@ -87,12 +87,14 @@ except ImportError: 'branches.inactive': 'none', 'diff.changed': 'white', 'diff.deleted': 'red', + 'diff.deleted.highlight': 'red bold underline', 'diff.diffline': 'bold', 'diff.extended': 'cyan bold', 'diff.file_a': 'red bold', 'diff.file_b': 'green bold', 'diff.hunk': 'magenta', 'diff.inserted': 'green', + 'diff.inserted.highlight': 'green bold underline', 'diff.tab': '', 'diff.trailingwhitespace': 'bold red_background', 'changeset.public': '', diff --git a/mercurial/configitems.py b/mercurial/configitems.py --- a/mercurial/configitems.py +++ b/mercurial/configitems.py @@ -481,6 +481,9 @@ coreconfigitem('experimental', 'evolutio coreconfigitem('experimental', 'evolution.track-operation', default=True, ) +coreconfigitem('experimental', 'worddiff', + default=False, +) coreconfigitem('experimental', 'maxdeltachainspan', default=-1, ) diff --git a/mercurial/mdiff.py b/mercurial/mdiff.py --- a/mercurial/mdiff.py +++ b/mercurial/mdiff.py @@ -67,6 +67,7 @@ class diffopts(object): 'ignoreblanklines': False, 'upgrade': False, 'showsimilarity': False, + 'worddiff': False, } def __init__(self, **opts): diff --git a/mercurial/patch.py b/mercurial/patch.py --- a/mercurial/patch.py +++ b/mercurial/patch.py @@ -10,6 +10,7 @@ from __future__ import absolute_import, import collections import copy +import difflib import email import errno import hashlib @@ -2252,6 +2253,7 @@ def difffeatureopts(ui, opts=None, untru 'showfunc': get('show_function', 'showfunc'), 'context': get('unified', getter=ui.config), } + buildopts['worddiff'] = ui.configbool('experimental', 'worddiff') if git: buildopts['git'] = get('git') @@ -2463,6 +2465,9 @@ def diffhunks(repo, node1=None, node2=No def difflabel(func, *args, **kw): '''yields 2-tuples of (output, label) based on the output of func()''' + inlinecolor = False + if kw.get('opts'): + inlinecolor = kw['opts'].worddiff headprefixes = [('diff', 'diff.diffline'), ('copy', 'diff.extended'), ('rename', 'diff.extended'), @@ -2479,6 +2484,9 @@ def difflabel(func, *args, **kw): head = False for chunk in func(*args, **kw): lines = chunk.split('\n') + matches = {} + if inlinecolor: + matches = _findmatches(lines) for i, line in enumerate(lines): if i != 0: yield ('\n', '') @@ -2506,7 +2514,14 @@ def difflabel(func, *args, **kw): if '\t' == token[0]: yield (token, 'diff.tab') else: - yield (token, label) + if i in matches: + for l, t in _inlinediff( + lines[i].rstrip(), + lines[matches[i]].rstrip(), + label): + yield (t, l) + else: + yield (token, label) else: yield (stripline, label) break @@ -2515,6 +2530,70 @@ def difflabel(func, *args, **kw): if line != stripline: yield (line[len(stripline):], 'diff.trailingwhitespace') +def _findmatches(slist): + '''Look for insertion matches to deletion and returns a dict of + correspondences. + ''' + lastmatch = 0 + matches = {} + for i, line in enumerate(slist): + if line == '': + continue + if line[0] == '-': + lastmatch = max(lastmatch, i) + newgroup = False + for j, newline in enumerate(slist[lastmatch + 1:]): + if newline == '': + continue + if newline[0] == '-' and newgroup: # too far, no match + break + if newline[0] == '+': # potential match + newgroup = True + sim = difflib.SequenceMatcher(None, line, newline).ratio() + if sim > 0.7: + lastmatch = lastmatch + 1 + j + matches[i] = lastmatch + matches[lastmatch] = i + break + return matches + +def _inlinediff(s1, s2, operation): + '''Perform string diff to highlight specific changes.''' + operation_skip = '+?' if operation == 'diff.deleted' else '-?' + if operation == 'diff.deleted': + s2, s1 = s1, s2 + + buff = [] + # we never want to higlight the leading +- + if operation == 'diff.deleted' and s2.startswith('-'): + label = operation + token = '-' + s2 = s2[1:] + s1 = s1[1:] + elif operation == 'diff.inserted' and s1.startswith('+'): + label = operation + token = '+' + s2 = s2[1:] + s1 = s1[1:] + + s = difflib.ndiff(re.split(br'(\W)', s2), re.split(br'(\W)', s1)) + for part in s: + if part[0] in operation_skip: + continue + l = operation + '.highlight' + if part[0] in ' ': + l = operation + if l == label: # contiguous token with same label + token += part[2:] + continue + else: + buff.append((label, token)) + label = l + token = part[2:] + buff.append((label, token)) + + return buff + def diffui(*args, **kw): '''like diff(), but yields 2-tuples of (output, label) for ui.write()''' return difflabel(diff, *args, **kw) diff --git a/tests/test-diff-color.t b/tests/test-diff-color.t --- a/tests/test-diff-color.t +++ b/tests/test-diff-color.t @@ -259,3 +259,95 @@ test tabs \x1b[0;32m+\x1b[0m\x1b[0;1;35m \x1b[0m\x1b[0;32mall\x1b[0m\x1b[0;1;35m \x1b[0m\x1b[0;32mtabs\x1b[0m\x1b[0;1;41m \x1b[0m (esc) $ cd .. + +test inline color diff + + $ hg init inline + $ cd inline + $ cat > file1 << EOF + > this is the first line + > this is the second line + > third line starts with space + > + starts with a plus sign + > + > this line won't change + > + > two lines are going to + > be changed into three! + > + > three of those lines will + > collapse onto one + > (to see if it works) + > EOF + $ hg add file1 + $ hg ci -m 'commit' + $ cat > file1 << EOF + > that is the first paragraph + > this is the second line + > third line starts with space + > - starts with a minus sign + > + > this line won't change + > + > two lines are going to + > (entirely magically, + > assuming this works) + > be changed into four! + > + > three of those lines have + > collapsed onto one + > EOF + $ hg diff --config experimental.worddiff=False --color=debug + [diff.diffline|diff --git a/file1 b/file1] + [diff.file_a|--- a/file1] + [diff.file_b|+++ b/file1] + [diff.hunk|@@ -1,13 +1,14 @@] + [diff.deleted|-this is the first line] + [diff.deleted|-this is the second line] + [diff.deleted|- third line starts with space] + [diff.deleted|-+ starts with a plus sign] + [diff.inserted|+that is the first paragraph] + [diff.inserted|+ this is the second line] + [diff.inserted|+third line starts with space] + [diff.inserted|+- starts with a minus sign] + + this line won't change + + two lines are going to + [diff.deleted|-be changed into three!] + [diff.inserted|+(entirely magically,] + [diff.inserted|+ assuming this works)] + [diff.inserted|+be changed into four!] + + [diff.deleted|-three of those lines will] + [diff.deleted|-collapse onto one] + [diff.deleted|-(to see if it works)] + [diff.inserted|+three of those lines have] + [diff.inserted|+collapsed onto one] + $ hg diff --config experimental.worddiff=True --color=debug + [diff.diffline|diff --git a/file1 b/file1] + [diff.file_a|--- a/file1] + [diff.file_b|+++ b/file1] + [diff.hunk|@@ -1,13 +1,14 @@] + [diff.deleted|-this is the ][diff.deleted.highlight|first][diff.deleted| line] + [diff.deleted|-this is the second line] + [diff.deleted|-][diff.deleted.highlight| ][diff.deleted|third line starts with space] + [diff.deleted|-][diff.deleted.highlight|+][diff.deleted| starts with a ][diff.deleted.highlight|plus][diff.deleted| sign] + [diff.inserted|+that is the first paragraph] + [diff.inserted|+][diff.inserted.highlight| ][diff.inserted|this is the ][diff.inserted.highlight|second][diff.inserted| line] + [diff.inserted|+third line starts with space] + [diff.inserted|+][diff.inserted.highlight|-][diff.inserted| starts with a ][diff.inserted.highlight|minus][diff.inserted| sign] + + this line won't change + + two lines are going to + [diff.deleted|-be changed into ][diff.deleted.highlight|three][diff.deleted|!] + [diff.inserted|+(entirely magically,] + [diff.inserted|+ assuming this works)] + [diff.inserted|+be changed into ][diff.inserted.highlight|four][diff.inserted|!] + + [diff.deleted|-three of those lines ][diff.deleted.highlight|will] + [diff.deleted|-][diff.deleted.highlight|collapse][diff.deleted| onto one] + [diff.deleted|-(to see if it works)] + [diff.inserted|+three of those lines ][diff.inserted.highlight|have] + [diff.inserted|+][diff.inserted.highlight|collapsed][diff.inserted| onto one]