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