diff-highlight (5399B)
1 #!/usr/bin/perl 2 3 use 5.008; 4 use warnings FATAL => 'all'; 5 use strict; 6 7 # Highlight by reversing foreground and background. You could do 8 # other things like bold or underline if you prefer. 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"; 21 my $COLOR = qr/\x1b\[[0-9;]*m/; 22 my $BORING = qr/$COLOR|\s/; 23 24 my @removed; 25 my @added; 26 my $in_hunk; 27 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 32 while (<>) { 33 if (!$in_hunk) { 34 print; 35 $in_hunk = /^$COLOR*\@/; 36 } 37 elsif (/^$COLOR*-/) { 38 push @removed, $_; 39 } 40 elsif (/^$COLOR*\+/) { 41 push @added, $_; 42 } 43 else { 44 show_hunk(\@removed, \@added); 45 @removed = (); 46 @added = (); 47 48 print; 49 $in_hunk = /^$COLOR*[\@ ]/; 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 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); 68 69 exit 0; 70 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 82 sub show_hunk { 83 my ($a, $b) = @_; 84 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; 107 } 108 109 sub highlight_pair { 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 156 if (is_pair_interesting(\@a, $pa, $sa, \@b, $pb, $sb)) { 157 return highlight_line(\@a, $pa, $sa, \@OLD_HIGHLIGHT), 158 highlight_line(\@b, $pb, $sb, \@NEW_HIGHLIGHT); 159 } 160 else { 161 return join('', @a), 162 join('', @b); 163 } 164 } 165 166 sub split_line { 167 local $_ = shift; 168 return utf8::decode($_) ? 169 map { utf8::encode($_); $_ } 170 map { /$COLOR/ ? $_ : (split //) } 171 split /($COLOR+)/ : 172 map { /$COLOR/ ? $_ : (split //) } 173 split /($COLOR+)/; 174 } 175 176 sub highlight_line { 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 } 201 } 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 }