@@ -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():
@@ -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': '',
@@ -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,
)
@@ -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 --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.inline-color-diff=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|-][diff.deleted|this ][diff.deleted|is ][diff.deleted|the ][diff.deleted.highlight|first ][diff.deleted|line][diff.deleted|]
+ [diff.deleted|-this is the second line][diff.deleted|]
+ [diff.deleted|-][diff.deleted.highlight| ][diff.deleted.highlight| ][diff.deleted.highlight| ][diff.deleted.highlight| ][diff.deleted|third ][diff.deleted|line ][diff.deleted|starts ][diff.deleted|with ][diff.deleted|space][diff.deleted|]
+ [diff.deleted|-][diff.deleted.highlight|+ ][diff.deleted|starts ][diff.deleted|with ][diff.deleted|a ][diff.deleted.highlight|plus ][diff.deleted|sign][diff.deleted|]
+ [diff.inserted|+that is the first paragraph][diff.inserted|]
+ [diff.inserted|+][diff.inserted.highlight| ][diff.inserted.highlight| ][diff.inserted.highlight| ][diff.inserted.highlight| ][diff.inserted|this ][diff.inserted|is ][diff.inserted|the ][diff.inserted.highlight|second ][diff.inserted|line][diff.inserted|]
+ [diff.inserted|+][diff.inserted|third ][diff.inserted|line ][diff.inserted|starts ][diff.inserted|with ][diff.inserted|space][diff.inserted|]
+ [diff.inserted|+][diff.inserted.highlight|- ][diff.inserted|starts ][diff.inserted|with ][diff.inserted|a ][diff.inserted.highlight|minus ][diff.inserted|sign][diff.inserted|]
+
+ this line won't change
+
+ two lines are going to
+ [diff.deleted|-][diff.deleted|be ][diff.deleted|changed ][diff.deleted|into ][diff.deleted.highlight|three!][diff.deleted|]
+ [diff.inserted|+(entirely magically,][diff.inserted|]
+ [diff.inserted|+ assuming this works)][diff.inserted|]
+ [diff.inserted|+][diff.inserted|be ][diff.inserted|changed ][diff.inserted|into ][diff.inserted.highlight|four!][diff.inserted|]
+
+ [diff.deleted|-][diff.deleted|three ][diff.deleted|of ][diff.deleted|those ][diff.deleted|lines ][diff.deleted.highlight|will][diff.deleted|]
+ [diff.deleted|-][diff.deleted.highlight|collapse ][diff.deleted|onto ][diff.deleted|one][diff.deleted|]
+ [diff.deleted|-(to see if it works)][diff.deleted|]
+ [diff.inserted|+][diff.inserted|three ][diff.inserted|of ][diff.inserted|those ][diff.inserted|lines ][diff.inserted.highlight|have][diff.inserted|]
+ [diff.inserted|+][diff.inserted.highlight|collapsed ][diff.inserted|onto ][diff.inserted|one][diff.inserted|]