Merge branch 'fc/mutt-alias'
[gitweb.git] / git-add--interactive.perl
index def062a9e2cbc67b78522aa47151d47cd50f0f33..392efb913f7c2ff13bbbb46e4414325b71e07676 100755 (executable)
@@ -72,6 +72,79 @@ sub colored {
 
 # command line options
 my $patch_mode;
+my $patch_mode_revision;
+
+sub apply_patch;
+sub apply_patch_for_checkout_commit;
+sub apply_patch_for_stash;
+
+my %patch_modes = (
+       'stage' => {
+               DIFF => 'diff-files -p',
+               APPLY => sub { apply_patch 'apply --cached', @_; },
+               APPLY_CHECK => 'apply --cached',
+               VERB => 'Stage',
+               TARGET => '',
+               PARTICIPLE => 'staging',
+               FILTER => 'file-only',
+       },
+       'stash' => {
+               DIFF => 'diff-index -p HEAD',
+               APPLY => sub { apply_patch 'apply --cached', @_; },
+               APPLY_CHECK => 'apply --cached',
+               VERB => 'Stash',
+               TARGET => '',
+               PARTICIPLE => 'stashing',
+               FILTER => undef,
+       },
+       'reset_head' => {
+               DIFF => 'diff-index -p --cached',
+               APPLY => sub { apply_patch 'apply -R --cached', @_; },
+               APPLY_CHECK => 'apply -R --cached',
+               VERB => 'Unstage',
+               TARGET => '',
+               PARTICIPLE => 'unstaging',
+               FILTER => 'index-only',
+       },
+       'reset_nothead' => {
+               DIFF => 'diff-index -R -p --cached',
+               APPLY => sub { apply_patch 'apply --cached', @_; },
+               APPLY_CHECK => 'apply --cached',
+               VERB => 'Apply',
+               TARGET => ' to index',
+               PARTICIPLE => 'applying',
+               FILTER => 'index-only',
+       },
+       'checkout_index' => {
+               DIFF => 'diff-files -p',
+               APPLY => sub { apply_patch 'apply -R', @_; },
+               APPLY_CHECK => 'apply -R',
+               VERB => 'Discard',
+               TARGET => ' from worktree',
+               PARTICIPLE => 'discarding',
+               FILTER => 'file-only',
+       },
+       'checkout_head' => {
+               DIFF => 'diff-index -p',
+               APPLY => sub { apply_patch_for_checkout_commit '-R', @_ },
+               APPLY_CHECK => 'apply -R',
+               VERB => 'Discard',
+               TARGET => ' from index and worktree',
+               PARTICIPLE => 'discarding',
+               FILTER => undef,
+       },
+       'checkout_nothead' => {
+               DIFF => 'diff-index -R -p',
+               APPLY => sub { apply_patch_for_checkout_commit '', @_ },
+               APPLY_CHECK => 'apply',
+               VERB => 'Apply',
+               TARGET => ' to index and worktree',
+               PARTICIPLE => 'applying',
+               FILTER => undef,
+       },
+);
+
+my %patch_mode_flavour = %{$patch_modes{stage}};
 
 sub run_cmd_pipe {
        if ($^O eq 'MSWin32' || $^O eq 'msys') {
@@ -190,7 +263,14 @@ sub list_modified {
                return if (!@tracked);
        }
 
-       my $reference = is_initial_commit() ? get_empty_tree() : 'HEAD';
+       my $reference;
+       if (defined $patch_mode_revision and $patch_mode_revision ne 'HEAD') {
+               $reference = $patch_mode_revision;
+       } elsif (is_initial_commit()) {
+               $reference = get_empty_tree();
+       } else {
+               $reference = 'HEAD';
+       }
        for (run_cmd_pipe(qw(git diff-index --cached
                             --numstat --summary), $reference,
                             '--', @tracked)) {
@@ -613,18 +693,31 @@ sub add_untracked_cmd {
        print "\n";
 }
 
+sub run_git_apply {
+       my $cmd = shift;
+       my $fh;
+       open $fh, '| git ' . $cmd;
+       print $fh @_;
+       return close $fh;
+}
+
 sub parse_diff {
        my ($path) = @_;
-       my @diff = run_cmd_pipe(qw(git diff-files -p --), $path);
+       my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
+       if (defined $patch_mode_revision) {
+               push @diff_cmd, $patch_mode_revision;
+       }
+       my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
        my @colored = ();
        if ($diff_use_color) {
-               @colored = run_cmd_pipe(qw(git diff-files -p --color --), $path);
+               @colored = run_cmd_pipe("git", @diff_cmd, qw(--color --), $path);
        }
-       my (@hunk) = { TEXT => [], DISPLAY => [] };
+       my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
 
        for (my $i = 0; $i < @diff; $i++) {
                if ($diff[$i] =~ /^@@ /) {
-                       push @hunk, { TEXT => [], DISPLAY => [] };
+                       push @hunk, { TEXT => [], DISPLAY => [],
+                               TYPE => 'hunk' };
                }
                push @{$hunk[-1]{TEXT}}, $diff[$i];
                push @{$hunk[-1]{DISPLAY}},
@@ -636,8 +729,8 @@ sub parse_diff {
 sub parse_diff_header {
        my $src = shift;
 
-       my $head = { TEXT => [], DISPLAY => [] };
-       my $mode = { TEXT => [], DISPLAY => [] };
+       my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
+       my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
 
        for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
                my $dest = $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ?
@@ -684,6 +777,7 @@ sub split_hunk {
                my $this = +{
                        TEXT => [],
                        DISPLAY => [],
+                       TYPE => 'hunk',
                        OLD => $o_ofs,
                        NEW => $n_ofs,
                        OCNT => 0,
@@ -765,6 +859,100 @@ 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 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;
+               }
+
+               last if ($o1_ofs <= $ofs);
+
+               $o_cnt++;
+               $ofs++;
+               if ($line =~ /^ /) {
+                       $n_cnt++;
+               }
+               push @line, $line;
+       }
+
+       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;
+       }
+       my $head = ("@@ -$o0_ofs" .
+                   (($o_cnt != 1) ? ",$o_cnt" : '') .
+                   " +$n0_ofs" .
+                   (($n_cnt != 1) ? ",$n_cnt" : '') .
+                   " @@\n");
+       @{$prev->{TEXT}} = ($head, @line);
+}
+
+sub coalesce_overlapping_hunks {
+       my (@in) = @_;
+       my @out = ();
+
+       my ($last_o_ctx, $last_was_dirty);
+
+       for (grep { $_->{USE} } @in) {
+               if ($_->{TYPE} ne 'hunk') {
+                       push @out, $_;
+                       next;
+               }
+               my $text = $_->{TEXT};
+               my ($o_ofs) = parse_hunk_header($text->[0]);
+               if (defined $last_o_ctx &&
+                   $o_ofs <= $last_o_ctx &&
+                   !$_->{DIRTY} &&
+                   !$last_was_dirty) {
+                       merge_hunk($out[-1], $_);
+               }
+               else {
+                       push @out, $_;
+               }
+               $last_o_ctx = find_last_o_ctx($out[-1]);
+               $last_was_dirty = $_->{DIRTY};
+       }
+       return @out;
+}
 
 sub color_diff {
        return map {
@@ -785,6 +973,7 @@ sub edit_hunk_manually {
                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;
+       my $participle = $patch_mode_flavour{PARTICIPLE};
        print $fh <<EOF;
 # ---
 # To remove '-' lines, make them ' ' lines (context).
@@ -792,7 +981,7 @@ sub edit_hunk_manually {
 # 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
+# marked for $participle. 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
@@ -826,11 +1015,8 @@ sub edit_hunk_manually {
 
 sub diff_applies {
        my $fh;
-       open $fh, '| git apply --recount --cached --check';
-       for my $h (@_) {
-               print $fh @{$h->{TEXT}};
-       }
-       return close $fh;
+       return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --recount --check',
+                            map { @{$_->{TEXT}} } @_);
 }
 
 sub _restore_terminal_and_die {
@@ -873,7 +1059,12 @@ sub edit_hunk_loop {
                if (!defined $text) {
                        return undef;
                }
-               my $newhunk = { TEXT => $text, USE => 1 };
+               my $newhunk = {
+                       TEXT => $text,
+                       TYPE => $hunk->[$ix]->{TYPE},
+                       USE => 1,
+                       DIRTY => 1,
+               };
                if (diff_applies($head,
                                 @{$hunk}[0..$ix-1],
                                 $newhunk,
@@ -891,11 +1082,14 @@ sub edit_hunk_loop {
 }
 
 sub help_patch_cmd {
-       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
-d - do not stage this hunk nor any of the remaining hunks in the file
+       my $verb = lc $patch_mode_flavour{VERB};
+       my $target = $patch_mode_flavour{TARGET};
+       print colored $help_color, <<EOF ;
+y - $verb this hunk$target
+n - do not $verb this hunk$target
+q - quit, do not $verb this hunk nor any of the remaining ones
+a - $verb this and all the remaining hunks in the file
+d - do not $verb 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
@@ -908,8 +1102,40 @@ sub help_patch_cmd {
 EOF
 }
 
+sub apply_patch {
+       my $cmd = shift;
+       my $ret = run_git_apply $cmd . ' --recount', @_;
+       if (!$ret) {
+               print STDERR @_;
+       }
+       return $ret;
+}
+
+sub apply_patch_for_checkout_commit {
+       my $reverse = shift;
+       my $applies_index = run_git_apply 'apply '.$reverse.' --cached --recount --check', @_;
+       my $applies_worktree = run_git_apply 'apply '.$reverse.' --recount --check', @_;
+
+       if ($applies_worktree && $applies_index) {
+               run_git_apply 'apply '.$reverse.' --cached --recount', @_;
+               run_git_apply 'apply '.$reverse.' --recount', @_;
+               return 1;
+       } elsif (!$applies_index) {
+               print colored $error_color, "The selected hunks do not apply to the index!\n";
+               if (prompt_yesno "Apply them to the worktree anyway? ") {
+                       return run_git_apply 'apply '.$reverse.' --recount', @_;
+               } else {
+                       print colored $error_color, "Nothing was applied.\n";
+                       return 0;
+               }
+       } else {
+               print STDERR @_;
+               return 0;
+       }
+}
+
 sub patch_update_cmd {
-       my @all_mods = list_modified('file-only');
+       my @all_mods = list_modified($patch_mode_flavour{FILTER});
        my @mods = grep { !($_->{BINARY}) } @all_mods;
        my @them;
 
@@ -930,7 +1156,7 @@ sub patch_update_cmd {
                                        @mods);
        }
        for (@them) {
-               patch_update_file($_->{VALUE});
+               return 0 if patch_update_file($_->{VALUE});
        }
 }
 
@@ -976,6 +1202,7 @@ sub display_hunks {
 }
 
 sub patch_update_file {
+       my $quit = 0;
        my ($ix, $num);
        my $path = shift;
        my ($head, @hunk) = parse_diff($path);
@@ -985,32 +1212,7 @@ sub patch_update_file {
        }
 
        if (@{$mode->{TEXT}}) {
-               while (1) {
-                       print @{$mode->{DISPLAY}};
-                       print colored $prompt_color,
-                               "Stage mode change [y/n/a/d/?]? ";
-                       my $line = prompt_single_character;
-                       if ($line =~ /^y/i) {
-                               $mode->{USE} = 1;
-                               last;
-                       }
-                       elsif ($line =~ /^n/i) {
-                               $mode->{USE} = 0;
-                               last;
-                       }
-                       elsif ($line =~ /^a/i) {
-                               $_->{USE} = 1 foreach ($mode, @hunk);
-                               last;
-                       }
-                       elsif ($line =~ /^d/i) {
-                               $_->{USE} = 0 foreach ($mode, @hunk);
-                               last;
-                       }
-                       else {
-                               help_patch_cmd('');
-                               next;
-                       }
-               }
+               unshift @hunk, $mode;
        }
 
        $num = scalar @hunk;
@@ -1054,14 +1256,20 @@ sub patch_update_file {
                }
                last if (!$undecided);
 
-               if (hunk_splittable($hunk[$ix]{TEXT})) {
+               if ($hunk[$ix]{TYPE} eq 'hunk' &&
+                   hunk_splittable($hunk[$ix]{TEXT})) {
                        $other .= ',s';
                }
-               $other .= ',e';
+               if ($hunk[$ix]{TYPE} eq 'hunk') {
+                       $other .= ',e';
+               }
                for (@{$hunk[$ix]{DISPLAY}}) {
                        print;
                }
-               print colored $prompt_color, "Stage this hunk [y,n,a,d,/$other,?]? ";
+               print colored $prompt_color, $patch_mode_flavour{VERB},
+                 ($hunk[$ix]{TYPE} eq 'mode' ? ' mode change' : ' this hunk'),
+                 $patch_mode_flavour{TARGET},
+                 " [y,n,q,a,d,/$other,?]? ";
                my $line = prompt_single_character;
                if ($line) {
                        if ($line =~ /^y/i) {
@@ -1113,6 +1321,16 @@ sub patch_update_file {
                                }
                                next;
                        }
+                       elsif ($line =~ /^q/i) {
+                               while ($ix < $num) {
+                                       if (!defined $hunk[$ix]{USE}) {
+                                               $hunk[$ix]{USE} = 0;
+                                       }
+                                       $ix++;
+                               }
+                               $quit = 1;
+                               next;
+                       }
                        elsif ($line =~ m|^/(.*)|) {
                                my $regex = $1;
                                if ($1 eq "") {
@@ -1193,7 +1411,7 @@ sub patch_update_file {
                                $num = scalar @hunk;
                                next;
                        }
-                       elsif ($line =~ /^e/) {
+                       elsif ($other =~ /e/ && $line =~ /^e/) {
                                my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
                                if (defined $newhunk) {
                                        splice @hunk, $ix, 1, $newhunk;
@@ -1212,11 +1430,10 @@ sub patch_update_file {
                }
        }
 
+       @hunk = coalesce_overlapping_hunks(@hunk);
+
        my $n_lofs = 0;
        my @result = ();
-       if ($mode->{USE}) {
-               push @result, @{$mode->{TEXT}};
-       }
        for (@hunk) {
                if ($_->{USE}) {
                        push @result, @{$_->{TEXT}};
@@ -1225,20 +1442,14 @@ sub patch_update_file {
 
        if (@result) {
                my $fh;
-
-               open $fh, '| git apply --cached --recount';
-               for (@{$head->{TEXT}}, @result) {
-                       print $fh $_;
-               }
-               if (!close $fh) {
-                       for (@{$head->{TEXT}}, @result) {
-                               print STDERR $_;
-                       }
-               }
+               my @patch = (@{$head->{TEXT}}, @result);
+               my $apply_routine = $patch_mode_flavour{APPLY};
+               &$apply_routine(@patch);
                refresh();
        }
 
        print "\n";
+       return $quit;
 }
 
 sub diff_cmd {
@@ -1274,11 +1485,41 @@ sub help_cmd {
 sub process_args {
        return unless @ARGV;
        my $arg = shift @ARGV;
-       if ($arg eq "--patch") {
-               $patch_mode = 1;
-               $arg = shift @ARGV or die "missing --";
+       if ($arg =~ /--patch(?:=(.*))?/) {
+               if (defined $1) {
+                       if ($1 eq 'reset') {
+                               $patch_mode = 'reset_head';
+                               $patch_mode_revision = 'HEAD';
+                               $arg = shift @ARGV or die "missing --";
+                               if ($arg ne '--') {
+                                       $patch_mode_revision = $arg;
+                                       $patch_mode = ($arg eq 'HEAD' ?
+                                                      'reset_head' : 'reset_nothead');
+                                       $arg = shift @ARGV or die "missing --";
+                               }
+                       } elsif ($1 eq 'checkout') {
+                               $arg = shift @ARGV or die "missing --";
+                               if ($arg eq '--') {
+                                       $patch_mode = 'checkout_index';
+                               } else {
+                                       $patch_mode_revision = $arg;
+                                       $patch_mode = ($arg eq 'HEAD' ?
+                                                      'checkout_head' : 'checkout_nothead');
+                                       $arg = shift @ARGV or die "missing --";
+                               }
+                       } elsif ($1 eq 'stage' or $1 eq 'stash') {
+                               $patch_mode = $1;
+                               $arg = shift @ARGV or die "missing --";
+                       } else {
+                               die "unknown --patch mode: $1";
+                       }
+               } else {
+                       $patch_mode = 'stage';
+                       $arg = shift @ARGV or die "missing --";
+               }
                die "invalid argument $arg, expecting --"
                    unless $arg eq "--";
+               %patch_mode_flavour = %{$patch_modes{$patch_mode}};
        }
        elsif ($arg ne "--") {
                die "invalid argument $arg, expecting --";