1#!/usr/bin/perl 2 3use5.008; 4use warnings FATAL =>'all'; 5use strict; 6 7# Highlight by reversing foreground and background. You could do 8# other things like bold or underline if you prefer. 9my@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); 14my@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 20my$RESET="\x1b[m"; 21my$COLOR=qr/\x1b\[[0-9;]*m/; 22my$BORING=qr/$COLOR|\s/; 23 24my@removed; 25my@added; 26my$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 32while(<>) { 33if(!$in_hunk) { 34print; 35$in_hunk=/^$COLOR*\@/; 36} 37elsif(/^$COLOR*-/) { 38push@removed,$_; 39} 40elsif(/^$COLOR*\+/) { 41push@added,$_; 42} 43else{ 44 show_hunk(\@removed, \@added); 45@removed= (); 46@added= (); 47 48print; 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. 60if(!length) { 61local$| =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). 67show_hunk(\@removed, \@added); 68 69exit0; 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. 76sub color_config { 77my($key,$default) =@_; 78my$s=`git config --get-color$key2>/dev/null`; 79returnlength($s) ?$s:$default; 80} 81 82sub show_hunk { 83my($a,$b) =@_; 84 85# If one side is empty, then there is nothing to compare or highlight. 86if(!@$a|| !@$b) { 87print@$a,@$b; 88return; 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. 95if(@$a!=@$b) { 96print@$a,@$b; 97return; 98} 99 100my@queue; 101for(my$i=0;$i<@$a;$i++) { 102my($rm,$add) = highlight_pair($a->[$i],$b->[$i]); 103print$rm; 104push@queue,$add; 105} 106print@queue; 107} 108 109sub highlight_pair { 110my@a= split_line(shift); 111my@b= split_line(shift); 112 113# Find common prefix, taking care to skip any ansi 114# color codes. 115my$seen_plusminus; 116my($pa,$pb) = (0,0); 117while($pa<@a&&$pb<@b) { 118if($a[$pa] =~/$COLOR/) { 119$pa++; 120} 121elsif($b[$pb] =~/$COLOR/) { 122$pb++; 123} 124elsif($a[$pa]eq$b[$pb]) { 125$pa++; 126$pb++; 127} 128elsif(!$seen_plusminus&&$a[$pa]eq'-'&&$b[$pb]eq'+') { 129$seen_plusminus=1; 130$pa++; 131$pb++; 132} 133else{ 134last; 135} 136} 137 138# Find common suffix, ignoring colors. 139my($sa,$sb) = ($#a,$#b); 140while($sa>=$pa&&$sb>=$pb) { 141if($a[$sa] =~/$COLOR/) { 142$sa--; 143} 144elsif($b[$sb] =~/$COLOR/) { 145$sb--; 146} 147elsif($a[$sa]eq$b[$sb]) { 148$sa--; 149$sb--; 150} 151else{ 152last; 153} 154} 155 156if(is_pair_interesting(\@a,$pa,$sa, \@b,$pb,$sb)) { 157return highlight_line(\@a,$pa,$sa, \@OLD_HIGHLIGHT), 158 highlight_line(\@b,$pb,$sb, \@NEW_HIGHLIGHT); 159} 160else{ 161returnjoin('',@a), 162join('',@b); 163} 164} 165 166sub split_line { 167local$_=shift; 168return utf8::decode($_) ? 169map{ utf8::encode($_);$_} 170map{/$COLOR/?$_: (split//) } 171split/($COLOR+)/: 172map{/$COLOR/?$_: (split//) } 173split/($COLOR+)/; 174} 175 176sub highlight_line { 177my($line,$prefix,$suffix,$theme) =@_; 178 179my$start=join('', @{$line}[0..($prefix-1)]); 180my$mid=join('', @{$line}[$prefix..$suffix]); 181my$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. 185if(defined$theme->[0]) { 186s/$COLOR//gfor($start,$mid,$end); 187chomp$end; 188returnjoin('', 189$theme->[0],$start,$RESET, 190$theme->[1],$mid,$RESET, 191$theme->[0],$end,$RESET, 192"\n" 193); 194}else{ 195returnjoin('', 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). 207sub is_pair_interesting { 208my($a,$pa,$sa,$b,$pb,$sb) =@_; 209my$prefix_a=join('',@$a[0..($pa-1)]); 210my$prefix_b=join('',@$b[0..($pb-1)]); 211my$suffix_a=join('',@$a[($sa+1)..$#$a]); 212my$suffix_b=join('',@$b[($sb+1)..$#$b]); 213 214return$prefix_a!~/^$COLOR*-$BORING*$/|| 215$prefix_b!~/^$COLOR*\+$BORING*$/|| 216$suffix_a!~/^$BORING*$/|| 217$suffix_b!~/^$BORING*$/; 218}