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