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