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$HIGHLIGHT="\x1b[7m"; 9my$UNHIGHLIGHT="\x1b[27m"; 10my$COLOR=qr/\x1b\[[0-9;]*m/; 11my$BORING=qr/$COLOR|\s/; 12 13my@window; 14 15while(<>) { 16# We highlight only single-line changes, so we need 17# a 4-line window to make a decision on whether 18# to highlight. 19push@window,$_; 20next if@window<4; 21if($window[0] =~/^$COLOR*(\@| )/&& 22$window[1] =~/^$COLOR*-/&& 23$window[2] =~/^$COLOR*\+/&& 24$window[3] !~/^$COLOR*\+/) { 25print shift@window; 26 show_pair(shift@window,shift@window); 27} 28else{ 29print shift@window; 30} 31 32# Most of the time there is enough output to keep things streaming, 33# but for something like "git log -Sfoo", you can get one early 34# commit and then many seconds of nothing. We want to show 35# that one commit as soon as possible. 36# 37# Since we can receive arbitrary input, there's no optimal 38# place to flush. Flushing on a blank line is a heuristic that 39# happens to match git-log output. 40if(!length) { 41local$| =1; 42} 43} 44 45# Special case a single-line hunk at the end of file. 46if(@window==3&& 47$window[0] =~/^$COLOR*(\@| )/&& 48$window[1] =~/^$COLOR*-/&& 49$window[2] =~/^$COLOR*\+/) { 50print shift@window; 51 show_pair(shift@window,shift@window); 52} 53 54# And then flush any remaining lines. 55while(@window) { 56print shift@window; 57} 58 59exit0; 60 61sub show_pair { 62my@a= split_line(shift); 63my@b= split_line(shift); 64 65# Find common prefix, taking care to skip any ansi 66# color codes. 67my$seen_plusminus; 68my($pa,$pb) = (0,0); 69while($pa<@a&&$pb<@b) { 70if($a[$pa] =~/$COLOR/) { 71$pa++; 72} 73elsif($b[$pb] =~/$COLOR/) { 74$pb++; 75} 76elsif($a[$pa]eq$b[$pb]) { 77$pa++; 78$pb++; 79} 80elsif(!$seen_plusminus&&$a[$pa]eq'-'&&$b[$pb]eq'+') { 81$seen_plusminus=1; 82$pa++; 83$pb++; 84} 85else{ 86last; 87} 88} 89 90# Find common suffix, ignoring colors. 91my($sa,$sb) = ($#a,$#b); 92while($sa>=$pa&&$sb>=$pb) { 93if($a[$sa] =~/$COLOR/) { 94$sa--; 95} 96elsif($b[$sb] =~/$COLOR/) { 97$sb--; 98} 99elsif($a[$sa]eq$b[$sb]) { 100$sa--; 101$sb--; 102} 103else{ 104last; 105} 106} 107 108if(is_pair_interesting(\@a,$pa,$sa, \@b,$pb,$sb)) { 109print highlight(\@a,$pa,$sa); 110print highlight(\@b,$pb,$sb); 111} 112else{ 113print join('',@a); 114print join('',@b); 115} 116} 117 118sub split_line { 119local$_=shift; 120returnmap{/$COLOR/?$_: (split//) } 121split/($COLOR*)/; 122} 123 124sub highlight { 125my($line,$prefix,$suffix) =@_; 126 127returnjoin('', 128@{$line}[0..($prefix-1)], 129$HIGHLIGHT, 130@{$line}[$prefix..$suffix], 131$UNHIGHLIGHT, 132@{$line}[($suffix+1)..$#$line] 133); 134} 135 136# Pairs are interesting to highlight only if we are going to end up 137# highlighting a subset (i.e., not the whole line). Otherwise, the highlighting 138# is just useless noise. We can detect this by finding either a matching prefix 139# or suffix (disregarding boring bits like whitespace and colorization). 140sub is_pair_interesting { 141my($a,$pa,$sa,$b,$pb,$sb) =@_; 142my$prefix_a=join('',@$a[0..($pa-1)]); 143my$prefix_b=join('',@$b[0..($pb-1)]); 144my$suffix_a=join('',@$a[($sa+1)..$#$a]); 145my$suffix_b=join('',@$b[($sb+1)..$#$b]); 146 147return$prefix_a!~/^$COLOR*-$BORING*$/|| 148$prefix_b!~/^$COLOR*\+$BORING*$/|| 149$suffix_a!~/^$BORING*$/|| 150$suffix_b!~/^$BORING*$/; 151}