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