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@removed; 14my@added; 15my$in_hunk; 16 17while(<>) { 18if(!$in_hunk) { 19print; 20$in_hunk=/^$COLOR*\@/; 21} 22elsif(/^$COLOR*-/) { 23push@removed,$_; 24} 25elsif(/^$COLOR*\+/) { 26push@added,$_; 27} 28else{ 29 show_hunk(\@removed, \@added); 30@removed= (); 31@added= (); 32 33print; 34$in_hunk=/^$COLOR*[\@ ]/; 35} 36 37# Most of the time there is enough output to keep things streaming, 38# but for something like "git log -Sfoo", you can get one early 39# commit and then many seconds of nothing. We want to show 40# that one commit as soon as possible. 41# 42# Since we can receive arbitrary input, there's no optimal 43# place to flush. Flushing on a blank line is a heuristic that 44# happens to match git-log output. 45if(!length) { 46local$| =1; 47} 48} 49 50# Flush any queued hunk (this can happen when there is no trailing context in 51# the final diff of the input). 52show_hunk(\@removed, \@added); 53 54exit0; 55 56sub show_hunk { 57my($a,$b) =@_; 58 59# If one side is empty, then there is nothing to compare or highlight. 60if(!@$a|| !@$b) { 61print@$a,@$b; 62return; 63} 64 65# If we have mismatched numbers of lines on each side, we could try to 66# be clever and match up similar lines. But for now we are simple and 67# stupid, and only handle multi-line hunks that remove and add the same 68# number of lines. 69if(@$a!=@$b) { 70print@$a,@$b; 71return; 72} 73 74my@queue; 75for(my$i=0;$i<@$a;$i++) { 76my($rm,$add) = highlight_pair($a->[$i],$b->[$i]); 77print$rm; 78push@queue,$add; 79} 80print@queue; 81} 82 83sub highlight_pair { 84my@a= split_line(shift); 85my@b= split_line(shift); 86 87# Find common prefix, taking care to skip any ansi 88# color codes. 89my$seen_plusminus; 90my($pa,$pb) = (0,0); 91while($pa<@a&&$pb<@b) { 92if($a[$pa] =~/$COLOR/) { 93$pa++; 94} 95elsif($b[$pb] =~/$COLOR/) { 96$pb++; 97} 98elsif($a[$pa]eq$b[$pb]) { 99$pa++; 100$pb++; 101} 102elsif(!$seen_plusminus&&$a[$pa]eq'-'&&$b[$pb]eq'+') { 103$seen_plusminus=1; 104$pa++; 105$pb++; 106} 107else{ 108last; 109} 110} 111 112# Find common suffix, ignoring colors. 113my($sa,$sb) = ($#a,$#b); 114while($sa>=$pa&&$sb>=$pb) { 115if($a[$sa] =~/$COLOR/) { 116$sa--; 117} 118elsif($b[$sb] =~/$COLOR/) { 119$sb--; 120} 121elsif($a[$sa]eq$b[$sb]) { 122$sa--; 123$sb--; 124} 125else{ 126last; 127} 128} 129 130if(is_pair_interesting(\@a,$pa,$sa, \@b,$pb,$sb)) { 131return highlight_line(\@a,$pa,$sa), 132 highlight_line(\@b,$pb,$sb); 133} 134else{ 135returnjoin('',@a), 136join('',@b); 137} 138} 139 140sub split_line { 141local$_=shift; 142returnmap{/$COLOR/?$_: (split//) } 143split/($COLOR*)/; 144} 145 146sub highlight_line { 147my($line,$prefix,$suffix) =@_; 148 149returnjoin('', 150@{$line}[0..($prefix-1)], 151$HIGHLIGHT, 152@{$line}[$prefix..$suffix], 153$UNHIGHLIGHT, 154@{$line}[($suffix+1)..$#$line] 155); 156} 157 158# Pairs are interesting to highlight only if we are going to end up 159# highlighting a subset (i.e., not the whole line). Otherwise, the highlighting 160# is just useless noise. We can detect this by finding either a matching prefix 161# or suffix (disregarding boring bits like whitespace and colorization). 162sub is_pair_interesting { 163my($a,$pa,$sa,$b,$pb,$sb) =@_; 164my$prefix_a=join('',@$a[0..($pa-1)]); 165my$prefix_b=join('',@$b[0..($pb-1)]); 166my$suffix_a=join('',@$a[($sa+1)..$#$a]); 167my$suffix_b=join('',@$b[($sb+1)..$#$b]); 168 169return$prefix_a!~/^$COLOR*-$BORING*$/|| 170$prefix_b!~/^$COLOR*\+$BORING*$/|| 171$suffix_a!~/^$BORING*$/|| 172$suffix_b!~/^$BORING*$/; 173}