contrib: add diff highlight script

This is a simple and stupid script for highlighting differing parts of lines in a unified diff. See the README for a discussion of the limitations. Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jeff King committed Oct 18, 2011 at 00:52 UTC 927a13fe87e4b2e0edabb2f8615f0851f70fbbd8
2 files changed +181
contrib/diff-highlight/README new
+57
@@ -0,0 +1,57 @@
1 +diff-highlight
2 +==============
3 +
4 +Line oriented diffs are great for reviewing code, because for most
5 +hunks, you want to see the old and the new segments of code next to each
6 +other. Sometimes, though, when an old line and a new line are very
7 +similar, it's hard to immediately see the difference.
8 +
9 +You can use "--color-words" to highlight only the changed portions of
10 +lines. However, this can often be hard to read for code, as it loses
11 +the line structure, and you end up with oddly formatted bits.
12 +
13 +Instead, this script post-processes the line-oriented diff, finds pairs
14 +of lines, and highlights the differing segments. It's currently very
15 +simple and stupid about doing these tasks. In particular:
16 +
17 + 1. It will only highlight a pair of lines if they are the only two
18 + lines in a hunk. It could instead try to match up "before" and
19 + "after" lines for a given hunk into pairs of similar lines.
20 + However, this may end up visually distracting, as the paired
21 + lines would have other highlighted lines in between them. And in
22 + practice, the lines which most need attention called to their
23 + small, hard-to-see changes are touching only a single line.
24 +
25 + 2. It will find the common prefix and suffix of two lines, and
26 + consider everything in the middle to be "different". It could
27 + instead do a real diff of the characters between the two lines and
28 + find common subsequences. However, the point of the highlight is to
29 + call attention to a certain area. Even if some small subset of the
30 + highlighted area actually didn't change, that's OK. In practice it
31 + ends up being more readable to just have a single blob on the line
32 + showing the interesting bit.
33 +
34 +The goal of the script is therefore not to be exact about highlighting
35 +changes, but to call attention to areas of interest without being
36 +visually distracting. Non-diff lines and existing diff coloration is
37 +preserved; the intent is that the output should look exactly the same as
38 +the input, except for the occasional highlight.
39 +
40 +Use
41 +---
42 +
43 +You can try out the diff-highlight program with:
44 +
45 +---------------------------------------------
46 +git log -p --color | /path/to/diff-highlight
47 +---------------------------------------------
48 +
49 +If you want to use it all the time, drop it in your $PATH and put the
50 +following in your git configuration:
51 +
52 +---------------------------------------------
53 +[pager]
54 + log = diff-highlight | less
55 + show = diff-highlight | less
56 + diff = diff-highlight | less
57 +---------------------------------------------
contrib/diff-highlight/diff-highlight new
+124
@@ -0,0 +1,124 @@
1 +#!/usr/bin/perl
2 +
3 +# Highlight by reversing foreground and background. You could do
4 +# other things like bold or underline if you prefer.
5 +my $HIGHLIGHT = "\x1b[7m";
6 +my $UNHIGHLIGHT = "\x1b[27m";
7 +my $COLOR = qr/\x1b\[[0-9;]*m/;
8 +
9 +my @window;
10 +
11 +while (<>) {
12 + # We highlight only single-line changes, so we need
13 + # a 4-line window to make a decision on whether
14 + # to highlight.
15 + push @window, $_;
16 + next if @window < 4;
17 + if ($window[0] =~ /^$COLOR*(\@| )/ &&
18 + $window[1] =~ /^$COLOR*-/ &&
19 + $window[2] =~ /^$COLOR*\+/ &&
20 + $window[3] !~ /^$COLOR*\+/) {
21 + print shift @window;
22 + show_pair(shift @window, shift @window);
23 + }
24 + else {
25 + print shift @window;
26 + }
27 +
28 + # Most of the time there is enough output to keep things streaming,
29 + # but for something like "git log -Sfoo", you can get one early
30 + # commit and then many seconds of nothing. We want to show
31 + # that one commit as soon as possible.
32 + #
33 + # Since we can receive arbitrary input, there's no optimal
34 + # place to flush. Flushing on a blank line is a heuristic that
35 + # happens to match git-log output.
36 + if (!length) {
37 + local $| = 1;
38 + }
39 +}
40 +
41 +# Special case a single-line hunk at the end of file.
42 +if (@window == 3 &&
43 + $window[0] =~ /^$COLOR*(\@| )/ &&
44 + $window[1] =~ /^$COLOR*-/ &&
45 + $window[2] =~ /^$COLOR*\+/) {
46 + print shift @window;
47 + show_pair(shift @window, shift @window);
48 +}
49 +
50 +# And then flush any remaining lines.
51 +while (@window) {
52 + print shift @window;
53 +}
54 +
55 +exit 0;
56 +
57 +sub show_pair {
58 + my @a = split_line(shift);
59 + my @b = split_line(shift);
60 +
61 + # Find common prefix, taking care to skip any ansi
62 + # color codes.
63 + my $seen_plusminus;
64 + my ($pa, $pb) = (0, 0);
65 + while ($pa < @a && $pb < @b) {
66 + if ($a[$pa] =~ /$COLOR/) {
67 + $pa++;
68 + }
69 + elsif ($b[$pb] =~ /$COLOR/) {
70 + $pb++;
71 + }
72 + elsif ($a[$pa] eq $b[$pb]) {
73 + $pa++;
74 + $pb++;
75 + }
76 + elsif (!$seen_plusminus && $a[$pa] eq '-' && $b[$pb] eq '+') {
77 + $seen_plusminus = 1;
78 + $pa++;
79 + $pb++;
80 + }
81 + else {
82 + last;
83 + }
84 + }
85 +
86 + # Find common suffix, ignoring colors.
87 + my ($sa, $sb) = ($#a, $#b);
88 + while ($sa >= $pa && $sb >= $pb) {
89 + if ($a[$sa] =~ /$COLOR/) {
90 + $sa--;
91 + }
92 + elsif ($b[$sb] =~ /$COLOR/) {
93 + $sb--;
94 + }
95 + elsif ($a[$sa] eq $b[$sb]) {
96 + $sa--;
97 + $sb--;
98 + }
99 + else {
100 + last;
101 + }
102 + }
103 +
104 + print highlight(\@a, $pa, $sa);
105 + print highlight(\@b, $pb, $sb);
106 +}
107 +
108 +sub split_line {
109 + local $_ = shift;
110 + return map { /$COLOR/ ? $_ : (split //) }
111 + split /($COLOR*)/;
112 +}
113 +
114 +sub highlight {
115 + my ($line, $prefix, $suffix) = @_;
116 +
117 + return join('',
118 + @{$line}[0..($prefix-1)],
119 + $HIGHLIGHT,
120 + @{$line}[$prefix..$suffix],
121 + $UNHIGHLIGHT,
122 + @{$line}[($suffix+1)..$#$line]
123 + );
124 +}