]> git.ipfire.org Git - thirdparty/git.git/commitdiff
checkout -p: handle new files correctly
authorJohannes Schindelin <johannes.schindelin@gmx.de>
Wed, 27 May 2020 21:09:06 +0000 (21:09 +0000)
committerJunio C Hamano <gitster@pobox.com>
Wed, 27 May 2020 21:50:20 +0000 (14:50 -0700)
The original patch selection code was written for `git add -p`, and the
fundamental unit on which it works is a hunk.

We hacked around that to handle deletions back in 24ab81ae4d
(add-interactive: handle deletion of empty files, 2009-10-27). But `git
add -p` would never see a new file, since we only consider the set of
tracked files in the index.

However, since the same machinery was used for `git checkout -p` &
friends, we can see new files.

Handle this case specifically, adding a new prompt for it that is
modeled after the `deleted file` case.

This also fixes the problem where added _empty_ files could not be
staged via `git checkout -p`.

Reported-by: Merlin Büge <toni@bluenox07.de>
Helped-by: Jeff King <peff@peff.net>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
add-patch.c
git-add--interactive.perl
t/t3701-add-interactive.sh

index d8dafa8168dc8389468d8c2c3cd6221220826605..eaace1b824f6f5efdcb2771a0f1214edd7b92699 100644 (file)
@@ -9,7 +9,7 @@
 #include "compat/terminal.h"
 
 enum prompt_mode_type {
-       PROMPT_MODE_CHANGE = 0, PROMPT_DELETION, PROMPT_HUNK,
+       PROMPT_MODE_CHANGE = 0, PROMPT_DELETION, PROMPT_ADDITION, PROMPT_HUNK,
        PROMPT_MODE_MAX, /* must be last */
 };
 
@@ -32,6 +32,7 @@ static struct patch_mode patch_mode_add = {
        .prompt_mode = {
                N_("Stage mode change [y,n,q,a,d%s,?]? "),
                N_("Stage deletion [y,n,q,a,d%s,?]? "),
+               N_("Stage addition [y,n,q,a,d%s,?]? "),
                N_("Stage this hunk [y,n,q,a,d%s,?]? ")
        },
        .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
@@ -53,6 +54,7 @@ static struct patch_mode patch_mode_stash = {
        .prompt_mode = {
                N_("Stash mode change [y,n,q,a,d%s,?]? "),
                N_("Stash deletion [y,n,q,a,d%s,?]? "),
+               N_("Stash addition [y,n,q,a,d%s,?]? "),
                N_("Stash this hunk [y,n,q,a,d%s,?]? "),
        },
        .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
@@ -76,6 +78,7 @@ static struct patch_mode patch_mode_reset_head = {
        .prompt_mode = {
                N_("Unstage mode change [y,n,q,a,d%s,?]? "),
                N_("Unstage deletion [y,n,q,a,d%s,?]? "),
+               N_("Unstage addition [y,n,q,a,d%s,?]? "),
                N_("Unstage this hunk [y,n,q,a,d%s,?]? "),
        },
        .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
@@ -98,6 +101,7 @@ static struct patch_mode patch_mode_reset_nothead = {
        .prompt_mode = {
                N_("Apply mode change to index [y,n,q,a,d%s,?]? "),
                N_("Apply deletion to index [y,n,q,a,d%s,?]? "),
+               N_("Apply addition to index [y,n,q,a,d%s,?]? "),
                N_("Apply this hunk to index [y,n,q,a,d%s,?]? "),
        },
        .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
@@ -120,6 +124,7 @@ static struct patch_mode patch_mode_checkout_index = {
        .prompt_mode = {
                N_("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
                N_("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
+               N_("Discard addition from worktree [y,n,q,a,d%s,?]? "),
                N_("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
        },
        .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
@@ -142,6 +147,7 @@ static struct patch_mode patch_mode_checkout_head = {
        .prompt_mode = {
                N_("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
                N_("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
+               N_("Discard addition from index and worktree [y,n,q,a,d%s,?]? "),
                N_("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
        },
        .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
@@ -163,6 +169,7 @@ static struct patch_mode patch_mode_checkout_nothead = {
        .prompt_mode = {
                N_("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
                N_("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
+               N_("Apply addition to index and worktree [y,n,q,a,d%s,?]? "),
                N_("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
        },
        .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
@@ -185,6 +192,7 @@ static struct patch_mode patch_mode_worktree_head = {
        .prompt_mode = {
                N_("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
                N_("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
+               N_("Discard addition from index and worktree [y,n,q,a,d%s,?]? "),
                N_("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
        },
        .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
@@ -206,6 +214,7 @@ static struct patch_mode patch_mode_worktree_nothead = {
        .prompt_mode = {
                N_("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
                N_("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
+               N_("Apply addition to index and worktree [y,n,q,a,d%s,?]? "),
                N_("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
        },
        .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk "
@@ -247,7 +256,7 @@ struct add_p_state {
                struct hunk head;
                struct hunk *hunk;
                size_t hunk_nr, hunk_alloc;
-               unsigned deleted:1, mode_change:1,binary:1;
+               unsigned deleted:1, added:1, mode_change:1,binary:1;
        } *file_diff;
        size_t file_diff_nr;
 
@@ -441,7 +450,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
        pend = p + plain->len;
        while (p != pend) {
                char *eol = memchr(p, '\n', pend - p);
-               const char *deleted = NULL, *mode_change = NULL;
+               const char *deleted = NULL, *added = NULL, *mode_change = NULL;
 
                if (!eol)
                        eol = pend;
@@ -460,11 +469,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
                } else if (p == plain->buf)
                        BUG("diff starts with unexpected line:\n"
                            "%.*s\n", (int)(eol - p), p);
-               else if (file_diff->deleted)
+               else if (file_diff->deleted || file_diff->added)
                        ; /* keep the rest of the file in a single "hunk" */
                else if (starts_with(p, "@@ ") ||
                         (hunk == &file_diff->head &&
-                         skip_prefix(p, "deleted file", &deleted))) {
+                         (skip_prefix(p, "deleted file", &deleted) ||
+                          skip_prefix(p, "new file", &added)))) {
                        if (marker == '-' || marker == '+')
                                /*
                                 * Should not happen; previous hunk did not end
@@ -484,6 +494,8 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
 
                        if (deleted)
                                file_diff->deleted = 1;
+                       else if (added)
+                               file_diff->added = 1;
                        else if (parse_hunk_header(s, hunk) < 0)
                                return -1;
 
@@ -536,8 +548,10 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
                           starts_with(p, "Binary files "))
                        file_diff->binary = 1;
 
-               if (file_diff->deleted && file_diff->mode_change)
-                       BUG("diff contains delete *and* a mode change?!?\n%.*s",
+               if (!!file_diff->deleted + !!file_diff->added +
+                   !!file_diff->mode_change > 1)
+                       BUG("diff can only contain delete *or* add *or* a "
+                           "mode change?!?\n%.*s",
                            (int)(eol - (plain->buf + file_diff->head.start)),
                            plain->buf + file_diff->head.start);
 
@@ -1397,6 +1411,8 @@ static int patch_update_file(struct add_p_state *s,
 
                if (file_diff->deleted)
                        prompt_mode_type = PROMPT_DELETION;
+               else if (file_diff->added)
+                       prompt_mode_type = PROMPT_ADDITION;
                else if (file_diff->mode_change && !hunk_index)
                        prompt_mode_type = PROMPT_MODE_CHANGE;
                else
index 10fd30ae16a3bdf943732d07bc33c0a723b5e344..f36c0078ac9a71758a7e1cab701d3a633b66c0d3 100755 (executable)
@@ -754,16 +754,18 @@ sub parse_diff_header {
        my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
        my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
        my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
+       my $addition = { TEXT => [], DISPLAY => [], TYPE => 'addition' };
 
        for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
                my $dest =
                   $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
                   $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
+                  $src->{TEXT}->[$i] =~ /^new file/ ? $addition :
                   $head;
                push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
                push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
        }
-       return ($head, $mode, $deletion);
+       return ($head, $mode, $deletion, $addition);
 }
 
 sub hunk_splittable {
@@ -1427,46 +1429,55 @@ my %patch_update_prompt_modes = (
        stage => {
                mode => N__("Stage mode change [y,n,q,a,d%s,?]? "),
                deletion => N__("Stage deletion [y,n,q,a,d%s,?]? "),
+               addition => N__("Stage addition [y,n,q,a,d%s,?]? "),
                hunk => N__("Stage this hunk [y,n,q,a,d%s,?]? "),
        },
        stash => {
                mode => N__("Stash mode change [y,n,q,a,d%s,?]? "),
                deletion => N__("Stash deletion [y,n,q,a,d%s,?]? "),
+               addition => N__("Stash addition [y,n,q,a,d%s,?]? "),
                hunk => N__("Stash this hunk [y,n,q,a,d%s,?]? "),
        },
        reset_head => {
                mode => N__("Unstage mode change [y,n,q,a,d%s,?]? "),
                deletion => N__("Unstage deletion [y,n,q,a,d%s,?]? "),
+               addition => N__("Unstage addition [y,n,q,a,d%s,?]? "),
                hunk => N__("Unstage this hunk [y,n,q,a,d%s,?]? "),
        },
        reset_nothead => {
                mode => N__("Apply mode change to index [y,n,q,a,d%s,?]? "),
                deletion => N__("Apply deletion to index [y,n,q,a,d%s,?]? "),
+               addition => N__("Apply addition to index [y,n,q,a,d%s,?]? "),
                hunk => N__("Apply this hunk to index [y,n,q,a,d%s,?]? "),
        },
        checkout_index => {
                mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
                deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
+               addition => N__("Discard addition from worktree [y,n,q,a,d%s,?]? "),
                hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
        },
        checkout_head => {
                mode => N__("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
                deletion => N__("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
+               addition => N__("Discard addition from index and worktree [y,n,q,a,d%s,?]? "),
                hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
        },
        checkout_nothead => {
                mode => N__("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
                deletion => N__("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
+               addition => N__("Apply addition to index and worktree [y,n,q,a,d%s,?]? "),
                hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
        },
        worktree_head => {
                mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
                deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
+               addition => N__("Discard addition from worktree [y,n,q,a,d%s,?]? "),
                hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
        },
        worktree_nothead => {
                mode => N__("Apply mode change to worktree [y,n,q,a,d%s,?]? "),
                deletion => N__("Apply deletion to worktree [y,n,q,a,d%s,?]? "),
+               addition => N__("Apply addition to worktree [y,n,q,a,d%s,?]? "),
                hunk => N__("Apply this hunk to worktree [y,n,q,a,d%s,?]? "),
        },
 );
@@ -1476,7 +1487,7 @@ sub patch_update_file {
        my ($ix, $num);
        my $path = shift;
        my ($head, @hunk) = parse_diff($path);
-       ($head, my $mode, my $deletion) = parse_diff_header($head);
+       ($head, my $mode, my $deletion, my $addition) = parse_diff_header($head);
        for (@{$head->{DISPLAY}}) {
                print;
        }
@@ -1490,6 +1501,12 @@ sub patch_update_file {
                        push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
                }
                @hunk = ($deletion);
+       } elsif (@{$addition->{TEXT}}) {
+               foreach my $hunk (@hunk) {
+                       push @{$addition->{TEXT}}, @{$hunk->{TEXT}};
+                       push @{$addition->{DISPLAY}}, @{$hunk->{DISPLAY}};
+               }
+               @hunk = ($addition);
        }
 
        $num = scalar @hunk;
index ac43f835a5b0778d26871a2bd73fa894d53e80ff..7bc0c3fe6efb5cab830fb3f2c5aff40aa562da11 100755 (executable)
@@ -403,6 +403,25 @@ test_expect_success 'deleting an empty file' '
        diff_cmp expected diff
 '
 
+test_expect_success 'adding an empty file' '
+       git init added &&
+       (
+               cd added &&
+               test_commit initial &&
+               >empty &&
+               git add empty &&
+               test_tick &&
+               git commit -m empty &&
+               git tag added-file &&
+               git reset --hard HEAD^ &&
+               test_path_is_missing empty &&
+
+               echo y | git checkout -p added-file -- >actual &&
+               test_path_is_file empty &&
+               test_i18ngrep "Apply addition to index and worktree" actual
+       )
+'
+
 test_expect_success 'split hunk setup' '
        git reset --hard &&
        test_write_lines 10 20 30 40 50 60 >test &&