dotfiles

config files for my linux setup
git clone git://edryd.org/dotfiles
Log | Files | Refs | README

diff-highlight (5399B)


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