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