Commit | Line | Data |
---|---|---|
927a13fe JK |
1 | #!/usr/bin/perl |
2 | ||
8d00662d | 3 | use 5.008; |
2b21008d JK |
4 | use warnings FATAL => 'all'; |
5 | use strict; | |
6 | ||
927a13fe JK |
7 | # Highlight by reversing foreground and background. You could do |
8 | # other things like bold or underline if you prefer. | |
bca45fbc JK |
9 | my @OLD_HIGHLIGHT = ( |
10 | color_config('color.diff-highlight.oldnormal'), | |
11 | color_config('color.diff-highlight.oldhighlight', "\x1b[7m"), | |
12 | color_config('color.diff-highlight.oldreset', "\x1b[27m") | |
13 | ); | |
14 | my @NEW_HIGHLIGHT = ( | |
15 | color_config('color.diff-highlight.newnormal', $OLD_HIGHLIGHT[0]), | |
16 | color_config('color.diff-highlight.newhighlight', $OLD_HIGHLIGHT[1]), | |
17 | color_config('color.diff-highlight.newreset', $OLD_HIGHLIGHT[2]) | |
18 | ); | |
19 | ||
20 | my $RESET = "\x1b[m"; | |
927a13fe | 21 | my $COLOR = qr/\x1b\[[0-9;]*m/; |
097128d1 | 22 | my $BORING = qr/$COLOR|\s/; |
927a13fe | 23 | |
34d9819e JK |
24 | my @removed; |
25 | my @added; | |
26 | my $in_hunk; | |
927a13fe | 27 | |
251e7dad JS |
28 | # Some scripts may not realize that SIGPIPE is being ignored when launching the |
29 | # pager--for instance scripts written in Python. | |
30 | $SIG{PIPE} = 'DEFAULT'; | |
31 | ||
927a13fe | 32 | while (<>) { |
34d9819e JK |
33 | if (!$in_hunk) { |
34 | print; | |
35 | $in_hunk = /^$COLOR*\@/; | |
36 | } | |
37 | elsif (/^$COLOR*-/) { | |
38 | push @removed, $_; | |
39 | } | |
40 | elsif (/^$COLOR*\+/) { | |
41 | push @added, $_; | |
927a13fe JK |
42 | } |
43 | else { | |
34d9819e JK |
44 | show_hunk(\@removed, \@added); |
45 | @removed = (); | |
46 | @added = (); | |
47 | ||
48 | print; | |
49 | $in_hunk = /^$COLOR*[\@ ]/; | |
927a13fe JK |
50 | } |
51 | ||
52 | # Most of the time there is enough output to keep things streaming, | |
53 | # but for something like "git log -Sfoo", you can get one early | |
54 | # commit and then many seconds of nothing. We want to show | |
55 | # that one commit as soon as possible. | |
56 | # | |
57 | # Since we can receive arbitrary input, there's no optimal | |
58 | # place to flush. Flushing on a blank line is a heuristic that | |
59 | # happens to match git-log output. | |
60 | if (!length) { | |
61 | local $| = 1; | |
62 | } | |
63 | } | |
64 | ||
34d9819e JK |
65 | # Flush any queued hunk (this can happen when there is no trailing context in |
66 | # the final diff of the input). | |
67 | show_hunk(\@removed, \@added); | |
927a13fe JK |
68 | |
69 | exit 0; | |
70 | ||
bca45fbc JK |
71 | # Ideally we would feed the default as a human-readable color to |
72 | # git-config as the fallback value. But diff-highlight does | |
73 | # not otherwise depend on git at all, and there are reports | |
74 | # of it being used in other settings. Let's handle our own | |
75 | # fallback, which means we will work even if git can't be run. | |
76 | sub color_config { | |
77 | my ($key, $default) = @_; | |
78 | my $s = `git config --get-color $key 2>/dev/null`; | |
79 | return length($s) ? $s : $default; | |
80 | } | |
81 | ||
6463fd7e JK |
82 | sub show_hunk { |
83 | my ($a, $b) = @_; | |
84 | ||
34d9819e JK |
85 | # If one side is empty, then there is nothing to compare or highlight. |
86 | if (!@$a || !@$b) { | |
87 | print @$a, @$b; | |
88 | return; | |
89 | } | |
90 | ||
91 | # If we have mismatched numbers of lines on each side, we could try to | |
92 | # be clever and match up similar lines. But for now we are simple and | |
93 | # stupid, and only handle multi-line hunks that remove and add the same | |
94 | # number of lines. | |
95 | if (@$a != @$b) { | |
96 | print @$a, @$b; | |
97 | return; | |
98 | } | |
99 | ||
100 | my @queue; | |
101 | for (my $i = 0; $i < @$a; $i++) { | |
102 | my ($rm, $add) = highlight_pair($a->[$i], $b->[$i]); | |
103 | print $rm; | |
104 | push @queue, $add; | |
105 | } | |
106 | print @queue; | |
6463fd7e JK |
107 | } |
108 | ||
109 | sub highlight_pair { | |
927a13fe JK |
110 | my @a = split_line(shift); |
111 | my @b = split_line(shift); | |
112 | ||
113 | # Find common prefix, taking care to skip any ansi | |
114 | # color codes. | |
115 | my $seen_plusminus; | |
116 | my ($pa, $pb) = (0, 0); | |
117 | while ($pa < @a && $pb < @b) { | |
118 | if ($a[$pa] =~ /$COLOR/) { | |
119 | $pa++; | |
120 | } | |
121 | elsif ($b[$pb] =~ /$COLOR/) { | |
122 | $pb++; | |
123 | } | |
124 | elsif ($a[$pa] eq $b[$pb]) { | |
125 | $pa++; | |
126 | $pb++; | |
127 | } | |
128 | elsif (!$seen_plusminus && $a[$pa] eq '-' && $b[$pb] eq '+') { | |
129 | $seen_plusminus = 1; | |
130 | $pa++; | |
131 | $pb++; | |
132 | } | |
133 | else { | |
134 | last; | |
135 | } | |
136 | } | |
137 | ||
138 | # Find common suffix, ignoring colors. | |
139 | my ($sa, $sb) = ($#a, $#b); | |
140 | while ($sa >= $pa && $sb >= $pb) { | |
141 | if ($a[$sa] =~ /$COLOR/) { | |
142 | $sa--; | |
143 | } | |
144 | elsif ($b[$sb] =~ /$COLOR/) { | |
145 | $sb--; | |
146 | } | |
147 | elsif ($a[$sa] eq $b[$sb]) { | |
148 | $sa--; | |
149 | $sb--; | |
150 | } | |
151 | else { | |
152 | last; | |
153 | } | |
154 | } | |
155 | ||
097128d1 | 156 | if (is_pair_interesting(\@a, $pa, $sa, \@b, $pb, $sb)) { |
bca45fbc JK |
157 | return highlight_line(\@a, $pa, $sa, \@OLD_HIGHLIGHT), |
158 | highlight_line(\@b, $pb, $sb, \@NEW_HIGHLIGHT); | |
097128d1 JK |
159 | } |
160 | else { | |
6463fd7e JK |
161 | return join('', @a), |
162 | join('', @b); | |
097128d1 | 163 | } |
927a13fe JK |
164 | } |
165 | ||
166 | sub split_line { | |
167 | local $_ = shift; | |
8d00662d KM |
168 | return utf8::decode($_) ? |
169 | map { utf8::encode($_); $_ } | |
170 | map { /$COLOR/ ? $_ : (split //) } | |
171 | split /($COLOR+)/ : | |
172 | map { /$COLOR/ ? $_ : (split //) } | |
173 | split /($COLOR+)/; | |
927a13fe JK |
174 | } |
175 | ||
6463fd7e | 176 | sub highlight_line { |
bca45fbc JK |
177 | my ($line, $prefix, $suffix, $theme) = @_; |
178 | ||
179 | my $start = join('', @{$line}[0..($prefix-1)]); | |
180 | my $mid = join('', @{$line}[$prefix..$suffix]); | |
181 | my $end = join('', @{$line}[($suffix+1)..$#$line]); | |
182 | ||
183 | # If we have a "normal" color specified, then take over the whole line. | |
184 | # Otherwise, we try to just manipulate the highlighted bits. | |
185 | if (defined $theme->[0]) { | |
186 | s/$COLOR//g for ($start, $mid, $end); | |
187 | chomp $end; | |
188 | return join('', | |
189 | $theme->[0], $start, $RESET, | |
190 | $theme->[1], $mid, $RESET, | |
191 | $theme->[0], $end, $RESET, | |
192 | "\n" | |
193 | ); | |
194 | } else { | |
195 | return join('', | |
196 | $start, | |
197 | $theme->[1], $mid, $theme->[2], | |
198 | $end | |
199 | ); | |
200 | } | |
927a13fe | 201 | } |
097128d1 JK |
202 | |
203 | # Pairs are interesting to highlight only if we are going to end up | |
204 | # highlighting a subset (i.e., not the whole line). Otherwise, the highlighting | |
205 | # is just useless noise. We can detect this by finding either a matching prefix | |
206 | # or suffix (disregarding boring bits like whitespace and colorization). | |
207 | sub is_pair_interesting { | |
208 | my ($a, $pa, $sa, $b, $pb, $sb) = @_; | |
209 | my $prefix_a = join('', @$a[0..($pa-1)]); | |
210 | my $prefix_b = join('', @$b[0..($pb-1)]); | |
211 | my $suffix_a = join('', @$a[($sa+1)..$#$a]); | |
212 | my $suffix_b = join('', @$b[($sb+1)..$#$b]); | |
213 | ||
214 | return $prefix_a !~ /^$COLOR*-$BORING*$/ || | |
215 | $prefix_b !~ /^$COLOR*\+$BORING*$/ || | |
216 | $suffix_a !~ /^$BORING*$/ || | |
217 | $suffix_b !~ /^$BORING*$/; | |
218 | } |