add-patch: classify '@' as a synonym for 'HEAD'

Currently, (restore, checkout, reset) commands correctly take '@' as a synonym for 'HEAD'. However, in patch mode different prompts/messages are given on command line due to patch mode machinery not considering '@' to be a synonym for 'HEAD' due to literal string comparison with the word 'HEAD', and therefore assigning patch_mode_($command)_nothead and triggering reverse mode (-R in diff-index). The NEEDSWORK comment suggested comparing commit objects to get around this. However, doing so would also take a non-checked out branch pointing to the same commit as HEAD, as HEAD. This would cause confusion to the user. Therefore, after parsing '@', replace it with 'HEAD' as reasonably early as possible. This also solves another problem of disparity between 'git checkout HEAD' and 'git checkout @' (latter detaches at the HEAD commit and the former does not). Trade-offs: - Some of the errors would show the revision argument as 'HEAD' when given '@'. This should be fine, as most users who probably use '@' would be aware that it is a shortcut for 'HEAD' and most probably used to use 'HEAD'. There is also relevant documentation in 'gitrevisions' manpage about '@' being the shortcut for 'HEAD'. Also, the simplicity of the solution far outweighs this cost. - Consider '@' as a shortcut for 'HEAD' even if 'refs/heads/@' exists at a different commit. Naming a branch '@' is an obvious foot-gun and many existing commands already take '@' for 'HEAD' even if 'refs/heads/@' exists at a different commit or does not exist at all (e.g. 'git log @', 'git push origin @' etc.). Therefore this is an existing assumption and should not be a problem. Helped-by: Junio C Hamano <gitster@pobox.com> Helped-by: Phillip Wood <phillip.wood123@gmail.com> Signed-off-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Ghanshyam Thakkar committed Feb 13, 2024 at 05:35 UTC 5a8ed3fe450caad740b1c236b3f14151f84bed05
7 files changed +65 -43
add-patch.c
-8
@@ -1729,14 +1729,6 @@ int run_add_p(struct repository *r, enum add_p_mode mode,
1729 if (mode == ADD_P_STASH)
1730 s.mode = &patch_mode_stash;
1731 else if (mode == ADD_P_RESET) {
1732 - /*
1733 - * NEEDSWORK: Instead of comparing to the literal "HEAD",
1734 - * compare the commit objects instead so that other ways of
1735 - * saying the same thing (such as "@") are also handled
1736 - * appropriately.
1737 - *
1738 - * This applies to the cases below too.
1739 - */
1732 if (!revision || !strcmp(revision, "HEAD"))
1733 s.mode = &patch_mode_reset_head;
1734 else
builtin/checkout.c
+3 -1
@@ -1224,7 +1224,9 @@ static void setup_new_branch_info_and_source_tree(
1224 struct tree **source_tree = &opts->source_tree;
1225 struct object_id branch_rev;
1226
1227 - new_branch_info->name = xstrdup(arg);
1227 + /* treat '@' as a shortcut for 'HEAD' */
1228 + new_branch_info->name = !strcmp(arg, "@") ? xstrdup("HEAD") :
1229 + xstrdup(arg);
1230 setup_branch_path(new_branch_info);
1231
1232 if (!check_refname_format(new_branch_info->path, 0) &&
builtin/reset.c
+3 -1
@@ -281,7 +281,9 @@ static void parse_args(struct pathspec *pathspec,
281 verify_filename(prefix, argv[0], 1);
282 }
283 }
284 - *rev_ret = rev;
284 +
285 + /* treat '@' as a shortcut for 'HEAD' */
286 + *rev_ret = !strcmp("@", rev) ? "HEAD" : rev;
287
288 parse_pathspec(pathspec, 0,
289 PATHSPEC_PREFER_FULL |
t/t2016-checkout-patch.sh
+26 -20
@@ -38,26 +38,32 @@ test_expect_success 'git checkout -p with staged changes' '
38 verify_state dir/foo index index
39 '
40
41 -test_expect_success 'git checkout -p HEAD with NO staged changes: abort' '
42 - set_and_save_state dir/foo work head &&
43 - test_write_lines n y n | git checkout -p HEAD &&
44 - verify_saved_state bar &&
45 - verify_saved_state dir/foo
46 -'
47 -
48 -test_expect_success 'git checkout -p HEAD with NO staged changes: apply' '
49 - test_write_lines n y y | git checkout -p HEAD &&
50 - verify_saved_state bar &&
51 - verify_state dir/foo head head
52 -'
53 -
54 -test_expect_success 'git checkout -p HEAD with change already staged' '
55 - set_state dir/foo index index &&
56 - # the third n is to get out in case it mistakenly does not apply
57 - test_write_lines n y n | git checkout -p HEAD &&
58 - verify_saved_state bar &&
59 - verify_state dir/foo head head
60 -'
41 +for opt in "HEAD" "@"
42 +do
43 + test_expect_success "git checkout -p $opt with NO staged changes: abort" '
44 + set_and_save_state dir/foo work head &&
45 + test_write_lines n y n | git checkout -p $opt >output &&
46 + verify_saved_state bar &&
47 + verify_saved_state dir/foo &&
48 + test_grep "Discard" output
49 + '
50 +
51 + test_expect_success "git checkout -p $opt with NO staged changes: apply" '
52 + test_write_lines n y y | git checkout -p $opt >output &&
53 + verify_saved_state bar &&
54 + verify_state dir/foo head head &&
55 + test_grep "Discard" output
56 + '
57 +
58 + test_expect_success "git checkout -p $opt with change already staged" '
59 + set_state dir/foo index index &&
60 + # the third n is to get out in case it mistakenly does not apply
61 + test_write_lines n y n | git checkout -p $opt >output &&
62 + verify_saved_state bar &&
63 + verify_state dir/foo head head &&
64 + test_grep "Discard" output
65 + '
66 +done
67
68 test_expect_success 'git checkout -p HEAD^...' '
69 # the third n is to get out in case it mistakenly does not apply
t/t2020-checkout-detach.sh
+12
@@ -45,6 +45,18 @@ test_expect_success 'checkout branch does not detach' '
45 check_not_detached
46 '
47
48 +for opt in "HEAD" "@"
49 +do
50 + test_expect_success "checkout $opt no-op/don't detach" '
51 + reset &&
52 + cat .git/HEAD >expect &&
53 + git checkout $opt &&
54 + cat .git/HEAD >actual &&
55 + check_not_detached &&
56 + test_cmp expect actual
57 + '
58 +done
59 +
60 test_expect_success 'checkout tag detaches' '
61 reset &&
62 git checkout tag &&
t/t2071-restore-patch.sh
+11 -7
@@ -44,13 +44,17 @@ test_expect_success PERL 'git restore -p with staged changes' '
44 verify_state dir/foo index index
45 '
46
47 -test_expect_success PERL 'git restore -p --source=HEAD' '
48 - set_state dir/foo work index &&
49 - # the third n is to get out in case it mistakenly does not apply
50 - test_write_lines n y n | git restore -p --source=HEAD &&
51 - verify_saved_state bar &&
52 - verify_state dir/foo head index
53 -'
47 +for opt in "HEAD" "@"
48 +do
49 + test_expect_success PERL "git restore -p --source=$opt" '
50 + set_state dir/foo work index &&
51 + # the third n is to get out in case it mistakenly does not apply
52 + test_write_lines n y n | git restore -p --source=$opt >output &&
53 + verify_saved_state bar &&
54 + verify_state dir/foo head index &&
55 + test_grep "Discard" output
56 + '
57 +done
58
59 test_expect_success PERL 'git restore -p --source=HEAD^' '
60 set_state dir/foo work index &&
t/t7105-reset-patch.sh
+10 -6
@@ -26,12 +26,16 @@ test_expect_success PERL 'saying "n" does nothing' '
26 verify_saved_state bar
27 '
28
29 -test_expect_success PERL 'git reset -p' '
30 - test_write_lines n y | git reset -p >output &&
31 - verify_state dir/foo work head &&
32 - verify_saved_state bar &&
33 - test_grep "Unstage" output
34 -'
29 +for opt in "HEAD" "@" ""
30 +do
31 + test_expect_success PERL "git reset -p $opt" '
32 + set_and_save_state dir/foo work work &&
33 + test_write_lines n y | git reset -p $opt >output &&
34 + verify_state dir/foo work head &&
35 + verify_saved_state bar &&
36 + test_grep "Unstage" output
37 + '
38 +done
39
40 test_expect_success PERL 'git reset -p HEAD^' '
41 test_write_lines n y | git reset -p HEAD^ >output &&