forked from extern/nix-config
1060 lines
29 KiB
Plaintext
1060 lines
29 KiB
Plaintext
|
#!/usr/bin/env perl
|
||
|
|
||
|
# This chunk of stuff was generated by App::FatPacker. To find the original
|
||
|
# file's code, look for the end of this BEGIN block or the string 'FATPACK'
|
||
|
BEGIN {
|
||
|
my %fatpacked;
|
||
|
|
||
|
$fatpacked{"DiffHighlight.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'DIFFHIGHLIGHT';
|
||
|
package DiffHighlight;
|
||
|
|
||
|
use 5.008;
|
||
|
use warnings FATAL => 'all';
|
||
|
use strict;
|
||
|
use Encode;
|
||
|
|
||
|
# Highlight by reversing foreground and background. You could do
|
||
|
# other things like bold or underline if you prefer.
|
||
|
our @OLD_HIGHLIGHT = (
|
||
|
color_config('color.diff-highlight.oldnormal'),
|
||
|
color_config('color.diff-highlight.oldhighlight', "\x1b[7m"),
|
||
|
"\x1b[27m",
|
||
|
);
|
||
|
our@NEW_HIGHLIGHT = (
|
||
|
color_config('color.diff-highlight.newnormal', $OLD_HIGHLIGHT[0]),
|
||
|
color_config('color.diff-highlight.newhighlight', $OLD_HIGHLIGHT[1]),
|
||
|
$OLD_HIGHLIGHT[2],
|
||
|
);
|
||
|
|
||
|
my $RESET = "\x1b[m";
|
||
|
my $COLOR = qr/\x1b\[[0-9;]*m/;
|
||
|
my $BORING = qr/$COLOR|\s/;
|
||
|
|
||
|
# The patch portion of git log -p --graph should only ever have preceding | and
|
||
|
# not / or \ as merge history only shows up on the commit line.
|
||
|
my $GRAPH = qr/$COLOR?\|$COLOR?\s+/;
|
||
|
|
||
|
my @removed;
|
||
|
my @added;
|
||
|
my $in_hunk;
|
||
|
|
||
|
our $line_cb = sub { print @_ };
|
||
|
our $flush_cb = sub { local $| = 1 };
|
||
|
|
||
|
sub handle_line {
|
||
|
local $_ = shift;
|
||
|
|
||
|
if (!$in_hunk) {
|
||
|
$line_cb->($_);
|
||
|
$in_hunk = /^$GRAPH*$COLOR*\@\@ /;
|
||
|
}
|
||
|
elsif (/^$GRAPH*$COLOR*-/) {
|
||
|
push @removed, $_;
|
||
|
}
|
||
|
elsif (/^$GRAPH*$COLOR*\+/) {
|
||
|
push @added, $_;
|
||
|
}
|
||
|
else {
|
||
|
show_hunk(\@removed, \@added);
|
||
|
@removed = ();
|
||
|
@added = ();
|
||
|
|
||
|
$line_cb->($_);
|
||
|
$in_hunk = /^$GRAPH*$COLOR*[\@ ]/;
|
||
|
}
|
||
|
|
||
|
# Most of the time there is enough output to keep things streaming,
|
||
|
# but for something like "git log -Sfoo", you can get one early
|
||
|
# commit and then many seconds of nothing. We want to show
|
||
|
# that one commit as soon as possible.
|
||
|
#
|
||
|
# Since we can receive arbitrary input, there's no optimal
|
||
|
# place to flush. Flushing on a blank line is a heuristic that
|
||
|
# happens to match git-log output.
|
||
|
if (!length) {
|
||
|
$flush_cb->();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
sub flush {
|
||
|
# Flush any queued hunk (this can happen when there is no trailing
|
||
|
# context in the final diff of the input).
|
||
|
show_hunk(\@removed, \@added);
|
||
|
}
|
||
|
|
||
|
sub highlight_stdin {
|
||
|
while (<STDIN>) {
|
||
|
handle_line($_);
|
||
|
}
|
||
|
flush();
|
||
|
}
|
||
|
|
||
|
# Ideally we would feed the default as a human-readable color to
|
||
|
# git-config as the fallback value. But diff-highlight does
|
||
|
# not otherwise depend on git at all, and there are reports
|
||
|
# of it being used in other settings. Let's handle our own
|
||
|
# fallback, which means we will work even if git can't be run.
|
||
|
sub color_config {
|
||
|
my ($key, $default) = @_;
|
||
|
my $s = `git config --get-color $key 2>/dev/null`;
|
||
|
return length($s) ? $s : $default;
|
||
|
}
|
||
|
|
||
|
sub show_hunk {
|
||
|
my ($a, $b) = @_;
|
||
|
|
||
|
# If one side is empty, then there is nothing to compare or highlight.
|
||
|
if (!@$a || !@$b) {
|
||
|
$line_cb->(@$a, @$b);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
# If we have mismatched numbers of lines on each side, we could try to
|
||
|
# be clever and match up similar lines. But for now we are simple and
|
||
|
# stupid, and only handle multi-line hunks that remove and add the same
|
||
|
# number of lines.
|
||
|
if (@$a != @$b) {
|
||
|
$line_cb->(@$a, @$b);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
my @queue;
|
||
|
for (my $i = 0; $i < @$a; $i++) {
|
||
|
my ($rm, $add) = highlight_pair($a->[$i], $b->[$i]);
|
||
|
$line_cb->($rm);
|
||
|
push @queue, $add;
|
||
|
}
|
||
|
$line_cb->(@queue);
|
||
|
}
|
||
|
|
||
|
sub highlight_pair {
|
||
|
my @a = split_line(shift);
|
||
|
my @b = split_line(shift);
|
||
|
my $opts = shift();
|
||
|
|
||
|
# Find common prefix, taking care to skip any ansi
|
||
|
# color codes.
|
||
|
my $seen_plusminus;
|
||
|
my ($pa, $pb) = (0, 0);
|
||
|
while ($pa < @a && $pb < @b) {
|
||
|
if ($a[$pa] =~ /$COLOR/) {
|
||
|
$pa++;
|
||
|
}
|
||
|
elsif ($b[$pb] =~ /$COLOR/) {
|
||
|
$pb++;
|
||
|
}
|
||
|
elsif ($a[$pa] eq $b[$pb]) {
|
||
|
$pa++;
|
||
|
$pb++;
|
||
|
}
|
||
|
elsif (!$seen_plusminus && $a[$pa] eq '-' && $b[$pb] eq '+') {
|
||
|
$seen_plusminus = 1;
|
||
|
$pa++;
|
||
|
$pb++;
|
||
|
}
|
||
|
else {
|
||
|
last;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# Find common suffix, ignoring colors.
|
||
|
my ($sa, $sb) = ($#a, $#b);
|
||
|
while ($sa >= $pa && $sb >= $pb) {
|
||
|
if ($a[$sa] =~ /$COLOR/) {
|
||
|
$sa--;
|
||
|
}
|
||
|
elsif ($b[$sb] =~ /$COLOR/) {
|
||
|
$sb--;
|
||
|
}
|
||
|
elsif ($a[$sa] eq $b[$sb]) {
|
||
|
$sa--;
|
||
|
$sb--;
|
||
|
}
|
||
|
else {
|
||
|
last;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
my @OLD_COLOR_SPEC = @OLD_HIGHLIGHT;
|
||
|
my @NEW_COLOR_SPEC = @NEW_HIGHLIGHT;
|
||
|
|
||
|
# If we're only highlight the differences temp disable the old/new normal colors
|
||
|
if ($opts->{'only_diff'}) {
|
||
|
$OLD_COLOR_SPEC[0] = '';
|
||
|
$NEW_COLOR_SPEC[0] = '';
|
||
|
}
|
||
|
|
||
|
if (is_pair_interesting(\@a, $pa, $sa, \@b, $pb, $sb)) {
|
||
|
return highlight_line(\@a, $pa, $sa, \@OLD_COLOR_SPEC),
|
||
|
highlight_line(\@b, $pb, $sb, \@NEW_COLOR_SPEC);
|
||
|
}
|
||
|
else {
|
||
|
return join('', @a),
|
||
|
join('', @b);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# we split either by $COLOR or by character. This has the side effect of
|
||
|
# leaving in graph cruft. It works because the graph cruft does not contain "-"
|
||
|
# or "+"
|
||
|
sub split_line {
|
||
|
local $_ = shift;
|
||
|
return eval { $_ = Encode::decode('UTF-8', $_, 1); 1 } ?
|
||
|
map { Encode::encode('UTF-8', $_) }
|
||
|
map { /$COLOR/ ? $_ : (split //) }
|
||
|
split /($COLOR+)/ :
|
||
|
map { /$COLOR/ ? $_ : (split //) }
|
||
|
split /($COLOR+)/;
|
||
|
}
|
||
|
|
||
|
sub highlight_line {
|
||
|
my ($line, $prefix, $suffix, $theme) = @_;
|
||
|
|
||
|
my $start = join('', @{$line}[0..($prefix-1)]);
|
||
|
my $mid = join('', @{$line}[$prefix..$suffix]);
|
||
|
my $end = join('', @{$line}[($suffix+1)..$#$line]);
|
||
|
|
||
|
# If we have a "normal" color specified, then take over the whole line.
|
||
|
# Otherwise, we try to just manipulate the highlighted bits.
|
||
|
if (defined $theme->[0]) {
|
||
|
s/$COLOR//g for ($start, $mid, $end);
|
||
|
chomp $end;
|
||
|
return join('',
|
||
|
$theme->[0], $start, $RESET,
|
||
|
$theme->[1], $mid, $RESET,
|
||
|
$theme->[0], $end, $RESET,
|
||
|
"\n"
|
||
|
);
|
||
|
} else {
|
||
|
return join('',
|
||
|
$start,
|
||
|
$theme->[1], $mid, $theme->[2],
|
||
|
$end
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# Pairs are interesting to highlight only if we are going to end up
|
||
|
# highlighting a subset (i.e., not the whole line). Otherwise, the highlighting
|
||
|
# is just useless noise. We can detect this by finding either a matching prefix
|
||
|
# or suffix (disregarding boring bits like whitespace and colorization).
|
||
|
sub is_pair_interesting {
|
||
|
my ($a, $pa, $sa, $b, $pb, $sb) = @_;
|
||
|
my $prefix_a = join('', @$a[0..($pa-1)]);
|
||
|
my $prefix_b = join('', @$b[0..($pb-1)]);
|
||
|
my $suffix_a = join('', @$a[($sa+1)..$#$a]);
|
||
|
my $suffix_b = join('', @$b[($sb+1)..$#$b]);
|
||
|
|
||
|
return $prefix_a !~ /^$GRAPH*$COLOR*-$BORING*$/ ||
|
||
|
$prefix_b !~ /^$GRAPH*$COLOR*\+$BORING*$/ ||
|
||
|
$suffix_a !~ /^$BORING*$/ ||
|
||
|
$suffix_b !~ /^$BORING*$/;
|
||
|
}
|
||
|
DIFFHIGHLIGHT
|
||
|
|
||
|
s/^ //mg for values %fatpacked;
|
||
|
|
||
|
my $class = 'FatPacked::'.(0+\%fatpacked);
|
||
|
no strict 'refs';
|
||
|
*{"${class}::files"} = sub { keys %{$_[0]} };
|
||
|
|
||
|
if ($] < 5.008) {
|
||
|
*{"${class}::INC"} = sub {
|
||
|
if (my $fat = $_[0]{$_[1]}) {
|
||
|
my $pos = 0;
|
||
|
my $last = length $fat;
|
||
|
return (sub {
|
||
|
return 0 if $pos == $last;
|
||
|
my $next = (1 + index $fat, "\n", $pos) || $last;
|
||
|
$_ .= substr $fat, $pos, $next - $pos;
|
||
|
$pos = $next;
|
||
|
return 1;
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
else {
|
||
|
*{"${class}::INC"} = sub {
|
||
|
if (my $fat = $_[0]{$_[1]}) {
|
||
|
open my $fh, '<', \$fat
|
||
|
or die "FatPacker error loading $_[1] (could be a perl installation issue?)";
|
||
|
return $fh;
|
||
|
}
|
||
|
return;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
unshift @INC, bless \%fatpacked, $class;
|
||
|
} # END OF FATPACK CODE
|
||
|
|
||
|
|
||
|
my $VERSION = "1.2.0";
|
||
|
|
||
|
#################################################################################
|
||
|
|
||
|
use File::Spec; # For catdir
|
||
|
use File::Basename; # For dirname
|
||
|
use Encode; # For handling UTF8 stuff
|
||
|
use Cwd qw(abs_path); # For realpath()
|
||
|
use lib dirname(abs_path(File::Spec->catdir($0))) . "/lib"; # Add the local lib/ to @INC
|
||
|
use DiffHighlight;
|
||
|
|
||
|
use strict;
|
||
|
use warnings FATAL => 'all';
|
||
|
|
||
|
my $remove_file_add_header = 1;
|
||
|
my $remove_file_delete_header = 1;
|
||
|
my $clean_permission_changes = 1;
|
||
|
my $change_hunk_indicators = git_config_boolean("diff-so-fancy.changeHunkIndicators","true");
|
||
|
my $strip_leading_indicators = git_config_boolean("diff-so-fancy.stripLeadingSymbols","true");
|
||
|
my $mark_empty_lines = git_config_boolean("diff-so-fancy.markEmptyLines","true");
|
||
|
my $use_unicode_dash_for_ruler = git_config_boolean("diff-so-fancy.useUnicodeRuler","true");
|
||
|
my $git_strip_prefix = git_config_boolean("diff.noprefix","false");
|
||
|
my $has_stdin = has_stdin();
|
||
|
|
||
|
my $ansi_color_regex = qr/(\e\[([0-9]{1,3}(;[0-9]{1,3}){0,3})[mK])?/;
|
||
|
my $dim_magenta = "\e[38;5;146m";
|
||
|
my $reset_color = "\e[0m";
|
||
|
my $bold = "\e[1m";
|
||
|
my $meta_color = "";
|
||
|
|
||
|
my ($file_1,$file_2);
|
||
|
my $last_file_seen = "";
|
||
|
my $last_file_mode = "";
|
||
|
my $i = 0;
|
||
|
my $in_hunk = 0;
|
||
|
my $columns_to_remove = 0;
|
||
|
|
||
|
# We only process ARGV if we don't have STDIN
|
||
|
if (!$has_stdin) {
|
||
|
my $args = argv();
|
||
|
|
||
|
if ($args->{v} || $args->{version}) {
|
||
|
die(version());
|
||
|
} elsif ($args->{'set-defaults'}) {
|
||
|
my $ok = set_defaults();
|
||
|
} elsif ($args->{colors}) {
|
||
|
# We print this to STDOUT so we can redirect to bash to auto-set the colors
|
||
|
print get_default_colors();
|
||
|
exit;
|
||
|
} elsif (!%$args || $args->{help} || $args->{h}) {
|
||
|
my $first = check_first_run();
|
||
|
|
||
|
if (!$first) {
|
||
|
die(usage());
|
||
|
}
|
||
|
} else {
|
||
|
die("Missing input on STDIN\n");
|
||
|
}
|
||
|
} else {
|
||
|
# Check to see if were using default settings
|
||
|
check_first_run();
|
||
|
|
||
|
my @lines;
|
||
|
local $DiffHighlight::line_cb = sub {
|
||
|
push(@lines,@_);
|
||
|
|
||
|
my $last_line = $lines[-1];
|
||
|
|
||
|
# Buffer X lines before we try and output anything
|
||
|
# Also make sure we're sending enough data to d-s-f to do it's magic.
|
||
|
# Certain things require a look-ahead line or two to function so
|
||
|
# we make sure we don't break on those sections prematurely
|
||
|
if (@lines > 24 && ($last_line !~ /^${ansi_color_regex}(---|index|old mode|similarity index|rename (from|to))/)) {
|
||
|
do_dsf_stuff(\@lines);
|
||
|
@lines = ();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
while (my $line = <STDIN>) {
|
||
|
my $ok = DiffHighlight::handle_line($line);
|
||
|
}
|
||
|
|
||
|
DiffHighlight::flush();
|
||
|
do_dsf_stuff(\@lines);
|
||
|
}
|
||
|
|
||
|
#################################################################################
|
||
|
|
||
|
sub do_dsf_stuff {
|
||
|
my $input = shift();
|
||
|
|
||
|
#print STDERR "START -------------------------------------------------\n";
|
||
|
#print STDERR join("",@$input);
|
||
|
#print STDERR "END ---------------------------------------------------\n";
|
||
|
|
||
|
while (my $line = shift(@$input)) {
|
||
|
######################################################
|
||
|
# Pre-process the line before we do any other markup #
|
||
|
######################################################
|
||
|
|
||
|
# If the first line of the input is a blank line, skip that
|
||
|
if ($i == 0 && $line =~ /^\s*$/) {
|
||
|
next;
|
||
|
}
|
||
|
|
||
|
######################
|
||
|
# End pre-processing #
|
||
|
######################
|
||
|
|
||
|
#######################################################################
|
||
|
|
||
|
####################################################################
|
||
|
# Look for git index and replace it horizontal line (header later) #
|
||
|
####################################################################
|
||
|
if ($line =~ /^${ansi_color_regex}index /) {
|
||
|
# Print the line color and then the actual line
|
||
|
$meta_color = $1 || DiffHighlight::color_config('color.diff.meta',"\e[38;5;11m");
|
||
|
print horizontal_rule($meta_color);
|
||
|
|
||
|
# Get the next line without incrementing counter while loop
|
||
|
my $next = $input->[0] || "";
|
||
|
my ($file_1,$file_2);
|
||
|
|
||
|
# The line immediately after the "index" line should be the --- file line
|
||
|
# If it's not it's an empty file add/delete
|
||
|
if ($next !~ /^$ansi_color_regex(---|Binary files)/) {
|
||
|
|
||
|
# We fake out the file names since it's a raw add/delete
|
||
|
if ($last_file_mode eq "add") {
|
||
|
$file_1 = "/dev/null";
|
||
|
$file_2 = $last_file_seen;
|
||
|
} elsif ($last_file_mode eq "delete") {
|
||
|
$file_1 = $last_file_seen;
|
||
|
$file_2 = "/dev/null";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($file_1 && $file_2) {
|
||
|
print $meta_color . file_change_string($file_1,$file_2) . "\n";
|
||
|
print horizontal_rule($meta_color);
|
||
|
}
|
||
|
#########################
|
||
|
# Look for the filename #
|
||
|
#########################
|
||
|
} elsif ($line =~ /^${ansi_color_regex}diff --(git|cc) (.*?)(\s|\e|$)/) {
|
||
|
$last_file_seen = $5;
|
||
|
$last_file_seen =~ s|^\w/||; # Remove a/ (and handle diff.mnemonicPrefix).
|
||
|
$in_hunk = 0;
|
||
|
########################################
|
||
|
# Find the first file: --- a/README.md #
|
||
|
########################################
|
||
|
} elsif (!$in_hunk && $line =~ /^$ansi_color_regex--- (\w\/)?(.+?)(\e|\t|$)/) {
|
||
|
if ($git_strip_prefix) {
|
||
|
my $file_dir = $4 || "";
|
||
|
$file_1 = $file_dir . $5;
|
||
|
} else {
|
||
|
$file_1 = $5;
|
||
|
}
|
||
|
|
||
|
# Find the second file on the next line: +++ b/README.md
|
||
|
my $next = shift(@$input);
|
||
|
$next =~ /^$ansi_color_regex\+\+\+ (\w\/)?(.+?)(\e|\t|$)/;
|
||
|
if ($1) {
|
||
|
print $1; # Print out whatever color we're using
|
||
|
}
|
||
|
if ($git_strip_prefix) {
|
||
|
my $file_dir = $4 || "";
|
||
|
$file_2 = $file_dir . $5;
|
||
|
} else {
|
||
|
$file_2 = $5;
|
||
|
}
|
||
|
|
||
|
if ($file_2 ne "/dev/null") {
|
||
|
$last_file_seen = $file_2;
|
||
|
}
|
||
|
|
||
|
print file_change_string($file_1,$file_2) . "\n";
|
||
|
|
||
|
# Print out the bottom horizontal line of the header
|
||
|
print horizontal_rule($meta_color);
|
||
|
########################################
|
||
|
# Check for "@@ -3,41 +3,63 @@" syntax #
|
||
|
########################################
|
||
|
} elsif ($change_hunk_indicators && $line =~ /^${ansi_color_regex}(@@@* .+? @@@*)(.*)/) {
|
||
|
$in_hunk = 1;
|
||
|
my $hunk_header = $4;
|
||
|
my $remain = bleach_text($5);
|
||
|
|
||
|
# The number of colums to remove (1 or 2) is based on how many commas in the hunk header
|
||
|
$columns_to_remove = (char_count(",",$hunk_header)) - 1;
|
||
|
# On single line removes there is NO comma in the hunk so we force one
|
||
|
if ($columns_to_remove <= 0) {
|
||
|
$columns_to_remove = 1;
|
||
|
}
|
||
|
|
||
|
if ($1) {
|
||
|
print $1; # Print out whatever color we're using
|
||
|
}
|
||
|
|
||
|
my ($orig_offset, $orig_count, $new_offset, $new_count) = parse_hunk_header($hunk_header);
|
||
|
$last_file_seen = basename($last_file_seen);
|
||
|
|
||
|
# Figure out the start line
|
||
|
my $start_line = start_line_calc($new_offset,$new_count);
|
||
|
print "@ $last_file_seen:$start_line \@${bold}${dim_magenta}${remain}${reset_color}\n";
|
||
|
###################################
|
||
|
# Remove any new file permissions #
|
||
|
###################################
|
||
|
} elsif ($remove_file_add_header && $line =~ /^${ansi_color_regex}.*new file mode/) {
|
||
|
# Don't print the line (i.e. remove it from the output);
|
||
|
$last_file_mode = "add";
|
||
|
######################################
|
||
|
# Remove any delete file permissions #
|
||
|
######################################
|
||
|
} elsif ($remove_file_delete_header && $line =~ /^${ansi_color_regex}deleted file mode/) {
|
||
|
# Don't print the line (i.e. remove it from the output);
|
||
|
$last_file_mode = "delete";
|
||
|
################################
|
||
|
# Look for binary file changes #
|
||
|
################################
|
||
|
} elsif ($line =~ /^Binary files (\w\/)?(.+?) and (\w\/)?(.+?) differ/) {
|
||
|
my $change = file_change_string($2,$4);
|
||
|
print "$meta_color$change (binary)\n";
|
||
|
print horizontal_rule($meta_color);
|
||
|
#####################################################
|
||
|
# Check if we're changing the permissions of a file #
|
||
|
#####################################################
|
||
|
} elsif ($clean_permission_changes && $line =~ /^${ansi_color_regex}old mode (\d+)/) {
|
||
|
my ($old_mode) = $4;
|
||
|
my $next = shift(@$input);
|
||
|
|
||
|
if ($1) {
|
||
|
print $1; # Print out whatever color we're using
|
||
|
}
|
||
|
|
||
|
my ($new_mode) = $next =~ m/new mode (\d+)/;
|
||
|
print "$last_file_seen changed file mode from $old_mode to $new_mode\n";
|
||
|
|
||
|
###############
|
||
|
# File rename #
|
||
|
###############
|
||
|
} elsif ($line =~ /^${ansi_color_regex}similarity index (\d+)%/) {
|
||
|
my $simil = $4;
|
||
|
|
||
|
# If it's a move with content change we ignore this and the next two lines
|
||
|
if ($simil != 100) {
|
||
|
shift(@$input);
|
||
|
shift(@$input);
|
||
|
next;
|
||
|
}
|
||
|
|
||
|
my $next = shift(@$input);
|
||
|
my ($file1) = $next =~ /rename from (.+)/;
|
||
|
|
||
|
$next = shift(@$input);
|
||
|
my ($file2) = $next =~ /rename to (.+)/;
|
||
|
|
||
|
if ($file1 && $file2) {
|
||
|
# We may not have extracted this yet, so we pull from the config if not
|
||
|
$meta_color ||= DiffHighlight::color_config('color.diff.meta',"\e[38;5;11m");
|
||
|
|
||
|
my $change = file_change_string($file1,$file2);
|
||
|
|
||
|
print horizontal_rule($meta_color);
|
||
|
print $meta_color . $change . "\n";
|
||
|
print horizontal_rule($meta_color);
|
||
|
}
|
||
|
|
||
|
$i += 3; # We've consumed three lines
|
||
|
next;
|
||
|
#####################################
|
||
|
# Just a regular line, print it out #
|
||
|
#####################################
|
||
|
} else {
|
||
|
# Mark empty line with a red/green box indicating addition/removal
|
||
|
if ($mark_empty_lines) {
|
||
|
$line = mark_empty_line($line);
|
||
|
}
|
||
|
|
||
|
# Remove the correct number of leading " " or "+" or "-"
|
||
|
if ($strip_leading_indicators) {
|
||
|
$line = strip_leading_indicators($line,$columns_to_remove);
|
||
|
}
|
||
|
print $line;
|
||
|
}
|
||
|
|
||
|
$i++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
######################################################################################################
|
||
|
# End regular code, begin functions
|
||
|
######################################################################################################
|
||
|
|
||
|
# Courtesy of github.com/git/git/blob/ab5d01a/git-add--interactive.perl#L798-L805
|
||
|
sub parse_hunk_header {
|
||
|
my ($line) = @_;
|
||
|
my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = $line =~ /^\@\@+(?: -(\d+)(?:,(\d+))?)+ \+(\d+)(?:,(\d+))? \@\@+/;
|
||
|
$o_cnt = 1 unless defined $o_cnt;
|
||
|
$n_cnt = 1 unless defined $n_cnt;
|
||
|
return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
|
||
|
}
|
||
|
|
||
|
# Mark the first char of an empty line
|
||
|
sub mark_empty_line {
|
||
|
my $line = shift();
|
||
|
|
||
|
my $reset_color = "\e\\[0?m";
|
||
|
my $reset_escape = "\e\[m";
|
||
|
my $invert_color = "\e\[7m";
|
||
|
|
||
|
$line =~ s/^($ansi_color_regex)[+-]$reset_color\s*$/$invert_color$1 $reset_escape\n/;
|
||
|
|
||
|
return $line;
|
||
|
}
|
||
|
|
||
|
# String to boolean
|
||
|
sub boolean {
|
||
|
my $str = shift();
|
||
|
$str = trim($str);
|
||
|
|
||
|
if ($str eq "" || $str =~ /^(no|false|0)$/i) {
|
||
|
return 0;
|
||
|
} else {
|
||
|
return 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# Memoize getting the git config
|
||
|
{
|
||
|
my $static_config;
|
||
|
|
||
|
sub git_config_raw {
|
||
|
if ($static_config) {
|
||
|
# If we already have the config return that
|
||
|
return $static_config;
|
||
|
}
|
||
|
|
||
|
my $cmd = "git config --list";
|
||
|
my @out = `$cmd`;
|
||
|
|
||
|
$static_config = \@out;
|
||
|
|
||
|
return \@out;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# Fetch a textual item from the git config
|
||
|
sub git_config {
|
||
|
my $search_key = lc($_[0] || "");
|
||
|
my $default_value = lc($_[1] || "");
|
||
|
|
||
|
my $out = git_config_raw();
|
||
|
|
||
|
# If we're in a unit test, use the default (don't read the users config)
|
||
|
if (in_unit_test()) {
|
||
|
return $default_value;
|
||
|
}
|
||
|
|
||
|
my $raw = {};
|
||
|
foreach my $line (@$out) {
|
||
|
if ($line =~ /=/) {
|
||
|
my ($key,$value) = split("=",$line,2);
|
||
|
$value =~ s/\s+$//;
|
||
|
$raw->{$key} = $value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# If we're given a search key return that, else return the hash
|
||
|
if ($search_key) {
|
||
|
return $raw->{$search_key} || $default_value;
|
||
|
} else {
|
||
|
return $raw;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# Fetch a boolean item from the git config
|
||
|
sub git_config_boolean {
|
||
|
my $search_key = lc($_[0] || "");
|
||
|
my $default_value = lc($_[1] || 0); # Default to false
|
||
|
|
||
|
# If we're in a unit test, use the default (don't read the users config)
|
||
|
if (in_unit_test()) {
|
||
|
return boolean($default_value);
|
||
|
}
|
||
|
|
||
|
my $result = git_config($search_key,$default_value);
|
||
|
my $ret = boolean($result);
|
||
|
|
||
|
return $ret;
|
||
|
}
|
||
|
|
||
|
# Check if we're inside of BATS
|
||
|
sub in_unit_test {
|
||
|
if ($ENV{BATS_CWD}) {
|
||
|
return 1;
|
||
|
} else {
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
sub get_less_charset {
|
||
|
my @less_char_vars = ("LESSCHARSET", "LESSCHARDEF", "LC_ALL", "LC_CTYPE", "LANG");
|
||
|
foreach (@less_char_vars) {
|
||
|
return $ENV{$_} if defined $ENV{$_};
|
||
|
}
|
||
|
|
||
|
return "";
|
||
|
}
|
||
|
|
||
|
sub should_print_unicode {
|
||
|
if (-t STDOUT) {
|
||
|
# Always print unicode chars if we're not piping stuff, e.g. to less(1)
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
# Otherwise, assume we're piping to less(1)
|
||
|
my $less_charset = get_less_charset();
|
||
|
if ($less_charset =~ /utf-?8/i) {
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
# Return git config as a hash
|
||
|
sub get_git_config_hash {
|
||
|
my $out = git_config_raw();
|
||
|
|
||
|
my %hash;
|
||
|
foreach my $line (@$out) {
|
||
|
my ($key,$value) = split("=",$line,2);
|
||
|
|
||
|
if ($key && $value) {
|
||
|
$value =~ s/\s+$//;
|
||
|
my @path = split(/\./,$key);
|
||
|
my $last = pop @path;
|
||
|
my $p = \%hash;
|
||
|
|
||
|
# Build the tree for each section
|
||
|
$p = $p->{$_} ||= {} for @path;
|
||
|
$p->{$last} = $value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return \%hash;
|
||
|
}
|
||
|
|
||
|
# Try and be smart about what line the diff hunk starts on
|
||
|
sub start_line_calc {
|
||
|
my ($line_num,$diff_context) = @_;
|
||
|
my $ret;
|
||
|
|
||
|
if ($line_num == 0 && $diff_context == 0) {
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
# Git defaults to three lines of context
|
||
|
my $default_context_lines = 3;
|
||
|
# Three lines on either side, and the line itself = 7
|
||
|
my $expected_context = ($default_context_lines * 2 + 1);
|
||
|
|
||
|
# The first three lines
|
||
|
if ($line_num == 1 && $diff_context < $expected_context) {
|
||
|
$ret = $diff_context - $default_context_lines;
|
||
|
} else {
|
||
|
$ret = $line_num + $default_context_lines;
|
||
|
}
|
||
|
|
||
|
if ($ret < 1) {
|
||
|
$ret = 1;
|
||
|
}
|
||
|
|
||
|
return $ret;
|
||
|
}
|
||
|
|
||
|
# Remove + or - at the beginning of the lines
|
||
|
sub strip_leading_indicators {
|
||
|
my $line = shift(); # Array passed in by reference
|
||
|
my $columns_to_remove = shift(); # Don't remove any lines by default
|
||
|
|
||
|
if ($columns_to_remove == 0) {
|
||
|
return $line; # Nothing to do
|
||
|
}
|
||
|
|
||
|
$line =~ s/^(${ansi_color_regex})[ +-]{${columns_to_remove}}/$1/;
|
||
|
|
||
|
return $line;
|
||
|
}
|
||
|
|
||
|
# Count the number of a given char in a string
|
||
|
sub char_count {
|
||
|
my ($needle,$str) = @_;
|
||
|
my $len = length($str);
|
||
|
my $ret = 0;
|
||
|
|
||
|
for (my $i = 0; $i < $len; $i++) {
|
||
|
my $found = substr($str,$i,1);
|
||
|
|
||
|
if ($needle eq $found) { $ret++; }
|
||
|
}
|
||
|
|
||
|
return $ret;
|
||
|
}
|
||
|
|
||
|
# Remove all ANSI codes from a string
|
||
|
sub bleach_text {
|
||
|
my $str = shift();
|
||
|
$str =~ s/\e\[\d*(;\d+)*m//mg;
|
||
|
|
||
|
return $str;
|
||
|
}
|
||
|
|
||
|
# Remove all trailing and leading spaces
|
||
|
sub trim {
|
||
|
my $s = shift();
|
||
|
if (!$s) { return ""; }
|
||
|
$s =~ s/^\s*|\s*$//g;
|
||
|
|
||
|
return $s;
|
||
|
}
|
||
|
|
||
|
# Print a line of em-dash or line-drawing chars the full width of the screen
|
||
|
sub horizontal_rule {
|
||
|
my $color = $_[0] || "";
|
||
|
my $width = `tput cols`;
|
||
|
|
||
|
if (is_windows()) {
|
||
|
$width--;
|
||
|
}
|
||
|
|
||
|
# em-dash http://www.fileformat.info/info/unicode/char/2014/index.htm
|
||
|
#my $dash = "\x{2014}";
|
||
|
# BOX DRAWINGS LIGHT HORIZONTAL http://www.fileformat.info/info/unicode/char/2500/index.htm
|
||
|
my $dash;
|
||
|
if ($use_unicode_dash_for_ruler && should_print_unicode()) {
|
||
|
$dash = Encode::encode('UTF-8', "\x{2500}");
|
||
|
} else {
|
||
|
$dash = "-";
|
||
|
}
|
||
|
|
||
|
# Draw the line
|
||
|
my $ret = $color . ($dash x $width) . "\n";
|
||
|
|
||
|
return $ret;
|
||
|
}
|
||
|
|
||
|
sub file_change_string {
|
||
|
my $file_1 = shift();
|
||
|
my $file_2 = shift();
|
||
|
|
||
|
# If they're the same it's a modify
|
||
|
if ($file_1 eq $file_2) {
|
||
|
return "modified: $file_1";
|
||
|
# If the first is /dev/null it's a new file
|
||
|
} elsif ($file_1 eq "/dev/null") {
|
||
|
my $add_color = $DiffHighlight::NEW_HIGHLIGHT[1];
|
||
|
return "added: $add_color$file_2$reset_color";
|
||
|
# If the second is /dev/null it's a deletion
|
||
|
} elsif ($file_2 eq "/dev/null") {
|
||
|
my $del_color = $DiffHighlight::OLD_HIGHLIGHT[1];
|
||
|
return "deleted: $del_color$file_1$reset_color";
|
||
|
# If the files aren't the same it's a rename
|
||
|
} elsif ($file_1 ne $file_2) {
|
||
|
my ($old, $new) = DiffHighlight::highlight_pair($file_1,$file_2,{only_diff => 1});
|
||
|
$old = trim($old);
|
||
|
$new = trim($new);
|
||
|
|
||
|
# highlight_pair resets the colors, but we want it to be the meta color
|
||
|
$old =~ s/(\e0?\[m)/$1$meta_color/g;
|
||
|
$new =~ s/(\e0?\[m)/$1$meta_color/g;
|
||
|
|
||
|
return "renamed: $old to $new";
|
||
|
# Something we haven't thought of yet
|
||
|
} else {
|
||
|
return "$file_1 -> $file_2";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# Check to see if STDIN is connected to an interactive terminal
|
||
|
sub has_stdin {
|
||
|
my $i = -t STDIN;
|
||
|
my $ret = int(!$i);
|
||
|
|
||
|
return $ret;
|
||
|
}
|
||
|
|
||
|
# We use this instead of Getopt::Long because it's faster and we're not parsing any
|
||
|
# crazy arguments
|
||
|
# Borrowed from: https://www.perturb.org/display/1153_Perl_Quick_extract_variables_from_ARGV.html
|
||
|
sub argv {
|
||
|
my $ret = {};
|
||
|
|
||
|
for (my $i = 0; $i < scalar(@ARGV); $i++) {
|
||
|
# If the item starts with "-" it's a key
|
||
|
if ((my ($key) = $ARGV[$i] =~ /^--?([a-zA-Z_]\w*)/) && ($ARGV[$i] !~ /^-\w\w/)) {
|
||
|
# If the next item does not start with "--" it's the value for this item
|
||
|
if (defined($ARGV[$i + 1]) && ($ARGV[$i + 1] !~ /^--?\D/)) {
|
||
|
$ret->{$key} = $ARGV[$i + 1];
|
||
|
# Bareword like --verbose with no options
|
||
|
} else {
|
||
|
$ret->{$key}++;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# We're looking for a certain item
|
||
|
if ($_[0]) { return $ret->{$_[0]}; }
|
||
|
|
||
|
return $ret;
|
||
|
}
|
||
|
|
||
|
# Output the command line usage for d-s-f
|
||
|
sub usage {
|
||
|
my $out = color("white_bold") . version() . color("reset") . "\n";
|
||
|
|
||
|
$out .= "Usage:
|
||
|
|
||
|
git diff --color | diff-so-fancy # Use d-s-f on one diff
|
||
|
diff-so-fancy --colors # View the commands to set the recommended colors
|
||
|
diff-so-fancy --set-defaults # Configure git-diff to use diff-so-fancy and suggested colors
|
||
|
|
||
|
# Configure git to use d-s-f for *all* diff operations
|
||
|
git config --global core.pager \"diff-so-fancy | less --tabs=4 -RFX\"\n";
|
||
|
|
||
|
return $out;
|
||
|
}
|
||
|
|
||
|
sub get_default_colors {
|
||
|
my $out = "# Recommended default colors for diff-so-fancy\n";
|
||
|
$out .= "# --------------------------------------------\n";
|
||
|
$out .= 'git config --global color.ui true
|
||
|
|
||
|
git config --global color.diff-highlight.oldNormal "red bold"
|
||
|
git config --global color.diff-highlight.oldHighlight "red bold 52"
|
||
|
git config --global color.diff-highlight.newNormal "green bold"
|
||
|
git config --global color.diff-highlight.newHighlight "green bold 22"
|
||
|
|
||
|
git config --global color.diff.meta "yellow"
|
||
|
git config --global color.diff.frag "magenta bold"
|
||
|
git config --global color.diff.commit "yellow bold"
|
||
|
git config --global color.diff.old "red bold"
|
||
|
git config --global color.diff.new "green bold"
|
||
|
git config --global color.diff.whitespace "red reverse"
|
||
|
';
|
||
|
|
||
|
return $out;
|
||
|
}
|
||
|
|
||
|
# Output the current version string
|
||
|
sub version {
|
||
|
my $ret = "Diff-so-fancy: https://github.com/so-fancy/diff-so-fancy\n";
|
||
|
$ret .= "Version : $VERSION\n";
|
||
|
|
||
|
return $ret;
|
||
|
}
|
||
|
|
||
|
# Feed the raw git input through diff-highlight to get line level highlights
|
||
|
sub filter_stdin_through_diff_highlight {
|
||
|
my @dh_lines;
|
||
|
|
||
|
# Have DH put the lines it's modified in an array
|
||
|
local $DiffHighlight::line_cb = sub { push(@dh_lines,@_) };
|
||
|
|
||
|
while (my $line = <STDIN>) {
|
||
|
my $ok = DiffHighlight::handle_line($line);
|
||
|
}
|
||
|
|
||
|
DiffHighlight::flush();
|
||
|
|
||
|
return @dh_lines;
|
||
|
}
|
||
|
|
||
|
sub is_windows {
|
||
|
if ($^O eq 'MSWin32' or $^O eq 'dos' or $^O eq 'os2' or $^O eq 'cygwin' or $^O eq 'msys') {
|
||
|
return 1;
|
||
|
} else {
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# Return value is whether this is the first time they've run d-s-f
|
||
|
sub check_first_run {
|
||
|
my $ret = 0;
|
||
|
|
||
|
# If first-run is not set, or it's set to "true"
|
||
|
my $first_run = git_config_boolean('diff-so-fancy.first-run');
|
||
|
# See if they're previously set SOME diff-highlight colors
|
||
|
my $has_dh_colors = git_config_boolean('color.diff-highlight.oldnormal') || git_config_boolean('color.diff-highlight.newnormal');
|
||
|
|
||
|
#$first_run = 1; $has_dh_colors = 0;
|
||
|
|
||
|
if (!$first_run || $has_dh_colors) {
|
||
|
return 0;
|
||
|
} else {
|
||
|
print "This appears to be the first time you've run diff-so-fancy, please note\n";
|
||
|
print "that the default git colors are not ideal. Diff-so-fancy recommends the\n";
|
||
|
print "following colors.\n\n";
|
||
|
|
||
|
print get_default_colors();
|
||
|
|
||
|
# Set the first run flag to false
|
||
|
my $cmd = 'git config --global diff-so-fancy.first-run false';
|
||
|
system($cmd);
|
||
|
|
||
|
exit;
|
||
|
}
|
||
|
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
sub set_defaults {
|
||
|
my $color_config = get_default_colors();
|
||
|
my $git_config = 'git config --global core.pager "diff-so-fancy | less --tabs=4 -RFX"';
|
||
|
my $first_cmd = 'git config --global diff-so-fancy.first-run false';
|
||
|
|
||
|
my @cmds = split(/\n/,$color_config);
|
||
|
push(@cmds,$git_config);
|
||
|
push(@cmds,$first_cmd);
|
||
|
|
||
|
# Remove all comments from the commands
|
||
|
foreach my $x (@cmds) {
|
||
|
$x =~ s/#.*//g;
|
||
|
}
|
||
|
|
||
|
# Remove any empty commands
|
||
|
@cmds = grep($_,@cmds);
|
||
|
|
||
|
foreach my $cmd (@cmds) {
|
||
|
system($cmd);
|
||
|
my $exit = ($? >> 8);
|
||
|
|
||
|
if ($exit != 0) {
|
||
|
die("Error running: '$cmd' (error #18941)\n");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
# String format: '115', '165_bold', '10_on_140', 'reset', 'on_173', 'red_bold', 'red_on_blue', 'blink', 'italic'
|
||
|
sub color {
|
||
|
my $str = shift();
|
||
|
|
||
|
# No string sent in, so we just reset
|
||
|
if (!length($str) || $str eq 'reset') { return "\e[0m"; }
|
||
|
|
||
|
# Some predefined colors
|
||
|
my %color_map = qw(red 160 blue 21 green 34 yellow 226 orange 214 purple 93 white 15 black 0);
|
||
|
$str =~ s/$_/$color_map{$_}/g for keys %color_map;
|
||
|
|
||
|
# Get foreground/background and any commands
|
||
|
my ($fc,$cmd) = $str =~ /^(\d+)?_?(\w+)?/g;
|
||
|
my ($bc) = $str =~ /on_?(\d+)$/g;
|
||
|
|
||
|
# Some predefined commands
|
||
|
my %cmd_map = qw(bold 1 italic 3 underline 4 blink 5 inverse 7);
|
||
|
my $cmd_num = $cmd_map{$cmd || 0};
|
||
|
|
||
|
my $ret = '';
|
||
|
if ($cmd_num) { $ret .= "\e[${cmd_num}m"; }
|
||
|
if (defined($fc)) { $ret .= "\e[38;5;${fc}m"; }
|
||
|
if (defined($bc)) { $ret .= "\e[48;5;${bc}m"; }
|
||
|
|
||
|
return $ret;
|
||
|
}
|
||
|
|
||
|
# vim: tabstop=4 shiftwidth=4 noexpandtab autoindent softtabstop=4
|