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