checkout -p: handle new files correctly
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>
			
			
				maint
			
			
		
							parent
							
								
									b2627cc3d4
								
							
						
					
					
						commit
						2c8bd8471a
					
				
							
								
								
									
										30
									
								
								add-patch.c
								
								
								
								
							
							
						
						
									
										30
									
								
								add-patch.c
								
								
								
								
							|  | @ -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 | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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 && | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Johannes Schindelin
						Johannes Schindelin