diff mbox series

[v2] switch: mention the --detach option when dying due to lack of a branch

Message ID 20220224064710.2252637-1-alexhenrie24@gmail.com (mailing list archive)
State Superseded
Headers show
Series [v2] switch: mention the --detach option when dying due to lack of a branch | expand

Commit Message

Alex Henrie Feb. 24, 2022, 6:47 a.m. UTC
Users who are accustomed to doing `git checkout <tag>` assume that
`git switch <tag>` will do the same thing. Inform them of the --detach
option so they aren't left wondering why `git switch` doesn't work but
`git checkout` does.

Signed-off-by: Alex Henrie <alexhenrie24@gmail.com>
---
v2:
- Rephrase advice
- Print advice after error message
- Create a new config variable to suppress the advice
- Add tests
---
 Documentation/config/advice.txt |  3 +++
 advice.c                        |  1 +
 advice.h                        |  1 +
 builtin/checkout.c              | 30 +++++++++++++++++++-----------
 t/t2060-switch.sh               | 11 +++++++++++
 5 files changed, 35 insertions(+), 11 deletions(-)

Comments

Ævar Arnfjörð Bjarmason Feb. 24, 2022, 8:53 a.m. UTC | #1
On Wed, Feb 23 2022, Alex Henrie wrote:

> Users who are accustomed to doing `git checkout <tag>` assume that
> `git switch <tag>` will do the same thing. Inform them of the --detach
> option so they aren't left wondering why `git switch` doesn't work but
> `git checkout` does.

Thanks, this looks good to me! FWIW while testing this v2 I came up with
this on top, which you may or may not want (some unrelated changes):

1. We had a to_free but didn't free it, now we do. Didn't matter with
   SANITIZE=leak, but FWIW valgrind with "valgrind --leak-check=full
   --show-leak-kinds=all" is marginally happier

2. Maybe s/code = /return / with a helper is easier to read, maybe not.

3. That #2 makes the code/advice wrapper simpler (also with the to_free)

4. Used test_cmp in the test to check the actual output we got, and added
   a missing test for the "tag" case.

   In any case s/test_i18ngrep/grep/ is the right thing nowadays for new code.

diff --git a/builtin/checkout.c b/builtin/checkout.c
index 9244827ca02..9e4d03343fa 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1393,34 +1393,39 @@ static int switch_unborn_to_new_branch(const struct checkout_opts *opts)
 	return status;
 }
 
-static void die_expecting_a_branch(const struct branch_info *branch_info)
+static int die_expecting_a_branch_message(const struct branch_info *branch_info, char **to_free)
 {
 	struct object_id oid;
-	char *to_free;
-	int code;
 
-	if (dwim_ref(branch_info->name, strlen(branch_info->name), &oid, &to_free, 0) == 1) {
-		const char *ref = to_free;
+	if (dwim_ref(branch_info->name, strlen(branch_info->name), &oid, to_free, 0) == 1) {
+		const char *ref = *to_free;
 
 		if (skip_prefix(ref, "refs/tags/", &ref))
-			code = die_message(_("a branch is expected, got tag '%s'"), ref);
+			return die_message(_("a branch is expected, got tag '%s'"), ref);
 		else if (skip_prefix(ref, "refs/remotes/", &ref))
-			code = die_message(_("a branch is expected, got remote branch '%s'"), ref);
+			return die_message(_("a branch is expected, got remote branch '%s'"), ref);
 		else
-			code = die_message(_("a branch is expected, got '%s'"), ref);
+			return die_message(_("a branch is expected, got '%s'"), ref);
 	}
 	else if (branch_info->commit)
-		code = die_message(_("a branch is expected, got commit '%s'"), branch_info->name);
+		return die_message(_("a branch is expected, got commit '%s'"), branch_info->name);
 	else
 		/*
 		 * This case should never happen because we already die() on
 		 * non-commit, but just in case.
 		 */
-		code = die_message(_("a branch is expected, got '%s'"), branch_info->name);
+		return die_message(_("a branch is expected, got '%s'"), branch_info->name);
+}
+
+static void die_expecting_a_branch(const struct branch_info *branch_info)
+{
+	char *to_free = NULL;
+	int code = die_expecting_a_branch_message(branch_info, &to_free);
 
 	if (advice_enabled(ADVICE_SUGGEST_DETACHING_HEAD))
 		advise(_("If you want to detach HEAD at the commit, try again with the --detach option."));
 
+	free(to_free);
 	exit(code);
 }
 
diff --git a/t/t2060-switch.sh b/t/t2060-switch.sh
index f54691bac9f..0708359ee24 100755
--- a/t/t2060-switch.sh
+++ b/t/t2060-switch.sh
@@ -32,15 +32,31 @@ test_expect_success 'switch and detach' '
 	test_must_fail git symbolic-ref HEAD
 '
 
-test_expect_success 'suggestion to detach' '
-	test_must_fail git switch main^{commit} 2>stderr &&
-	test_i18ngrep "try again with the --detach option" stderr
+test_expect_success 'suggestion to detach commit' '
+	test_must_fail git switch main^{commit} 2>actual &&
+	cat >expect <<-\EOF &&
+	fatal: a branch is expected, got commit '\''main^{commit}'\''
+	hint: If you want to detach HEAD at the commit, try again with the --detach option.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'suggestion to detach tag' '
+	test_must_fail git switch first 2>actual &&
+	cat >expect <<-\EOF &&
+	fatal: a branch is expected, got tag '\''first'\''
+	hint: If you want to detach HEAD at the commit, try again with the --detach option.
+	EOF
+	test_cmp expect actual
 '
 
 test_expect_success 'suggestion to detach is suppressed with advice.suggestDetachingHead=false' '
 	test_config advice.suggestDetachingHead false &&
-	test_must_fail git switch main^{commit} 2>stderr &&
-	test_i18ngrep ! "try again with the --detach option" stderr
+	test_must_fail git switch main^{commit} 2>actual &&
+	cat >expect <<-\EOF &&
+	fatal: a branch is expected, got commit '\''main^{commit}'\''
+	EOF
+	test_cmp expect actual
 '
 
 test_expect_success 'switch and detach current branch' '
Junio C Hamano Feb. 24, 2022, 7:02 p.m. UTC | #2
Alex Henrie <alexhenrie24@gmail.com> writes:

> diff --git a/builtin/checkout.c b/builtin/checkout.c
> index d9b31bbb6d..9244827ca0 100644
> --- a/builtin/checkout.c
> +++ b/builtin/checkout.c
> @@ -1397,23 +1397,31 @@ static void die_expecting_a_branch(const struct branch_info *branch_info)
>  {
>  	struct object_id oid;
>  	char *to_free;
> +	int code;
>  
>  	if (dwim_ref(branch_info->name, strlen(branch_info->name), &oid, &to_free, 0) == 1) {
>  		const char *ref = to_free;
>  
>  		if (skip_prefix(ref, "refs/tags/", &ref))
> -			die(_("a branch is expected, got tag '%s'"), ref);
> -		if (skip_prefix(ref, "refs/remotes/", &ref))
> -			die(_("a branch is expected, got remote branch '%s'"), ref);
> -		die(_("a branch is expected, got '%s'"), ref);
> +			code = die_message(_("a branch is expected, got tag '%s'"), ref);
> +		else if (skip_prefix(ref, "refs/remotes/", &ref))
> +			code = die_message(_("a branch is expected, got remote branch '%s'"), ref);
> +		else
> +			code = die_message(_("a branch is expected, got '%s'"), ref);
>  	}
> -	if (branch_info->commit)
> -		die(_("a branch is expected, got commit '%s'"), branch_info->name);

In the original code, when dwim_ref() said there is a unique hit, we
died with varying messages.  So it was OK to have this check not as
a part of if/elseif/... cascade.

> -	/*
> -	 * This case should never happen because we already die() on
> -	 * non-commit, but just in case.
> -	 */
> -	die(_("a branch is expected, got '%s'"), branch_info->name);
> +	else if (branch_info->commit)

But now, new code only sets code without dying, so we avoid
overwriting the code (and calling die_message() twice) by turning it
"else if".  OK.

> +		code = die_message(_("a branch is expected, got commit '%s'"), branch_info->name);
> +	else
> +		/*
> +		 * This case should never happen because we already die() on
> +		 * non-commit, but just in case.
> +		 */
> +		code = die_message(_("a branch is expected, got '%s'"), branch_info->name);

OK.  So "code" gets assigned exactly once in the above
if/elseif/... cascade.  Not defining the variable with
initialization at the beginning of this function is correct.

> +	if (advice_enabled(ADVICE_SUGGEST_DETACHING_HEAD))
> +		advise(_("If you want to detach HEAD at the commit, try again with the --detach option."));
> +
> +	exit(code);
>  }

OK.

> diff --git a/t/t2060-switch.sh b/t/t2060-switch.sh
> index ebb961be29..f54691bac9 100755
> --- a/t/t2060-switch.sh
> +++ b/t/t2060-switch.sh
> @@ -32,6 +32,17 @@ test_expect_success 'switch and detach' '
>  	test_must_fail git symbolic-ref HEAD
>  '
>  
> +test_expect_success 'suggestion to detach' '
> +	test_must_fail git switch main^{commit} 2>stderr &&
> +	test_i18ngrep "try again with the --detach option" stderr
> +'
> +
> +test_expect_success 'suggestion to detach is suppressed with advice.suggestDetachingHead=false' '
> +	test_config advice.suggestDetachingHead false &&
> +	test_must_fail git switch main^{commit} 2>stderr &&
> +	test_i18ngrep ! "try again with the --detach option" stderr
> +'

OK, we try to be consistent with other tests in the file, and leave
s/test_i18n// to a file-wide clean-up outside the topic.

>  test_expect_success 'switch and detach current branch' '
>  	test_when_finished git switch main &&
>  	git switch main &&
Ævar Arnfjörð Bjarmason Feb. 25, 2022, 11:57 a.m. UTC | #3
On Thu, Feb 24 2022, Junio C Hamano wrote:

> Alex Henrie <alexhenrie24@gmail.com> writes:
>> diff --git a/t/t2060-switch.sh b/t/t2060-switch.sh
>> index ebb961be29..f54691bac9 100755
>> --- a/t/t2060-switch.sh
>> +++ b/t/t2060-switch.sh
>> @@ -32,6 +32,17 @@ test_expect_success 'switch and detach' '
>>  	test_must_fail git symbolic-ref HEAD
>>  '
>>  
>> +test_expect_success 'suggestion to detach' '
>> +	test_must_fail git switch main^{commit} 2>stderr &&
>> +	test_i18ngrep "try again with the --detach option" stderr
>> +'
>> +
>> +test_expect_success 'suggestion to detach is suppressed with advice.suggestDetachingHead=false' '
>> +	test_config advice.suggestDetachingHead false &&
>> +	test_must_fail git switch main^{commit} 2>stderr &&
>> +	test_i18ngrep ! "try again with the --detach option" stderr
>> +'
>
> OK, we try to be consistent with other tests in the file, and leave
> s/test_i18n// to a file-wide clean-up outside the topic.

FWIW that's not the case here. This is the first use of test_i18ngrep in
this file.

But better to use test_cmp as noted in
<220224.86sfs8abj6.gmgdl@evledraar.gmail.com> in the sid-thread.
Alex Henrie Feb. 25, 2022, 5:20 p.m. UTC | #4
On Fri, Feb 25, 2022 at 4:57 AM Ævar Arnfjörð Bjarmason
<avarab@gmail.com> wrote:
>
> On Thu, Feb 24 2022, Junio C Hamano wrote:
>
> > Alex Henrie <alexhenrie24@gmail.com> writes:
> >> diff --git a/t/t2060-switch.sh b/t/t2060-switch.sh
> >> index ebb961be29..f54691bac9 100755
> >> --- a/t/t2060-switch.sh
> >> +++ b/t/t2060-switch.sh
> >> @@ -32,6 +32,17 @@ test_expect_success 'switch and detach' '
> >>      test_must_fail git symbolic-ref HEAD
> >>  '
> >>
> >> +test_expect_success 'suggestion to detach' '
> >> +    test_must_fail git switch main^{commit} 2>stderr &&
> >> +    test_i18ngrep "try again with the --detach option" stderr
> >> +'
> >> +
> >> +test_expect_success 'suggestion to detach is suppressed with advice.suggestDetachingHead=false' '
> >> +    test_config advice.suggestDetachingHead false &&
> >> +    test_must_fail git switch main^{commit} 2>stderr &&
> >> +    test_i18ngrep ! "try again with the --detach option" stderr
> >> +'
> >
> > OK, we try to be consistent with other tests in the file, and leave
> > s/test_i18n// to a file-wide clean-up outside the topic.
>
> FWIW that's not the case here. This is the first use of test_i18ngrep in
> this file.
>
> But better to use test_cmp as noted in
> <220224.86sfs8abj6.gmgdl@evledraar.gmail.com> in the sid-thread.

Why is test_cmp preferable to grep in tests like this?

-Alex
Junio C Hamano Feb. 25, 2022, 5:26 p.m. UTC | #5
Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

>>> +	test_i18ngrep ! "try again with the --detach option" stderr
>>> +'
>>
>> OK, we try to be consistent with other tests in the file, and leave
>> s/test_i18n// to a file-wide clean-up outside the topic.
>
> FWIW that's not the case here. This is the first use of test_i18ngrep in
> this file.

Oh, thanks for pointing it out---I remembered that somebody gave a
similar suggestion and blindly assumed that there are other
instances already.  If so, picking, between grep and test_cmp,
whichever requires the least amount of boilerplate code is fine by
me.

Thanks.
diff mbox series

Patch

diff --git a/Documentation/config/advice.txt b/Documentation/config/advice.txt
index adee26fbbb..c40eb09cb7 100644
--- a/Documentation/config/advice.txt
+++ b/Documentation/config/advice.txt
@@ -85,6 +85,9 @@  advice.*::
 		linkgit:git-switch[1] or linkgit:git-checkout[1]
 		to move to the detach HEAD state, to instruct how to
 		create a local branch after the fact.
+	suggestDetachingHead::
+		Advice shown when linkgit:git-switch[1] refuses to detach HEAD
+		without the explicit `--detach` option.
 	checkoutAmbiguousRemoteBranchName::
 		Advice shown when the argument to
 		linkgit:git-checkout[1] and linkgit:git-switch[1]
diff --git a/advice.c b/advice.c
index e00d30254c..2e1fd48304 100644
--- a/advice.c
+++ b/advice.c
@@ -42,6 +42,7 @@  static struct {
 	[ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME] 	= { "checkoutAmbiguousRemoteBranchName", 1 },
 	[ADVICE_COMMIT_BEFORE_MERGE]			= { "commitBeforeMerge", 1 },
 	[ADVICE_DETACHED_HEAD]				= { "detachedHead", 1 },
+	[ADVICE_SUGGEST_DETACHING_HEAD]			= { "suggestDetachingHead", 1 },
 	[ADVICE_FETCH_SHOW_FORCED_UPDATES]		= { "fetchShowForcedUpdates", 1 },
 	[ADVICE_GRAFT_FILE_DEPRECATED]			= { "graftFileDeprecated", 1 },
 	[ADVICE_IGNORED_HOOK]				= { "ignoredHook", 1 },
diff --git a/advice.h b/advice.h
index a7521d6087..a3957123a1 100644
--- a/advice.h
+++ b/advice.h
@@ -20,6 +20,7 @@  struct string_list;
 	ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME,
 	ADVICE_COMMIT_BEFORE_MERGE,
 	ADVICE_DETACHED_HEAD,
+	ADVICE_SUGGEST_DETACHING_HEAD,
 	ADVICE_FETCH_SHOW_FORCED_UPDATES,
 	ADVICE_GRAFT_FILE_DEPRECATED,
 	ADVICE_IGNORED_HOOK,
diff --git a/builtin/checkout.c b/builtin/checkout.c
index d9b31bbb6d..9244827ca0 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1397,23 +1397,31 @@  static void die_expecting_a_branch(const struct branch_info *branch_info)
 {
 	struct object_id oid;
 	char *to_free;
+	int code;
 
 	if (dwim_ref(branch_info->name, strlen(branch_info->name), &oid, &to_free, 0) == 1) {
 		const char *ref = to_free;
 
 		if (skip_prefix(ref, "refs/tags/", &ref))
-			die(_("a branch is expected, got tag '%s'"), ref);
-		if (skip_prefix(ref, "refs/remotes/", &ref))
-			die(_("a branch is expected, got remote branch '%s'"), ref);
-		die(_("a branch is expected, got '%s'"), ref);
+			code = die_message(_("a branch is expected, got tag '%s'"), ref);
+		else if (skip_prefix(ref, "refs/remotes/", &ref))
+			code = die_message(_("a branch is expected, got remote branch '%s'"), ref);
+		else
+			code = die_message(_("a branch is expected, got '%s'"), ref);
 	}
-	if (branch_info->commit)
-		die(_("a branch is expected, got commit '%s'"), branch_info->name);
-	/*
-	 * This case should never happen because we already die() on
-	 * non-commit, but just in case.
-	 */
-	die(_("a branch is expected, got '%s'"), branch_info->name);
+	else if (branch_info->commit)
+		code = die_message(_("a branch is expected, got commit '%s'"), branch_info->name);
+	else
+		/*
+		 * This case should never happen because we already die() on
+		 * non-commit, but just in case.
+		 */
+		code = die_message(_("a branch is expected, got '%s'"), branch_info->name);
+
+	if (advice_enabled(ADVICE_SUGGEST_DETACHING_HEAD))
+		advise(_("If you want to detach HEAD at the commit, try again with the --detach option."));
+
+	exit(code);
 }
 
 static void die_if_some_operation_in_progress(void)
diff --git a/t/t2060-switch.sh b/t/t2060-switch.sh
index ebb961be29..f54691bac9 100755
--- a/t/t2060-switch.sh
+++ b/t/t2060-switch.sh
@@ -32,6 +32,17 @@  test_expect_success 'switch and detach' '
 	test_must_fail git symbolic-ref HEAD
 '
 
+test_expect_success 'suggestion to detach' '
+	test_must_fail git switch main^{commit} 2>stderr &&
+	test_i18ngrep "try again with the --detach option" stderr
+'
+
+test_expect_success 'suggestion to detach is suppressed with advice.suggestDetachingHead=false' '
+	test_config advice.suggestDetachingHead false &&
+	test_must_fail git switch main^{commit} 2>stderr &&
+	test_i18ngrep ! "try again with the --detach option" stderr
+'
+
 test_expect_success 'switch and detach current branch' '
 	test_when_finished git switch main &&
 	git switch main &&