Code

t4020: don't use grep -a
[git.git] / git-add--interactive.perl
index 23fd2f741b8f2aec35f65239499b967aeb4499d2..a0a81f134a6288dfc1d87431698f29597ed5e488 100755 (executable)
@@ -1,6 +1,42 @@
 #!/usr/bin/perl -w
 
 use strict;
+use Git;
+
+my $repo = Git->repository();
+
+my $menu_use_color = $repo->get_colorbool('color.interactive');
+my ($prompt_color, $header_color, $help_color) =
+       $menu_use_color ? (
+               $repo->get_color('color.interactive.prompt', 'bold blue'),
+               $repo->get_color('color.interactive.header', 'bold'),
+               $repo->get_color('color.interactive.help', 'red bold'),
+       ) : ();
+
+my $diff_use_color = $repo->get_colorbool('color.diff');
+my ($fraginfo_color) =
+       $diff_use_color ? (
+               $repo->get_color('color.diff.frag', 'cyan'),
+       ) : ();
+
+my $normal_color = $repo->get_color("", "reset");
+
+sub colored {
+       my $color = shift;
+       my $string = join("", @_);
+
+       if (defined $color) {
+               # Put a color code at the beginning of each line, a reset at the end
+               # color after newlines that are not at the end of the string
+               $string =~ s/(\n+)(.)/$1$color$2/g;
+               # reset before newlines
+               $string =~ s/(\n+)/$normal_color$1/g;
+               # codes at beginning and end (if necessary):
+               $string =~ s/^/$color/;
+               $string =~ s/$/$normal_color/ unless $string =~ /\n$/;
+       }
+       return $string;
+}
 
 # command line options
 my $patch_mode;
@@ -46,6 +82,19 @@ sub list_untracked {
 my $status_fmt = '%12s %12s %s';
 my $status_head = sprintf($status_fmt, 'staged', 'unstaged', 'path');
 
+{
+       my $initial;
+       sub is_initial_commit {
+               $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
+                       unless defined $initial;
+               return $initial;
+       }
+}
+
+sub get_empty_tree {
+       return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
+}
+
 # Returns list of hashes, contents of each of which are:
 # VALUE:       pathname
 # BINARY:      is a binary path
@@ -67,8 +116,10 @@ sub list_modified {
                return if (!@tracked);
        }
 
+       my $reference = is_initial_commit() ? get_empty_tree() : 'HEAD';
        for (run_cmd_pipe(qw(git diff-index --cached
-                            --numstat --summary HEAD --), @tracked)) {
+                            --numstat --summary), $reference,
+                            '--', @tracked)) {
                if (($add, $del, $file) =
                    /^([-\d]+)  ([-\d]+)        (.*)/) {
                        my ($change, $bin);
@@ -193,7 +244,7 @@ sub find_unique_prefixes {
                if ((ref $print) eq 'ARRAY') {
                        $print = $print->[0];
                }
-               else {
+               elsif ((ref $print) eq 'HASH') {
                        $print = $print->{VALUE};
                }
                update_trie(\%trie, $print);
@@ -230,12 +281,36 @@ sub find_unique_prefixes {
        return @return;
 }
 
+# filters out prefixes which have special meaning to list_and_choose()
+sub is_valid_prefix {
+       my $prefix = shift;
+       return (defined $prefix) &&
+           !($prefix =~ /[\s,]/) && # separators
+           !($prefix =~ /^-/) &&    # deselection
+           !($prefix =~ /^\d+/) &&  # selection
+           ($prefix ne '*') &&      # "all" wildcard
+           ($prefix ne '?');        # prompt help
+}
+
 # given a prefix/remainder tuple return a string with the prefix highlighted
 # for now use square brackets; later might use ANSI colors (underline, bold)
 sub highlight_prefix {
        my $prefix = shift;
        my $remainder = shift;
-       return (defined $prefix) ? "[$prefix]$remainder" : $remainder;
+
+       if (!defined $prefix) {
+               return $remainder;
+       }
+
+       if (!is_valid_prefix($prefix)) {
+               return "$prefix$remainder";
+       }
+
+       if (!$menu_use_color) {
+               return "[$prefix]$remainder";
+       }
+
+       return "$prompt_color$prefix$normal_color$remainder";
 }
 
 sub list_and_choose {
@@ -252,26 +327,26 @@ sub list_and_choose {
                        if (!$opts->{LIST_FLAT}) {
                                print "     ";
                        }
-                       print "$opts->{HEADER}\n";
+                       print colored $header_color, "$opts->{HEADER}\n";
                }
                for ($i = 0; $i < @stuff; $i++) {
                        my $chosen = $chosen[$i] ? '*' : ' ';
                        my $print = $stuff[$i];
-                       if (ref $print) {
-                               if ((ref $print) eq 'ARRAY') {
-                                       $print = @prefixes ?
-                                           highlight_prefix(@{$prefixes[$i]}) :
-                                           $print->[0];
-                               }
-                               else {
-                                       my $value = @prefixes ?
-                                           highlight_prefix(@{$prefixes[$i]}) :
-                                           $print->{VALUE};
-                                       $print = sprintf($status_fmt,
-                                           $print->{INDEX},
-                                           $print->{FILE},
-                                           $value);
-                               }
+                       my $ref = ref $print;
+                       my $highlighted = highlight_prefix(@{$prefixes[$i]})
+                           if @prefixes;
+                       if ($ref eq 'ARRAY') {
+                               $print = $highlighted || $print->[0];
+                       }
+                       elsif ($ref eq 'HASH') {
+                               my $value = $highlighted || $print->{VALUE};
+                               $print = sprintf($status_fmt,
+                                   $print->{INDEX},
+                                   $print->{FILE},
+                                   $value);
+                       }
+                       else {
+                               $print = $highlighted || $print;
                        }
                        printf("%s%2d: %s", $chosen, $i+1, $print);
                        if (($opts->{LIST_FLAT}) &&
@@ -290,7 +365,7 @@ sub list_and_choose {
 
                return if ($opts->{LIST_ONLY});
 
-               print $opts->{PROMPT};
+               print colored $prompt_color, $opts->{PROMPT};
                if ($opts->{SINGLETON}) {
                        print "> ";
                }
@@ -305,6 +380,12 @@ sub list_and_choose {
                }
                chomp $line;
                last if $line eq '';
+               if ($line eq '?') {
+                       $opts->{SINGLETON} ?
+                           singleton_prompt_help_cmd() :
+                           prompt_help_cmd();
+                       next TOPLOOP;
+               }
                for my $choice (split(/[\s,]+/, $line)) {
                        my $choose = 1;
                        my ($bottom, $top);
@@ -350,6 +431,28 @@ sub list_and_choose {
        return @return;
 }
 
+sub singleton_prompt_help_cmd {
+       print colored $help_color, <<\EOF ;
+Prompt help:
+1          - select a numbered item
+foo        - select item based on unique prefix
+           - (empty) select nothing
+EOF
+}
+
+sub prompt_help_cmd {
+       print colored $help_color, <<\EOF ;
+Prompt help:
+1          - select a single item
+3-5        - select a range of items
+2-3,6-9    - select multiple ranges
+foo        - select item based on unique prefix
+-...       - unselect specified items
+*          - choose all items
+           - (empty) finish selecting
+EOF
+}
+
 sub status_cmd {
        list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
                        list_modified());
@@ -388,21 +491,27 @@ sub revert_cmd {
                                       HEADER => $status_head, },
                                     list_modified());
        if (@update) {
-               my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
-                                        map { $_->{VALUE} } @update);
-               my $fh;
-               open $fh, '| git update-index --index-info'
-                   or die;
-               for (@lines) {
-                       print $fh $_;
+               if (is_initial_commit()) {
+                       system(qw(git rm --cached),
+                               map { $_->{VALUE} } @update);
                }
-               close($fh);
-               for (@update) {
-                       if ($_->{INDEX_ADDDEL} &&
-                           $_->{INDEX_ADDDEL} eq 'create') {
-                               system(qw(git update-index --force-remove --),
-                                      $_->{VALUE});
-                               print "note: $_->{VALUE} is untracked now.\n";
+               else {
+                       my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
+                                                map { $_->{VALUE} } @update);
+                       my $fh;
+                       open $fh, '| git update-index --index-info'
+                           or die;
+                       for (@lines) {
+                               print $fh $_;
+                       }
+                       close($fh);
+                       for (@update) {
+                               if ($_->{INDEX_ADDDEL} &&
+                                   $_->{INDEX_ADDDEL} eq 'create') {
+                                       system(qw(git update-index --force-remove --),
+                                              $_->{VALUE});
+                                       print "note: $_->{VALUE} is untracked now.\n";
+                               }
                        }
                }
                refresh();
@@ -424,13 +533,19 @@ sub add_untracked_cmd {
 sub parse_diff {
        my ($path) = @_;
        my @diff = run_cmd_pipe(qw(git diff-files -p --), $path);
-       my (@hunk) = { TEXT => [] };
+       my @colored = ();
+       if ($diff_use_color) {
+               @colored = run_cmd_pipe(qw(git diff-files -p --color --), $path);
+       }
+       my (@hunk) = { TEXT => [], DISPLAY => [] };
 
-       for (@diff) {
-               if (/^@@ /) {
-                       push @hunk, { TEXT => [] };
+       for (my $i = 0; $i < @diff; $i++) {
+               if ($diff[$i] =~ /^@@ /) {
+                       push @hunk, { TEXT => [], DISPLAY => [] };
                }
-               push @{$hunk[-1]{TEXT}}, $_;
+               push @{$hunk[-1]{TEXT}}, $diff[$i];
+               push @{$hunk[-1]{DISPLAY}},
+                       ($diff_use_color ? $colored[$i] : $diff[$i]);
        }
        return @hunk;
 }
@@ -452,9 +567,11 @@ sub parse_hunk_header {
 }
 
 sub split_hunk {
-       my ($text) = @_;
+       my ($text, $display) = @_;
        my @split = ();
-
+       if (!defined $display) {
+               $display = $text;
+       }
        # If there are context lines in the middle of a hunk,
        # it can be split, but we would need to take care of
        # overlaps later.
@@ -468,16 +585,19 @@ sub split_hunk {
                my $i = $hunk_start - 1;
                my $this = +{
                        TEXT => [],
+                       DISPLAY => [],
                        OLD => $o_ofs,
                        NEW => $n_ofs,
                        OCNT => 0,
                        NCNT => 0,
                        ADDDEL => 0,
                        POSTCTX => 0,
+                       USE => undef,
                };
 
                while (++$i < @$text) {
                        my $line = $text->[$i];
+                       my $display = $display->[$i];
                        if ($line =~ /^ /) {
                                if ($this->{ADDDEL} &&
                                    !defined $next_hunk_start) {
@@ -489,6 +609,7 @@ sub split_hunk {
                                        $next_hunk_start = $i;
                                }
                                push @{$this->{TEXT}}, $line;
+                               push @{$this->{DISPLAY}}, $display;
                                $this->{OCNT}++;
                                $this->{NCNT}++;
                                if (defined $next_hunk_start) {
@@ -511,6 +632,7 @@ sub split_hunk {
                                redo OUTER;
                        }
                        push @{$this->{TEXT}}, $line;
+                       push @{$this->{DISPLAY}}, $display;
                        $this->{ADDDEL}++;
                        if ($line =~ /^-/) {
                                $this->{OCNT}++;
@@ -535,9 +657,14 @@ sub split_hunk {
                            " +$n_ofs" .
                            (($n_cnt != 1) ? ",$n_cnt" : '') .
                            " @@\n");
+               my $display_head = $head;
                unshift @{$hunk->{TEXT}}, $head;
+               if ($diff_use_color) {
+                       $display_head = colored($fraginfo_color, $head);
+               }
+               unshift @{$hunk->{DISPLAY}}, $display_head;
        }
-       return map { $_->{TEXT} } @split;
+       return @split;
 }
 
 sub find_last_o_ctx {
@@ -629,7 +756,7 @@ sub coalesce_overlapping_hunks {
 }
 
 sub help_patch_cmd {
-       print <<\EOF ;
+       print colored $help_color, <<\EOF ;
 y - stage this hunk
 n - do not stage this hunk
 a - stage this and all the remaining hunks in the file
@@ -668,7 +795,7 @@ sub patch_update_file {
        my ($ix, $num);
        my $path = shift;
        my ($head, @hunk) = parse_diff($path);
-       for (@{$head->{TEXT}}) {
+       for (@{$head->{DISPLAY}}) {
                print;
        }
        $num = scalar @hunk;
@@ -712,10 +839,10 @@ sub patch_update_file {
                if (hunk_splittable($hunk[$ix]{TEXT})) {
                        $other .= '/s';
                }
-               for (@{$hunk[$ix]{TEXT}}) {
+               for (@{$hunk[$ix]{DISPLAY}}) {
                        print;
                }
-               print "Stage this hunk [y/n/a/d$other/?]? ";
+               print colored $prompt_color, "Stage this hunk [y/n/a/d$other/?]? ";
                my $line = <STDIN>;
                if ($line) {
                        if ($line =~ /^y/i) {
@@ -767,14 +894,12 @@ sub patch_update_file {
                                next;
                        }
                        elsif ($other =~ /s/ && $line =~ /^s/) {
-                               my @split = split_hunk($hunk[$ix]{TEXT});
+                               my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
                                if (1 < @split) {
-                                       print "Split into ",
+                                       print colored $header_color, "Split into ",
                                        scalar(@split), " hunks.\n";
                                }
-                               splice(@hunk, $ix, 1,
-                                      map { +{ TEXT => $_, USE => undef } }
-                                      @split);
+                               splice (@hunk, $ix, 1, @split);
                                $num = scalar @hunk;
                                next;
                        }
@@ -852,8 +977,9 @@ sub diff_cmd {
                                     HEADER => $status_head, },
                                   @mods);
        return if (!@them);
-       system(qw(git diff-index -p --cached HEAD --),
-              map { $_->{VALUE} } @them);
+       my $reference = is_initial_commit() ? get_empty_tree() : 'HEAD';
+       system(qw(git diff -p --cached), $reference, '--',
+               map { $_->{VALUE} } @them);
 }
 
 sub quit_cmd {
@@ -862,7 +988,7 @@ sub quit_cmd {
 }
 
 sub help_cmd {
-       print <<\EOF ;
+       print colored $help_color, <<\EOF ;
 status        - show paths with changes
 update        - add working tree state to the staged set of changes
 revert        - revert staged set of changes back to the HEAD version