@@ -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,
)
@@ -10,6 +10,7 @@ from __future__ import absolute_import,
import collections
import copy
+import difflib
import email
import errno
import hashlib
@@ -2461,6 +2462,12 @@ 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
+ for arg in args:
+ if util.safehasattr(arg, 'ui'):
+ inlinecolor = arg.ui.configbool("experimental", "inline-color-diff")
+ break
+
headprefixes = [('diff', 'diff.diffline'),
('copy', 'diff.extended'),
('rename', 'diff.extended'),
@@ -2477,6 +2484,7 @@ def difflabel(func, *args, **kw):
head = False
for chunk in func(*args, **kw):
lines = chunk.split('\n')
+ matches = [-1 for x in range(len(lines) + 1)]
for i, line in enumerate(lines):
if i != 0:
yield ('\n', '')
@@ -2504,7 +2512,12 @@ def difflabel(func, *args, **kw):
if '\t' == token[0]:
yield (token, 'diff.tab')
else:
- yield (token, label)
+ if not inlinecolor:
+ yield (token, label)
+ else:
+ buff, matches = _worddiff(lines, i, matches)
+ for (color, word) in buff:
+ yield (word, color)
else:
yield (stripline, label)
break
@@ -2513,6 +2526,63 @@ def difflabel(func, *args, **kw):
if line != stripline:
yield (line[len(stripline):], 'diff.trailingwhitespace')
+def _worddiff(slist, idx, matches):
+ '''Find match of a given string in current chunk and performs word diff.'''
+ operation = 'inserted' if slist[idx][0] == '+' else 'deleted'
+ bound = matches[-1] # last item in matches stores the id of the last match
+
+ # inserted lines should only be compared to lines that matched them before
+ if operation == 'inserted':
+ if matches[idx] != -1:
+ return _inlinediff(slist[idx],
+ slist[matches[idx]],
+ operation), matches
+ else:
+ return [('diff.' + operation, slist[idx])], matches
+
+ # deleted lines first need to be matched
+ for i, line in enumerate(slist[bound + 1:-1]):
+ if line == '':
+ continue
+ if line[0] == '+':
+ sim = difflib.SequenceMatcher(None, slist[idx], line).ratio()
+ if sim > 0.7:
+ matches[i + bound + 1] = idx
+ matches[-1] = i + bound + 1
+ return _inlinediff(slist[idx], line, operation), matches
+ return [('diff.' + operation, slist[idx])], matches
+
+def _inlinediff(s1, s2, operation):
+ '''Perform string diff to highlight specific changes.'''
+ operation_skip = '+?' if operation == 'deleted' else '-?'
+ if operation == 'deleted':
+ s2, s1 = s1, s2
+
+ # buffer required to remove last space, there may be smarter ways to do this
+ buff = []
+
+ # we never want to higlight the leading +-
+ if operation == 'deleted' and s2.startswith('-'):
+ buff.append(('diff.deleted', '-'))
+ s2 = s2[1:]
+ s1 = s1[1:]
+ elif operation == 'inserted' and s1.startswith('+'):
+ buff.append(('diff.inserted', '+'))
+ s2 = s2[1:]
+ s1 = s1[1:]
+
+ s = difflib.ndiff(re.split(r'(\W)', s2), re.split(r'(\W)', s1))
+ for line in s:
+ if line[0] in operation_skip:
+ continue
+ l = 'diff.' + operation + '.highlight'
+ if line[0] in ' ': # unchanged parts
+ l = 'diff.' + operation
+ buff.append((l, line[2:]))
+
+ buff[-1] = (buff[-1][0], buff[-1][1].strip(' '))
+ return buff
+
def diffui(*args, **kw):
'''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
return difflabel(diff, *args, **kw)
@@ -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| ][diff.deleted|is][diff.deleted| ][diff.deleted|the][diff.deleted| ][diff.deleted.highlight|first][diff.deleted| ][diff.deleted|line]
+ [diff.deleted|-this is the second line]
+ [diff.deleted|-][diff.deleted.highlight| ][diff.deleted.highlight| ][diff.deleted.highlight| ][diff.deleted.highlight| ][diff.deleted|third][diff.deleted| ][diff.deleted|line][diff.deleted| ][diff.deleted|starts][diff.deleted| ][diff.deleted|with][diff.deleted| ][diff.deleted|space]
+ [diff.deleted|-][diff.deleted.highlight|+][diff.deleted| ][diff.deleted|starts][diff.deleted| ][diff.deleted|with][diff.deleted| ][diff.deleted|a][diff.deleted| ][diff.deleted.highlight|plus][diff.deleted| ][diff.deleted|sign]
+ [diff.inserted|+that is the first paragraph]
+ [diff.inserted|+][diff.inserted.highlight| ][diff.inserted.highlight| ][diff.inserted.highlight| ][diff.inserted.highlight| ][diff.inserted|this][diff.inserted| ][diff.inserted|is][diff.inserted| ][diff.inserted|the][diff.inserted| ][diff.inserted.highlight|second][diff.inserted| ][diff.inserted|line]
+ [diff.inserted|+][diff.inserted|third][diff.inserted| ][diff.inserted|line][diff.inserted| ][diff.inserted|starts][diff.inserted| ][diff.inserted|with][diff.inserted| ][diff.inserted|space]
+ [diff.inserted|+][diff.inserted.highlight|-][diff.inserted| ][diff.inserted|starts][diff.inserted| ][diff.inserted|with][diff.inserted| ][diff.inserted|a][diff.inserted| ][diff.inserted.highlight|minus][diff.inserted| ][diff.inserted|sign]
+
+ this line won't change
+
+ two lines are going to
+ [diff.deleted|-][diff.deleted|be][diff.deleted| ][diff.deleted|changed][diff.deleted| ][diff.deleted|into][diff.deleted| ][diff.deleted.highlight|three][diff.deleted|!]
+ [diff.inserted|+(entirely magically,]
+ [diff.inserted|+ assuming this works)]
+ [diff.inserted|+][diff.inserted|be][diff.inserted| ][diff.inserted|changed][diff.inserted| ][diff.inserted|into][diff.inserted| ][diff.inserted.highlight|four][diff.inserted|!]
+
+ [diff.deleted|-][diff.deleted|three][diff.deleted| ][diff.deleted|of][diff.deleted| ][diff.deleted|those][diff.deleted| ][diff.deleted|lines][diff.deleted| ][diff.deleted.highlight|will]
+ [diff.deleted|-][diff.deleted.highlight|collapse][diff.deleted| ][diff.deleted|onto][diff.deleted| ][diff.deleted|one]
+ [diff.deleted|-(to see if it works)]
+ [diff.inserted|+][diff.inserted|three][diff.inserted| ][diff.inserted|of][diff.inserted| ][diff.inserted|those][diff.inserted| ][diff.inserted|lines][diff.inserted| ][diff.inserted.highlight|have]
+ [diff.inserted|+][diff.inserted.highlight|collapsed][diff.inserted| ][diff.inserted|onto][diff.inserted| ][diff.inserted|one]