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_hunk(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_hunk(shift@window,shift@window); 52} 53 54# And then flush any remaining lines. 55while(@window) { 56print shift@window; 57} 58 59exit0; 60 61sub show_hunk { 62my($a,$b) =@_; 63 64print highlight_pair($a,$b); 65} 66 67sub highlight_pair { 68my@a= split_line(shift); 69my@b= split_line(shift); 70 71# Find common prefix, taking care to skip any ansi 72# color codes. 73my$seen_plusminus; 74my($pa,$pb) = (0,0); 75while($pa<@a&&$pb<@b) { 76if($a[$pa] =~/$COLOR/) { 77$pa++; 78} 79elsif($b[$pb] =~/$COLOR/) { 80$pb++; 81} 82elsif($a[$pa]eq$b[$pb]) { 83$pa++; 84$pb++; 85} 86elsif(!$seen_plusminus&&$a[$pa]eq'-'&&$b[$pb]eq'+') { 87$seen_plusminus=1; 88$pa++; 89$pb++; 90} 91else{ 92last; 93} 94} 95 96# Find common suffix, ignoring colors. 97my($sa,$sb) = ($#a,$#b); 98while($sa>=$pa&&$sb>=$pb) { 99if($a[$sa] =~/$COLOR/) { 100$sa--; 101} 102elsif($b[$sb] =~/$COLOR/) { 103$sb--; 104} 105elsif($a[$sa]eq$b[$sb]) { 106$sa--; 107$sb--; 108} 109else{ 110last; 111} 112} 113 114if(is_pair_interesting(\@a,$pa,$sa, \@b,$pb,$sb)) { 115return highlight_line(\@a,$pa,$sa), 116 highlight_line(\@b,$pb,$sb); 117} 118else{ 119returnjoin('',@a), 120join('',@b); 121} 122} 123 124sub split_line { 125local$_=shift; 126returnmap{/$COLOR/?$_: (split//) } 127split/($COLOR*)/; 128} 129 130sub highlight_line { 131my($line,$prefix,$suffix) =@_; 132 133returnjoin('', 134@{$line}[0..($prefix-1)], 135$HIGHLIGHT, 136@{$line}[$prefix..$suffix], 137$UNHIGHLIGHT, 138@{$line}[($suffix+1)..$#$line] 139); 140} 141 142# Pairs are interesting to highlight only if we are going to end up 143# highlighting a subset (i.e., not the whole line). Otherwise, the highlighting 144# is just useless noise. We can detect this by finding either a matching prefix 145# or suffix (disregarding boring bits like whitespace and colorization). 146sub is_pair_interesting { 147my($a,$pa,$sa,$b,$pb,$sb) =@_; 148my$prefix_a=join('',@$a[0..($pa-1)]); 149my$prefix_b=join('',@$b[0..($pb-1)]); 150my$suffix_a=join('',@$a[($sa+1)..$#$a]); 151my$suffix_b=join('',@$b[($sb+1)..$#$b]); 152 153return$prefix_a!~/^$COLOR*-$BORING*$/|| 154$prefix_b!~/^$COLOR*\+$BORING*$/|| 155$suffix_a!~/^$BORING*$/|| 156$suffix_b!~/^$BORING*$/; 157}