From 1f42c726c57fdde280281ff427a24ddb41ee5ea3 Mon Sep 17 00:00:00 2001 From: Jan Wirth Date: Mon, 9 Mar 2020 22:28:00 +0100 Subject: [PATCH] Improve differ --- diff-highlight | 225 +++++++++ diff-so-fancy | 1165 +++++++++++++++++++++++++++++++++++++++++++++ gitconfig | 1 + install.conf.yaml | 3 +- xinitrc | 6 +- zshrc | 4 +- 6 files changed, 1397 insertions(+), 7 deletions(-) create mode 100755 diff-highlight create mode 100755 diff-so-fancy diff --git a/diff-highlight b/diff-highlight new file mode 100755 index 0000000..81bd804 --- /dev/null +++ b/diff-highlight @@ -0,0 +1,225 @@ +#!/usr/bin/perl + +use 5.008; +use warnings FATAL => 'all'; +use strict; + +# Highlight by reversing foreground and background. You could do +# other things like bold or underline if you prefer. +my @OLD_HIGHLIGHT = ( + color_config('color.diff-highlight.oldnormal'), + color_config('color.diff-highlight.oldhighlight', "\x1b[7m"), + color_config('color.diff-highlight.oldreset', "\x1b[27m") +); +my @NEW_HIGHLIGHT = ( + color_config('color.diff-highlight.newnormal', $OLD_HIGHLIGHT[0]), + color_config('color.diff-highlight.newhighlight', $OLD_HIGHLIGHT[1]), + color_config('color.diff-highlight.newreset', $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; + +# Some scripts may not realize that SIGPIPE is being ignored when launching the +# pager--for instance scripts written in Python. +$SIG{PIPE} = 'DEFAULT'; + +while (<>) { + if (!$in_hunk) { + print; + $in_hunk = /^$GRAPH*$COLOR*\@\@ /; + } + elsif (/^$GRAPH*$COLOR*-/) { + push @removed, $_; + } + elsif (/^$GRAPH*$COLOR*\+/) { + push @added, $_; + } + else { + show_hunk(\@removed, \@added); + @removed = (); + @added = (); + + print; + $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) { + local $| = 1; + } +} + +# Flush any queued hunk (this can happen when there is no trailing context in +# the final diff of the input). +show_hunk(\@removed, \@added); + +exit 0; + +# 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) { + print @$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) { + print @$a, @$b; + return; + } + + my @queue; + for (my $i = 0; $i < @$a; $i++) { + my ($rm, $add) = highlight_pair($a->[$i], $b->[$i]); + print $rm; + push @queue, $add; + } + print @queue; +} + +sub highlight_pair { + my @a = split_line(shift); + my @b = split_line(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; + } + } + + if (is_pair_interesting(\@a, $pa, $sa, \@b, $pb, $sb)) { + return highlight_line(\@a, $pa, $sa, \@OLD_HIGHLIGHT), + highlight_line(\@b, $pb, $sb, \@NEW_HIGHLIGHT); + } + 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 utf8::decode($_) ? + map { utf8::encode($_); $_ } + 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*$/; +} diff --git a/diff-so-fancy b/diff-so-fancy new file mode 100755 index 0000000..4df0aad --- /dev/null +++ b/diff-so-fancy @@ -0,0 +1,1165 @@ +#!/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 the correct value for both UNIX and Windows (/dev/null vs nul) + use File::Spec; + + my $NULL = File::Spec->devnull(); + + # 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', "\e[1;31m"), + color_config('color.diff-highlight.oldhighlight', "\e[1;31;48;5;52m"), + "\x1b[27m", + ); + our @NEW_HIGHLIGHT = ( + color_config('color.diff-highlight.newnormal', "\e[1;32m"), + color_config('color.diff-highlight.newhighlight', "\e[1;32;48;5;22m"), + $OLD_HIGHLIGHT[2], + ); + + + + my $RESET = "\x1b[m"; + my $COLOR = qr/\x1b\[[0-9;]*m/; + my $BORING = qr/$COLOR|\s/; + + my @removed; + my @added; + my $in_hunk; + my $graph_indent = 0; + + our $line_cb = sub { print @_ }; + our $flush_cb = sub { local $| = 1 }; + + # Count the visible width of a string, excluding any terminal color sequences. + sub visible_width { + local $_ = shift; + my $ret = 0; + while (length) { + if (s/^$COLOR//) { + # skip colors + } elsif (s/^.//) { + $ret++; + } + } + return $ret; + } + + # Return a substring of $str, omitting $len visible characters from the + # beginning, where terminal color sequences do not count as visible. + sub visible_substr { + my ($str, $len) = @_; + while ($len > 0) { + if ($str =~ s/^$COLOR//) { + next + } + $str =~ s/^.//; + $len--; + } + return $str; + } + + sub handle_line { + my $orig = shift; + local $_ = $orig; + + # match a graph line that begins a commit + if (/^(?:$COLOR?\|$COLOR?[ ])* # zero or more leading "|" with space + $COLOR?\*$COLOR?[ ] # a "*" with its trailing space + (?:$COLOR?\|$COLOR?[ ])* # zero or more trailing "|" + [ ]* # trailing whitespace for merges + /x) { + my $graph_prefix = $&; + + # We must flush before setting graph indent, since the + # new commit may be indented differently from what we + # queued. + flush(); + $graph_indent = visible_width($graph_prefix); + + } elsif ($graph_indent) { + if (length($_) < $graph_indent) { + $graph_indent = 0; + } else { + $_ = visible_substr($_, $graph_indent); + } + } + + if (!$in_hunk) { + $line_cb->($orig); + $in_hunk = /^$COLOR*\@\@ /; + } + elsif (/^$COLOR*-/) { + push @removed, $orig; + } + elsif (/^$COLOR*\+/) { + push @added, $orig; + } + else { + flush(); + $line_cb->($orig); + $in_hunk = /^$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); + @removed = (); + @added = (); + } + + sub highlight_stdin { + while () { + 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>$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); + + # 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; + } + } + + if (is_pair_interesting(\@a, $pa, $sa, \@b, $pb, $sb)) { + return highlight_line(\@a, $pa, $sa, \@OLD_HIGHLIGHT), + highlight_line(\@b, $pb, $sb, \@NEW_HIGHLIGHT); + } + 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 utf8::decode($_) ? + map { utf8::encode($_); $_ } + 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 visible_substr($prefix_a, $graph_indent) !~ /^$COLOR*-$BORING*$/ || + visible_substr($prefix_b, $graph_indent) !~ /^$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.6"; + +################################################################################# + +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 $manually_color_lines = 0; # Usually git/hg colorizes the lines, but for raw patches we use this +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 $ruler_width = git_config("diff-so-fancy.rulerWidth", undef); +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,10})[mK])?/; +my $reset_color = color("reset"); +my $bold = color("bold"); +my $meta_color = ""; + +my ($file_1,$file_2); +my $args = argv(); # Hashref of all the ARGV stuff +my $last_file_seen = ""; +my $last_file_mode = ""; +my $i = 0; +my $in_hunk = 0; +my $columns_to_remove = 0; +my $is_mercurial = 0; +my $color_forced = 0; # Has the color been forced on/off + +# We try and be smart about whether we need to do line coloring, but +# this is an option to force it on/off +if ($args->{color_on}) { + $manually_color_lines = 1; + $color_forced = 1; +} elsif ($args->{color_off}) { + $manually_color_lines = 0; + $color_forced = 1; +} + +# We only process ARGV if we don't have STDIN +if (!$has_stdin) { + 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 = (); + } + }; + + my $line_count = 0; + while (my $line = ) { + # If the very first line of the diff doesn't start with ANSI color we're assuming + # it's a raw patch file, and we have to color the added/removed lines ourself + if (!$color_forced && $line_count == 0 && starts_with_ansi($line)) { + $manually_color_lines = 1; + } + + my $ok = DiffHighlight::handle_line($line); + $line_count++; + } + + 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 || get_config_color("meta"); + + # 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 horizontal_rule($meta_color); + print $meta_color . file_change_string($file_1,$file_2) . "\n"; + print horizontal_rule($meta_color); + } + ######################### + # Look for the filename # + ######################### + # $4 $5 + } elsif ($line =~ /^${ansi_color_regex}diff (-r|--git|--cc) (.+?)(\s|\e|$)/) { + + # Mercurial looks like: diff -r 82e55d328c8c hello.c + if ($4 eq "-r") { + $is_mercurial = 1; + $meta_color ||= get_config_color("meta"); + # Git looks like: diff --git a/diff-so-fancy b/diff-so-fancy + } else { + $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|$)/) { + $meta_color ||= get_config_color("meta"); + + 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 out the top horizontal line of the header + print $reset_color; + print horizontal_rule($meta_color); + + # Mercurial coloring is slightly different so we need to hard reset colors + if ($is_mercurial) { + print $reset_color; + } + + print $meta_color; + 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); + + # Last function has it's own color + my $last_function_color = get_config_color("last_function"); + print "@ $last_file_seen:$start_line \@${bold}${last_function_color}${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 horizontal_rule($meta_color); + 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 ||= get_config_color("meta"); + + 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; +} + +# 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/; + + if ($manually_color_lines) { + if (defined($5) && $5 eq "+") { + my $add_line_color = get_config_color("add_line"); + $line = $add_line_color . $line . $reset_color; + } elsif (defined($5) && $5 eq "-") { + my $remove_line_color = get_config_color("remove_line"); + $line = $remove_line_color . $line . $reset_color; + } + } + + 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 = $ruler_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; +} + +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; +} + +# Borrowed from: https://www.perturb.org/display/1167_Perl_ANSI_colors.html +# String format: '115', '165_bold', '10_on_140', 'reset', 'on_173', 'red', 'white_on_blue' +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|([A-Za-z]+)|$color_map{$1} // $1|eg; + + # 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; +} + +# Get colors used for various output sections (memoized) +{ + my $static_config; + + sub get_config_color { + my $str = shift(); + + my $ret = ""; + if ($static_config->{$str}) { + return $static_config->{$str}; + } + + if ($str eq "meta") { + # Default ANSI yellow + $ret = DiffHighlight::color_config('color.diff.meta', color(11)); + } elsif ($str eq "reset") { + $ret = color("reset"); + } elsif ($str eq "add_line") { + # Default ANSI green + $ret = DiffHighlight::color_config('color.diff.new', color('bold') . color(2)); + } elsif ($str eq "remove_line") { + # Default ANSI red + $ret = DiffHighlight::color_config('color.diff.old', color('bold') . color(1)); + } elsif ($str eq "last_function") { + $ret = DiffHighlight::color_config('color.diff.func', color(146)); + } + + # Cache (memoize) the entry for later + $static_config->{$str} = $ret; + + return $ret; + } +} + +sub starts_with_ansi { + my $str = shift(); + + if ($str =~ /^$ansi_color_regex/) { + return 1; + } else { + return 0; + } +} + +# vim: tabstop=4 shiftwidth=4 noexpandtab autoindent softtabstop=4 diff --git a/gitconfig b/gitconfig index c6f1bec..629b021 100644 --- a/gitconfig +++ b/gitconfig @@ -1,6 +1,7 @@ [core] excludesfile = /home/jan/.gitignore ignorecase = false + pager = diff-so-fancy | less --tabs=4 -RFX [user] name = Jan Wirth email = jan@scalab.app diff --git a/install.conf.yaml b/install.conf.yaml index 59ecdb0..83d1217 100644 --- a/install.conf.yaml +++ b/install.conf.yaml @@ -32,7 +32,8 @@ ~/.local/bin/inc: increment.sh ~/.local/bin/dec: decrement.sh ~/.local/bin/toggleGit: toggleGit.sh - ~/.local/bin/decgen: ../decgen/cli.js + ~/.local/bin/diff-so-fancy: diff-so-fancy + ~/.local/bin/diff-highlight: diff-highlight ~/Library/Preferences/com.apple.Terminal.plist: force: true path: com.apple.Terminal.plist diff --git a/xinitrc b/xinitrc index d4444b1..98ab02b 100644 --- a/xinitrc +++ b/xinitrc @@ -1,6 +1,4 @@ xkbcomp .keymap.xkb $DISPLAY setxkbmap -option caps:escape -option ctrl:swap_lalt_lctl -layout us -variant colemak -xscreensaver -clipit -sudo modprobe -r psmouse -sudo modprobe psmouse +sudo pkill syndaemon +syndaemon -i 0.5 -k -R diff --git a/zshrc b/zshrc index d8ba6ed..ba719e5 100644 --- a/zshrc +++ b/zshrc @@ -210,7 +210,7 @@ if [ -f '/Users/wirthjan/google-cloud-sdk/path.zsh.inc' ]; then . '/Users/wirthj # The next line enables shell command completion for gcloud. if [ -f '/Users/wirthjan/google-cloud-sdk/completion.zsh.inc' ]; then . '/Users/wirthjan/google-cloud-sdk/completion.zsh.inc'; fi -source ~/z/z.sh - alias int="sh ~/.xinitrc" + +git config --global core.pager "diff-so-fancy | less --tabs=4 -RFX"