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