# HG changeset patch # User Matthieu Laneuville <matthieu.laneuvi...@octobus.net> # Date 1508944418 -32400 # Thu Oct 26 00:13:38 2017 +0900 # Node ID bab248bd389941e034b002aed0e29b211b6995cf # Parent f56a30b844aa91eecd4e31b1fbc291d9d24973b3 # EXP-Topic inline-diff cmdutil: add within-line color diff capacity
The `diff' command usually writes deletion in red and insertions in green. This patch adds within-line colors, to highlight which part of the lines differ. Lines to compare are decided based on their similarity ratio, as computed by difflib SequenceMatcher, with an arbitrary threshold (0.7) to decide at which point two lines are considered entirely different (therefore no inline-diff required). The current implementation is kept behind an experimental flag in order to test the effect on performance. In order to activate it, set inline-color-diff to true in [experimental]. diff -r f56a30b844aa -r bab248bd3899 mercurial/cmdutil.py --- a/mercurial/cmdutil.py Sat Oct 28 17:50:25 2017 +0530 +++ b/mercurial/cmdutil.py Thu Oct 26 00:13:38 2017 +0900 @@ -7,6 +7,7 @@ from __future__ import absolute_import +import difflib import errno import itertools import os @@ -1513,6 +1514,11 @@ def diffordiffstat(ui, repo, diffopts, n ui.warn(_('warning: %s not inside relative root %s\n') % ( match.uipath(matchroot), uirelroot)) + store = { + 'diff.inserted': [], + 'diff.deleted': [] + } + status = False if stat: diffopts = diffopts.copy(context=0) width = 80 @@ -1529,7 +1535,31 @@ def diffordiffstat(ui, repo, diffopts, n changes, diffopts, prefix=prefix, relroot=relroot, hunksfilterfn=hunksfilterfn): - write(chunk, label=label) + + if not ui.configbool("experimental", "inline-color-diff"): + write(chunk, label=label) + continue + + # Each deleted/inserted chunk is followed by an EOL chunk with '' + # label. The 'status' flag helps us grab that second line. + if label in ['diff.deleted', 'diff.inserted'] or status: + if status: + store[status].append(chunk) + status = False + else: + store[label].append(chunk) + status = label + continue + + if store['diff.inserted'] or store['diff.deleted']: + for line, l in _chunkdiff(store): + write(line, label=l) + + store['diff.inserted'] = [] + store['diff.deleted'] = [] + + if chunk: + write(chunk, label=label) if listsubrepos: ctx1 = repo[node1] @@ -1548,6 +1578,66 @@ def diffordiffstat(ui, repo, diffopts, n sub.diff(ui, diffopts, tempnode2, submatch, changes=changes, stat=stat, fp=fp, prefix=prefix) +def _chunkdiff(store): + '''Returns a (line, label) iterator over a corresponding deletion and + insertion set. The set has to be considered as a whole in order to match + lines and perform inline coloring. + ''' + def chunkiterator(list1, list2, direction): + '''For each string in list1, finds matching string in list2 and returns + an iterator over their differences. + ''' + used = [] + for a in list1: + done = False + for i, b in enumerate(list2): + if done or i in used: + continue + if difflib.SequenceMatcher(None, a, b).ratio() > 0.7: + buff = _inlinediff(a, b, direction=direction) + for line in buff: + yield (line[1], line[0]) + done = True + used.append(i) # insure lines in b can be matched only once + if not done: + yield (a, 'diff.' + direction) + + insert = store['diff.inserted'] + delete = store['diff.deleted'] + return itertools.chain(chunkiterator(delete, insert, 'deleted'), + chunkiterator(insert, delete, 'inserted')) + +def _inlinediff(from_string, to_string, direction): + '''Perform string diff to highlight specific changes.''' + direction_skip = '+?' if direction == 'deleted' else '-?' + if direction == 'deleted': + to_string, from_string = from_string, to_string + + # buffer required to remove last space, there may be smarter ways to do this + buff = [] + + # we never want to higlight the leading +- + if direction == 'deleted' and to_string.startswith('-'): + buff.append(('diff.deleted', '-')) + to_string = to_string[1:] + from_string = from_string[1:] + elif direction == 'inserted' and from_string.startswith('+'): + buff.append(('diff.inserted', '+')) + to_string = to_string[1:] + from_string = from_string[1:] + + s = difflib.ndiff(to_string.split(' '), from_string.split(' ')) + for line in s: + if line[0] in direction_skip: + continue + l = 'diff.' + direction + '.highlight' + if line[0] in ' ': # unchanged parts + l = 'diff.' + direction + buff.append((l, line[2:] + ' ')) + + buff[-1] = (buff[-1][0], buff[-1][1].strip(' ')) + return buff + def _changesetlabels(ctx): labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()] if ctx.obsolete(): diff -r f56a30b844aa -r bab248bd3899 mercurial/color.py --- a/mercurial/color.py Sat Oct 28 17:50:25 2017 +0530 +++ b/mercurial/color.py Thu Oct 26 00:13:38 2017 +0900 @@ -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 -r f56a30b844aa -r bab248bd3899 mercurial/configitems.py --- a/mercurial/configitems.py Sat Oct 28 17:50:25 2017 +0530 +++ b/mercurial/configitems.py Thu Oct 26 00:13:38 2017 +0900 @@ -388,6 +388,9 @@ coreconfigitem('experimental', 'evolutio coreconfigitem('experimental', 'evolution.track-operation', default=True, ) +coreconfigitem('experimental', 'inline-color-diff', + default=False, +) coreconfigitem('experimental', 'maxdeltachainspan', default=-1, ) diff -r f56a30b844aa -r bab248bd3899 tests/test-diff-color.t --- a/tests/test-diff-color.t Sat Oct 28 17:50:25 2017 +0530 +++ b/tests/test-diff-color.t Thu Oct 26 00:13:38 2017 +0900 @@ -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.inline-color-diff=False + \x1b[0;1mdiff --git a/file1 b/file1\x1b[0m (esc) + \x1b[0;31;1m--- a/file1\x1b[0m (esc) + \x1b[0;32;1m+++ b/file1\x1b[0m (esc) + \x1b[0;35m@@ -1,13 +1,14 @@\x1b[0m (esc) + \x1b[0;31m-this is the first line\x1b[0m (esc) + \x1b[0;31m-this is the second line\x1b[0m (esc) + \x1b[0;31m- third line starts with space\x1b[0m (esc) + \x1b[0;31m-+ starts with a plus sign\x1b[0m (esc) + \x1b[0;32m+that is the first paragraph\x1b[0m (esc) + \x1b[0;32m+ this is the second line\x1b[0m (esc) + \x1b[0;32m+third line starts with space\x1b[0m (esc) + \x1b[0;32m+- starts with a minus sign\x1b[0m (esc) + + this line won't change + + two lines are going to + \x1b[0;31m-be changed into three!\x1b[0m (esc) + \x1b[0;32m+(entirely magically,\x1b[0m (esc) + \x1b[0;32m+ assuming this works)\x1b[0m (esc) + \x1b[0;32m+be changed into four!\x1b[0m (esc) + + \x1b[0;31m-three of those lines will\x1b[0m (esc) + \x1b[0;31m-collapse onto one\x1b[0m (esc) + \x1b[0;31m-(to see if it works)\x1b[0m (esc) + \x1b[0;32m+three of those lines have\x1b[0m (esc) + \x1b[0;32m+collapsed onto one\x1b[0m (esc) + $ hg diff --config experimental.inline-color-diff=True + \x1b[0;1mdiff --git a/file1 b/file1\x1b[0m (esc) + \x1b[0;31;1m--- a/file1\x1b[0m (esc) + \x1b[0;32;1m+++ b/file1\x1b[0m (esc) + \x1b[0;35m@@ -1,13 +1,14 @@\x1b[0m (esc) + \x1b[0;31m-\x1b[0m\x1b[0;31mthis \x1b[0m\x1b[0;31mis \x1b[0m\x1b[0;31mthe \x1b[0m\x1b[0;31;1;4mfirst \x1b[0m\x1b[0;31mline\x1b[0m (esc) + \x1b[0;31m-this is the second line\x1b[0m (esc) + \x1b[0;31m-\x1b[0m\x1b[0;31;1;4m \x1b[0m\x1b[0;31;1;4m \x1b[0m\x1b[0;31;1;4m \x1b[0m\x1b[0;31;1;4m \x1b[0m\x1b[0;31mthird \x1b[0m\x1b[0;31mline \x1b[0m\x1b[0;31mstarts \x1b[0m\x1b[0;31mwith \x1b[0m\x1b[0;31mspace\x1b[0m (esc) + \x1b[0;31m-\x1b[0m\x1b[0;31;1;4m+ \x1b[0m\x1b[0;31mstarts \x1b[0m\x1b[0;31mwith \x1b[0m\x1b[0;31ma \x1b[0m\x1b[0;31;1;4mplus \x1b[0m\x1b[0;31msign\x1b[0m (esc) + \x1b[0;32m+that is the first paragraph\x1b[0m (esc) + \x1b[0;32m+\x1b[0m\x1b[0;32;1;4m \x1b[0m\x1b[0;32;1;4m \x1b[0m\x1b[0;32;1;4m \x1b[0m\x1b[0;32;1;4m \x1b[0m\x1b[0;32mthis \x1b[0m\x1b[0;32mis \x1b[0m\x1b[0;32mthe \x1b[0m\x1b[0;32;1;4msecond \x1b[0m\x1b[0;32mline\x1b[0m (esc) + \x1b[0;32m+\x1b[0m\x1b[0;32mthird \x1b[0m\x1b[0;32mline \x1b[0m\x1b[0;32mstarts \x1b[0m\x1b[0;32mwith \x1b[0m\x1b[0;32mspace\x1b[0m (esc) + \x1b[0;32m+\x1b[0m\x1b[0;32;1;4m- \x1b[0m\x1b[0;32mstarts \x1b[0m\x1b[0;32mwith \x1b[0m\x1b[0;32ma \x1b[0m\x1b[0;32;1;4mminus \x1b[0m\x1b[0;32msign\x1b[0m (esc) + + this line won't change + + two lines are going to + \x1b[0;31m-\x1b[0m\x1b[0;31mbe \x1b[0m\x1b[0;31mchanged \x1b[0m\x1b[0;31minto \x1b[0m\x1b[0;31;1;4mthree!\x1b[0m (esc) + \x1b[0;32m+(entirely magically,\x1b[0m (esc) + \x1b[0;32m+ assuming this works)\x1b[0m (esc) + \x1b[0;32m+\x1b[0m\x1b[0;32mbe \x1b[0m\x1b[0;32mchanged \x1b[0m\x1b[0;32minto \x1b[0m\x1b[0;32;1;4mfour!\x1b[0m (esc) + + \x1b[0;31m-\x1b[0m\x1b[0;31mthree \x1b[0m\x1b[0;31mof \x1b[0m\x1b[0;31mthose \x1b[0m\x1b[0;31mlines \x1b[0m\x1b[0;31;1;4mwill\x1b[0m (esc) + \x1b[0;31m-\x1b[0m\x1b[0;31;1;4mcollapse \x1b[0m\x1b[0;31monto \x1b[0m\x1b[0;31mone\x1b[0m (esc) + \x1b[0;31m-(to see if it works)\x1b[0m (esc) + \x1b[0;32m+\x1b[0m\x1b[0;32mthree \x1b[0m\x1b[0;32mof \x1b[0m\x1b[0;32mthose \x1b[0m\x1b[0;32mlines \x1b[0m\x1b[0;32;1;4mhave\x1b[0m (esc) + \x1b[0;32m+\x1b[0m\x1b[0;32;1;4mcollapsed \x1b[0m\x1b[0;32monto \x1b[0m\x1b[0;32mone\x1b[0m (esc) _______________________________________________ Mercurial-devel mailing list Mercurial-devel@mercurial-scm.org https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel