Add / command in add --patch
[gitweb.git] / git-add--interactive.perl
index 709caa9055e139731bedd8b4e0b2659df6017bfa..64ad28998e55f189cfa079db232bbf7ef12d93ba 100755 (executable)
        $diff_use_color ? (
                $repo->get_color('color.diff.frag', 'cyan'),
        ) : ();
+my ($diff_plain_color) =
+       $diff_use_color ? (
+               $repo->get_color('color.diff.plain', ''),
+       ) : ();
+my ($diff_old_color) =
+       $diff_use_color ? (
+               $repo->get_color('color.diff.old', 'red'),
+       ) : ();
+my ($diff_new_color) =
+       $diff_use_color ? (
+               $repo->get_color('color.diff.new', 'green'),
+       ) : ();
 
 my $normal_color = $repo->get_color("", "reset");
 
@@ -42,7 +54,7 @@ sub colored {
 my $patch_mode;
 
 sub run_cmd_pipe {
-       if ($^O eq 'MSWin32') {
+       if ($^O eq 'MSWin32' || $^O eq 'msys') {
                my @invalid = grep {m/[":*]/} @_;
                die "$^O does not support: @invalid\n" if @invalid;
                my @args = map { m/ /o ? "\"$_\"": $_ } @_;
@@ -682,92 +694,104 @@ sub split_hunk {
        return @split;
 }
 
-sub find_last_o_ctx {
-       my ($it) = @_;
-       my $text = $it->{TEXT};
-       my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
-       my $i = @{$text};
-       my $last_o_ctx = $o_ofs + $o_cnt;
-       while (0 < --$i) {
-               my $line = $text->[$i];
-               if ($line =~ /^ /) {
-                       $last_o_ctx--;
-                       next;
-               }
-               last;
-       }
-       return $last_o_ctx;
+
+sub color_diff {
+       return map {
+               colored((/^@/  ? $fraginfo_color :
+                        /^\+/ ? $diff_new_color :
+                        /^-/  ? $diff_old_color :
+                        $diff_plain_color),
+                       $_);
+       } @_;
 }
 
-sub merge_hunk {
-       my ($prev, $this) = @_;
-       my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
-           parse_hunk_header($prev->{TEXT}[0]);
-       my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
-           parse_hunk_header($this->{TEXT}[0]);
-
-       my (@line, $i, $ofs, $o_cnt, $n_cnt);
-       $ofs = $o0_ofs;
-       $o_cnt = $n_cnt = 0;
-       for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
-               my $line = $prev->{TEXT}[$i];
-               if ($line =~ /^\+/) {
-                       $n_cnt++;
-                       push @line, $line;
-                       next;
-               }
+sub edit_hunk_manually {
+       my ($oldtext) = @_;
+
+       my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
+       my $fh;
+       open $fh, '>', $hunkfile
+               or die "failed to open hunk edit file for writing: " . $!;
+       print $fh "# Manual hunk edit mode -- see bottom for a quick guide\n";
+       print $fh @$oldtext;
+       print $fh <<EOF;
+# ---
+# To remove '-' lines, make them ' ' lines (context).
+# To remove '+' lines, delete them.
+# Lines starting with # will be removed.
+#
+# If the patch applies cleanly, the edited hunk will immediately be
+# marked for staging. If it does not apply cleanly, you will be given
+# an opportunity to edit again. If all lines of the hunk are removed,
+# then the edit is aborted and the hunk is left unchanged.
+EOF
+       close $fh;
 
-               last if ($o1_ofs <= $ofs);
+       my $editor = $ENV{GIT_EDITOR} || $repo->config("core.editor")
+               || $ENV{VISUAL} || $ENV{EDITOR} || "vi";
+       system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
 
-               $o_cnt++;
-               $ofs++;
-               if ($line =~ /^ /) {
-                       $n_cnt++;
-               }
-               push @line, $line;
+       open $fh, '<', $hunkfile
+               or die "failed to open hunk edit file for reading: " . $!;
+       my @newtext = grep { !/^#/ } <$fh>;
+       close $fh;
+       unlink $hunkfile;
+
+       # Abort if nothing remains
+       if (!grep { /\S/ } @newtext) {
+               return undef;
        }
 
-       for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
-               my $line = $this->{TEXT}[$i];
-               if ($line =~ /^\+/) {
-                       $n_cnt++;
-                       push @line, $line;
-                       next;
-               }
-               $ofs++;
-               $o_cnt++;
-               if ($line =~ /^ /) {
-                       $n_cnt++;
-               }
-               push @line, $line;
+       # Reinsert the first hunk header if the user accidentally deleted it
+       if ($newtext[0] !~ /^@/) {
+               unshift @newtext, $oldtext->[0];
+       }
+       return \@newtext;
+}
+
+sub diff_applies {
+       my $fh;
+       open $fh, '| git apply --recount --cached --check';
+       for my $h (@_) {
+               print $fh @{$h->{TEXT}};
        }
-       my $head = ("@@ -$o0_ofs" .
-                   (($o_cnt != 1) ? ",$o_cnt" : '') .
-                   " +$n0_ofs" .
-                   (($n_cnt != 1) ? ",$n_cnt" : '') .
-                   " @@\n");
-       @{$prev->{TEXT}} = ($head, @line);
+       return close $fh;
 }
 
-sub coalesce_overlapping_hunks {
-       my (@in) = @_;
-       my @out = ();
+sub prompt_yesno {
+       my ($prompt) = @_;
+       while (1) {
+               print colored $prompt_color, $prompt;
+               my $line = <STDIN>;
+               return 0 if $line =~ /^n/i;
+               return 1 if $line =~ /^y/i;
+       }
+}
 
-       my ($last_o_ctx);
+sub edit_hunk_loop {
+       my ($head, $hunk, $ix) = @_;
+       my $text = $hunk->[$ix]->{TEXT};
 
-       for (grep { $_->{USE} } @in) {
-               my $text = $_->{TEXT};
-               my ($o_ofs) = parse_hunk_header($text->[0]);
-               if (defined $last_o_ctx &&
-                   $o_ofs <= $last_o_ctx) {
-                       merge_hunk($out[-1], $_);
+       while (1) {
+               $text = edit_hunk_manually($text);
+               if (!defined $text) {
+                       return undef;
+               }
+               my $newhunk = { TEXT => $text, USE => 1 };
+               if (diff_applies($head,
+                                @{$hunk}[0..$ix-1],
+                                $newhunk,
+                                @{$hunk}[$ix+1..$#{$hunk}])) {
+                       $newhunk->{DISPLAY} = [color_diff(@{$text})];
+                       return $newhunk;
                }
                else {
-                       push @out, $_;
+                       prompt_yesno(
+                               'Your edited hunk does not apply. Edit again '
+                               . '(saying "no" discards!) [y/n]? '
+                               ) or return undef;
                }
-               $last_o_ctx = find_last_o_ctx($out[-1]);
        }
-       return @out;
 }
 
 sub help_patch_cmd {
@@ -776,21 +800,29 @@ sub help_patch_cmd {
 n - do not stage this hunk
 a - stage this and all the remaining hunks in the file
 d - do not stage this hunk nor any of the remaining hunks in the file
+g - select a hunk to go to
+/ - search for a hunk matching the given regex
 j - leave this hunk undecided, see next undecided hunk
 J - leave this hunk undecided, see next hunk
 k - leave this hunk undecided, see previous undecided hunk
 K - leave this hunk undecided, see previous hunk
 s - split the current hunk into smaller hunks
+e - manually edit the current hunk
 ? - print help
 EOF
 }
 
 sub patch_update_cmd {
-       my @mods = grep { !($_->{BINARY}) } list_modified('file-only');
+       my @all_mods = list_modified('file-only');
+       my @mods = grep { !($_->{BINARY}) } @all_mods;
        my @them;
 
        if (!@mods) {
-               print STDERR "No changes.\n";
+               if (@all_mods) {
+                       print STDERR "Only binary files changed.\n";
+               } else {
+                       print STDERR "No changes.\n";
+               }
                return 0;
        }
        if ($patch_mode) {
@@ -806,6 +838,47 @@ sub patch_update_cmd {
        }
 }
 
+# Generate a one line summary of a hunk.
+sub summarize_hunk {
+       my $rhunk = shift;
+       my $summary = $rhunk->{TEXT}[0];
+
+       # Keep the line numbers, discard extra context.
+       $summary =~ s/@@(.*?)@@.*/$1 /s;
+       $summary .= " " x (20 - length $summary);
+
+       # Add some user context.
+       for my $line (@{$rhunk->{TEXT}}) {
+               if ($line =~ m/^[+-].*\w/) {
+                       $summary .= $line;
+                       last;
+               }
+       }
+
+       chomp $summary;
+       return substr($summary, 0, 80) . "\n";
+}
+
+
+# Print a one-line summary of each hunk in the array ref in
+# the first argument, starting wih the index in the 2nd.
+sub display_hunks {
+       my ($hunks, $i) = @_;
+       my $ctr = 0;
+       $i ||= 0;
+       for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
+               my $status = " ";
+               if (defined $hunks->[$i]{USE}) {
+                       $status = $hunks->[$i]{USE} ? "+" : "-";
+               }
+               printf "%s%2d: %s",
+                       $status,
+                       $i + 1,
+                       summarize_hunk($hunks->[$i]);
+       }
+       return $i;
+}
+
 sub patch_update_file {
        my ($ix, $num);
        my $path = shift;
@@ -857,22 +930,25 @@ sub patch_update_file {
                for ($i = 0; $i < $ix; $i++) {
                        if (!defined $hunk[$i]{USE}) {
                                $prev = 1;
-                               $other .= '/k';
+                               $other .= ',k';
                                last;
                        }
                }
                if ($ix) {
-                       $other .= '/K';
+                       $other .= ',K';
                }
                for ($i = $ix + 1; $i < $num; $i++) {
                        if (!defined $hunk[$i]{USE}) {
                                $next = 1;
-                               $other .= '/j';
+                               $other .= ',j';
                                last;
                        }
                }
                if ($ix < $num - 1) {
-                       $other .= '/J';
+                       $other .= ',J';
+               }
+               if ($num > 1) {
+                       $other .= '/g';
                }
                for ($i = 0; $i < $num; $i++) {
                        if (!defined $hunk[$i]{USE}) {
@@ -883,12 +959,13 @@ sub patch_update_file {
                last if (!$undecided);
 
                if (hunk_splittable($hunk[$ix]{TEXT})) {
-                       $other .= '/s';
+                       $other .= ',s';
                }
+               $other .= ',e';
                for (@{$hunk[$ix]{DISPLAY}}) {
                        print;
                }
-               print colored $prompt_color, "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) {
@@ -906,6 +983,28 @@ sub patch_update_file {
                                }
                                next;
                        }
+                       elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
+                               my $response = $1;
+                               my $no = $ix > 10 ? $ix - 10 : 0;
+                               while ($response eq '') {
+                                       my $extra = "";
+                                       $no = display_hunks(\@hunk, $no);
+                                       if ($no < $num) {
+                                               $extra = " (<ret> to see more)";
+                                       }
+                                       print "go to which hunk$extra? ";
+                                       $response = <STDIN>;
+                                       chomp $response;
+                               }
+                               if ($response !~ /^\s*\d+\s*$/) {
+                                       print STDERR "Invalid number: '$response'\n";
+                               } elsif (0 < $response && $response <= $num) {
+                                       $ix = $response - 1;
+                               } else {
+                                       print STDERR "Sorry, only $num hunks available.\n";
+                               }
+                               next;
+                       }
                        elsif ($line =~ /^d/i) {
                                while ($ix < $num) {
                                        if (!defined $hunk[$ix]{USE}) {
@@ -915,6 +1014,31 @@ sub patch_update_file {
                                }
                                next;
                        }
+                       elsif ($line =~ m|^/(.*)|) {
+                               my $search_string;
+                               eval {
+                                       $search_string = qr{$1}m;
+                               };
+                               if ($@) {
+                                       my ($err,$exp) = ($@, $1);
+                                       $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
+                                       print STDERR "Malformed search regexp $exp: $err\n";
+                                       next;
+                               }
+                               my $iy = $ix;
+                               while (1) {
+                                       my $text = join ("", @{$hunk[$iy]{TEXT}});
+                                       last if ($text =~ $search_string);
+                                       $iy++;
+                                       $iy = 0 if ($iy >= $num);
+                                       if ($ix == $iy) {
+                                               print STDERR "No hunk matches the given pattern\n";
+                                               last;
+                                       }
+                               }
+                               $ix = $iy;
+                               next;
+                       }
                        elsif ($other =~ /K/ && $line =~ /^K/) {
                                $ix--;
                                next;
@@ -949,6 +1073,12 @@ sub patch_update_file {
                                $num = scalar @hunk;
                                next;
                        }
+                       elsif ($line =~ /^e/) {
+                               my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
+                               if (defined $newhunk) {
+                                       splice @hunk, $ix, 1, $newhunk;
+                               }
+                       }
                        else {
                                help_patch_cmd($other);
                                next;
@@ -962,47 +1092,21 @@ sub patch_update_file {
                }
        }
 
-       @hunk = coalesce_overlapping_hunks(@hunk);
-
        my $n_lofs = 0;
        my @result = ();
        if ($mode->{USE}) {
                push @result, @{$mode->{TEXT}};
        }
        for (@hunk) {
-               my $text = $_->{TEXT};
-               my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
-                   parse_hunk_header($text->[0]);
-
-               if (!$_->{USE}) {
-                       # We would have added ($n_cnt - $o_cnt) lines
-                       # to the postimage if we were to use this hunk,
-                       # but we didn't.  So the line number that the next
-                       # hunk starts at would be shifted by that much.
-                       $n_lofs -= ($n_cnt - $o_cnt);
-                       next;
-               }
-               else {
-                       if ($n_lofs) {
-                               $n_ofs += $n_lofs;
-                               $text->[0] = ("@@ -$o_ofs" .
-                                             (($o_cnt != 1)
-                                              ? ",$o_cnt" : '') .
-                                             " +$n_ofs" .
-                                             (($n_cnt != 1)
-                                              ? ",$n_cnt" : '') .
-                                             " @@\n");
-                       }
-                       for (@$text) {
-                               push @result, $_;
-                       }
+               if ($_->{USE}) {
+                       push @result, @{$_->{TEXT}};
                }
        }
 
        if (@result) {
                my $fh;
 
-               open $fh, '| git apply --cached';
+               open $fh, '| git apply --cached --recount';
                for (@{$head->{TEXT}}, @result) {
                        print $fh $_;
                }