t0021-conversion.sh: fix NoTerminatingSymbolAtEOF test
[gitweb.git] / git-add--interactive.perl
index 360610314e8e98c4c79a86273696317bf44cb1dd..4f08fe704bbaff64613ab07630a71da8bf8bbca0 100755 (executable)
@@ -1,6 +1,8 @@
-#!/usr/bin/perl -w
+#!/usr/bin/perl
 
+use 5.008;
 use strict;
+use warnings;
 use Git;
 
 binmode(STDOUT, ":raw");
@@ -72,8 +74,11 @@ 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' => {
@@ -84,6 +89,67 @@ sub colored {
                TARGET => '',
                PARTICIPLE => 'staging',
                FILTER => 'file-only',
+               IS_REVERSE => 0,
+       },
+       'stash' => {
+               DIFF => 'diff-index -p HEAD',
+               APPLY => sub { apply_patch 'apply --cached', @_; },
+               APPLY_CHECK => 'apply --cached',
+               VERB => 'Stash',
+               TARGET => '',
+               PARTICIPLE => 'stashing',
+               FILTER => undef,
+               IS_REVERSE => 0,
+       },
+       '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',
+               IS_REVERSE => 1,
+       },
+       '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',
+               IS_REVERSE => 0,
+       },
+       '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',
+               IS_REVERSE => 1,
+       },
+       '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,
+               IS_REVERSE => 1,
+       },
+       '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,
+               IS_REVERSE => 0,
        },
 );
 
@@ -202,11 +268,18 @@ sub list_modified {
                @tracked = map {
                        chomp $_;
                        unquote_path($_);
-               } run_cmd_pipe(qw(git ls-files --exclude-standard --), @ARGV);
+               } run_cmd_pipe(qw(git ls-files --), @ARGV);
                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)) {
@@ -632,7 +705,7 @@ sub add_untracked_cmd {
 sub run_git_apply {
        my $cmd = shift;
        my $fh;
-       open $fh, '| git ' . $cmd;
+       open $fh, '| git ' . $cmd . " --recount --allow-overlap";
        print $fh @_;
        return close $fh;
 }
@@ -640,6 +713,9 @@ sub run_git_apply {
 sub parse_diff {
        my ($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) {
@@ -664,14 +740,17 @@ sub parse_diff_header {
 
        my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
        my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
+       my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
 
        for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
-               my $dest = $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ?
-                       $mode : $head;
+               my $dest =
+                  $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
+                  $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
+                  $head;
                push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
                push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
        }
-       return ($head, $mode);
+       return ($head, $mode, $deletion);
 }
 
 sub hunk_splittable {
@@ -866,6 +945,10 @@ sub coalesce_overlapping_hunks {
        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 &&
@@ -883,6 +966,28 @@ sub coalesce_overlapping_hunks {
        return @out;
 }
 
+sub reassemble_patch {
+       my $head = shift;
+       my @patch;
+
+       # Include everything in the header except the beginning of the diff.
+       push @patch, (grep { !/^[-+]{3}/ } @$head);
+
+       # Then include any headers from the hunk lines, which must
+       # come before any actual hunk.
+       while (@_ && $_[0] !~ /^@/) {
+               push @patch, shift;
+       }
+
+       # Then begin the diff.
+       push @patch, grep { /^[-+]{3}/ } @$head;
+
+       # And then the actual hunks.
+       push @patch, @_;
+
+       return @patch;
+}
+
 sub color_diff {
        return map {
                colored((/^@/  ? $fraginfo_color :
@@ -903,10 +1008,12 @@ sub edit_hunk_manually {
        print $fh "# Manual hunk edit mode -- see bottom for a quick guide\n";
        print $fh @$oldtext;
        my $participle = $patch_mode_flavour{PARTICIPLE};
+       my $is_reverse = $patch_mode_flavour{IS_REVERSE};
+       my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
        print $fh <<EOF;
 # ---
-# To remove '-' lines, make them ' ' lines (context).
-# To remove '+' lines, delete them.
+# To remove '$remove_minus' lines, make them ' ' lines (context).
+# To remove '$remove_plus' lines, delete them.
 # Lines starting with # will be removed.
 #
 # If the patch applies cleanly, the edited hunk will immediately be
@@ -916,8 +1023,7 @@ sub edit_hunk_manually {
 EOF
        close $fh;
 
-       my $editor = $ENV{GIT_EDITOR} || $repo->config("core.editor")
-               || $ENV{VISUAL} || $ENV{EDITOR} || "vi";
+       chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
        system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
 
        if ($? != 0) {
@@ -944,7 +1050,7 @@ sub edit_hunk_manually {
 
 sub diff_applies {
        my $fh;
-       return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --recount --check',
+       return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
                             map { @{$_->{TEXT}} } @_);
 }
 
@@ -1016,9 +1122,9 @@ sub help_patch_cmd {
        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
+q - quit; do not $verb this hunk nor any of the remaining ones
+a - $verb this hunk and all later hunks in the file
+d - do not $verb this hunk nor any of the later 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
@@ -1033,13 +1139,36 @@ sub help_patch_cmd {
 
 sub apply_patch {
        my $cmd = shift;
-       my $ret = run_git_apply $cmd . ' --recount', @_;
+       my $ret = run_git_apply $cmd, @_;
        if (!$ret) {
                print STDERR @_;
        }
        return $ret;
 }
 
+sub apply_patch_for_checkout_commit {
+       my $reverse = shift;
+       my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
+       my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
+
+       if ($applies_worktree && $applies_index) {
+               run_git_apply 'apply '.$reverse.' --cached', @_;
+               run_git_apply 'apply '.$reverse, @_;
+               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, @_;
+               } 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($patch_mode_flavour{FILTER});
        my @mods = grep { !($_->{BINARY}) } @all_mods;
@@ -1112,7 +1241,7 @@ sub patch_update_file {
        my ($ix, $num);
        my $path = shift;
        my ($head, @hunk) = parse_diff($path);
-       ($head, my $mode) = parse_diff_header($head);
+       ($head, my $mode, my $deletion) = parse_diff_header($head);
        for (@{$head->{DISPLAY}}) {
                print;
        }
@@ -1120,6 +1249,13 @@ sub patch_update_file {
        if (@{$mode->{TEXT}}) {
                unshift @hunk, $mode;
        }
+       if (@{$deletion->{TEXT}}) {
+               foreach my $hunk (@hunk) {
+                       push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
+                       push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
+               }
+               @hunk = ($deletion);
+       }
 
        $num = scalar @hunk;
        $ix = 0;
@@ -1173,7 +1309,9 @@ sub patch_update_file {
                        print;
                }
                print colored $prompt_color, $patch_mode_flavour{VERB},
-                 ($hunk[$ix]{TYPE} eq 'mode' ? ' mode change' : ' this hunk'),
+                 ($hunk[$ix]{TYPE} eq 'mode' ? ' mode change' :
+                  $hunk[$ix]{TYPE} eq 'deletion' ? ' deletion' :
+                  ' this hunk'),
                  $patch_mode_flavour{TARGET},
                  " [y,n,q,a,d,/$other,?]? ";
                my $line = prompt_single_character;
@@ -1228,14 +1366,13 @@ sub patch_update_file {
                                next;
                        }
                        elsif ($line =~ /^q/i) {
-                               while ($ix < $num) {
-                                       if (!defined $hunk[$ix]{USE}) {
-                                               $hunk[$ix]{USE} = 0;
+                               for ($i = 0; $i < $num; $i++) {
+                                       if (!defined $hunk[$i]{USE}) {
+                                               $hunk[$i]{USE} = 0;
                                        }
-                                       $ix++;
                                }
                                $quit = 1;
-                               next;
+                               last;
                        }
                        elsif ($line =~ m|^/(.*)|) {
                                my $regex = $1;
@@ -1348,7 +1485,7 @@ sub patch_update_file {
 
        if (@result) {
                my $fh;
-               my @patch = (@{$head->{TEXT}}, @result);
+               my @patch = reassemble_patch($head->{TEXT}, @result);
                my $apply_routine = $patch_mode_flavour{APPLY};
                &$apply_routine(@patch);
                refresh();
@@ -1391,11 +1528,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 --";