Volume XXII, number 280Wednesday, October 7, 2026Latest message 1 hour ago

The Git List

News and archive of git@vger.kernel.org, since April 2005

RFC patchswitch: provide configurable detach

9 messages between Apr 4, 2026 and Apr 7, 2026, from Thibaud CANALE, Pablo, D. Ben Knoble, Junio C Hamano, Kristoffer Haugsbakk.

Plain Markdown or JSON for tools and agents. Diffs are folded; open one to read it.

Thibaud CANALEApr 4, 2026, 14:28 UTC on lore

Its purpose is to provide for git-switch(1) same detach behavior on commit it than git-checkout(1) through configuration option `checkout.switchDetach`.

Signed-off-by: Thibaud CANALE <thican@thican.net>
---
 Documentation/config/checkout.adoc |  4 ++++
 builtin/checkout.c                 | 18 +++++++++++++++---
 t/t2060-switch.sh                  | 27 +++++++++++++++++++++++++++
 3 files changed, 46 insertions(+), 3 deletions(-)
Show changes to 3 files +46 −3

Documentation/config/checkout.adoc, builtin/checkout.c, t/t2060-switch.sh

diff --git a/Documentation/config/checkout.adoc b/Documentation/config/checkout.adoc
index e35d2129..3a6c1049 100644
--- a/Documentation/config/checkout.adoc
+++ b/Documentation/config/checkout.adoc
@@ -22,6 +22,10 @@ commands or functionality in the future.
 	option in `git checkout` and `git switch`. See
 	linkgit:git-switch[1] and linkgit:git-checkout[1].
 
+`checkout.switchDetach`::
+	Provides for linkgit:git-switch[1] the same detach behavior on commit id
+	than linkgit:git-checkout[1] without the explicit --detach option.
+
 `checkout.workers`::
 	The number of parallel workers to use when updating the working tree.
 	The default is one, i.e. sequential execution. If set to a value less
diff --git a/builtin/checkout.c b/builtin/checkout.c
index e031e618..1da1062e 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -53,6 +53,7 @@ struct checkout_opts {
 	int force;
 	int force_detach;
 	int implicit_detach;
+	int switch_detach;  // For checkout.switchDetach configuration
 	int writeout_stage;
 	int overwrite_ignore;
 	int ignore_skipworktree;
@@ -1005,7 +1006,10 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 	else
 		strbuf_insertstr(&msg, 0, reflog_msg);
 
-	if (!strcmp(new_branch_info->name, "HEAD") && !new_branch_info->path && !opts->force_detach) {
+	if (!strcmp(new_branch_info->name, "HEAD") &&
+	    !new_branch_info->path &&
+	    !opts->force_detach &&
+	    !opts->switch_detach) {
 		/* Nothing to do. */
 	} else if (opts->force_detach || !new_branch_info->path) {	/* No longer on any branch. */
 		refs_update_ref(get_main_ref_store(the_repository), msg.buf,
@@ -1014,7 +1018,9 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 				REF_NO_DEREF, UPDATE_REFS_DIE_ON_ERR);
 		if (!opts->quiet) {
 			if (old_branch_info->path &&
-			    advice_enabled(ADVICE_DETACHED_HEAD) && !opts->force_detach)
+			    advice_enabled(ADVICE_DETACHED_HEAD) &&
+			    !opts->force_detach &&
+			    !opts->switch_detach)
 				detach_advice(new_branch_info->name);
 			describe_detached_head(_("HEAD is now at"), new_branch_info->commit);
 		}
@@ -1541,8 +1547,11 @@ static void die_expecting_a_branch(const struct branch_info *branch_info)
 		 */
 		code = die_message(_("a branch is expected, got '%s'"), branch_info->name);
 
-	if (advice_enabled(ADVICE_SUGGEST_DETACHING_HEAD))
+	if (advice_enabled(ADVICE_SUGGEST_DETACHING_HEAD)) {
 		advise(_("If you want to detach HEAD at the commit, try again with the --detach option."));
+		advise(_("Also you can detach by default by setting the config variable "
+		         "checkout.switchDetach to true."));
+	}
 
 	exit(code);
 }
@@ -1660,6 +1669,7 @@ static int checkout_branch(struct checkout_opts *opts,
 
 	if (!opts->implicit_detach &&
 	    !opts->force_detach &&
+	    !opts->switch_detach &&
 	    !opts->new_branch &&
 	    !opts->new_branch_force &&
 	    new_branch_info->name &&
@@ -2119,6 +2129,8 @@ int cmd_switch(int argc,
 	options = add_common_options(&opts, options);
 	options = add_common_switch_branch_options(&opts, options);
 
+	repo_config_get_bool(the_repository, "checkout.switchDetach", &opts.switch_detach);
+
 	cb_option = 'c';
 
 	return checkout_main(argc, argv, prefix, &opts, options,
diff --git a/t/t2060-switch.sh b/t/t2060-switch.sh
index c91c4db9..3435ae98 100755
--- a/t/t2060-switch.sh
+++ b/t/t2060-switch.sh
@@ -177,4 +177,31 @@ test_expect_success 'switch back when temporarily detached and checked out elsew
 	git -C wt2 switch --ignore-other-worktrees shared
 '
 
+test_expect_success 'switch with configured detach behavior from configuration ' '
+	test_when_finished "
+		test_config checkout.switchDetach false;
+		git switch main
+	" &&
+	test_config checkout.switchDetach true &&
+	git switch main &&
+	git symbolic-ref HEAD &&
+	git switch main~ &&
+	test_must_fail git symbolic-ref HEAD &&
+	git switch - &&
+	git symbolic-ref HEAD &&
+	git switch - &&
+	test_must_fail git symbolic-ref HEAD
+'
+
+test_expect_success 'switch without configured detach behavior from configuration ' '
+	test_when_finished "
+		test_config checkout.switchDetach false;
+		git switch main
+	" &&
+	test_config checkout.switchDetach false &&
+	git switch main &&
+	git symbolic-ref HEAD &&
+	test_must_fail git switch main~
+'
+
 test_done

base-commit: 2855562ca6a9c6b0e7bc780b050c1e83c9fcfbd0
-- 
Thibaud CANALE
thican [at] thican [dot] net
https://thican.net/
GPG: rsa4096 2013-10-14 485EF628CB85CDD4CB7CFF0D52F5127650733A18
PabloApr 4, 2026, 15:23 UTC in reply to Thibaud CANALE on lore

Re: [RFC PATCH] switch: provide configurable detach

El sáb, 4 abr 2026 a las 16:36, Thibaud CANALE (<thican@thican.net>) escribió:
Hi Thibaud,
>
> Its purpose is to provide for git-switch(1) same detach behavior on
> commit it than git-checkout(1) through configuration option
> `checkout.switchDetach`.

The commit needs some work, following Documentation/SubmittingPatches: The subject looks fine but the body, you should write imperatively, something like "Allow git-switch to..." instead of "Its purpose is to...".

>  same detach behavior on commit it than
This is confusing.
Also the body should explain WHY this change would be wanted.
Show 20 quoted lines
>
> Signed-off-by: Thibaud CANALE <thican@thican.net>
> ---
>  Documentation/config/checkout.adoc |  4 ++++
>  builtin/checkout.c                 | 18 +++++++++++++++---
>  t/t2060-switch.sh                  | 27 +++++++++++++++++++++++++++
>  3 files changed, 46 insertions(+), 3 deletions(-)
>
> diff --git a/Documentation/config/checkout.adoc b/Documentation/config/checkout.adoc
> index e35d2129..3a6c1049 100644
> --- a/Documentation/config/checkout.adoc
> +++ b/Documentation/config/checkout.adoc
> @@ -22,6 +22,10 @@ commands or functionality in the future.
>         option in `git checkout` and `git switch`. See
>         linkgit:git-switch[1] and linkgit:git-checkout[1].
>
> +`checkout.switchDetach`::
> +       Provides for linkgit:git-switch[1] the same detach behavior on commit id
> +       than linkgit:git-checkout[1] without the explicit --detach option.
> +

I think it would be better to describe how it would be if set rather than directly the what it does. e.g.: "If set, detaches HEAD when..."

Show 12 quoted lines
>  `checkout.workers`::
>         The number of parallel workers to use when updating the working tree.
>         The default is one, i.e. sequential execution. If set to a value less
> diff --git a/builtin/checkout.c b/builtin/checkout.c
> index e031e618..1da1062e 100644
> --- a/builtin/checkout.c
> +++ b/builtin/checkout.c
> @@ -53,6 +53,7 @@ struct checkout_opts {
>         int force;
>         int force_detach;
>         int implicit_detach;
> +       int switch_detach;  // For checkout.switchDetach configuration

// coments are not used in Git, if you need to add comments make them with /* */. Nit I don't think a comment here is necessary, it is very self explanatory already.

Show 12 quoted lines
>         int writeout_stage;
>         int overwrite_ignore;
>         int ignore_skipworktree;
> @@ -1005,7 +1006,10 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
>         else
>                 strbuf_insertstr(&msg, 0, reflog_msg);
>
> -       if (!strcmp(new_branch_info->name, "HEAD") && !new_branch_info->path && !opts->force_detach) {
> +       if (!strcmp(new_branch_info->name, "HEAD") &&
> +           !new_branch_info->path &&
> +           !opts->force_detach &&
> +           !opts->switch_detach) {

seems that you're checking force_detach and switch_detach together every time,they look very similar, sure you need them separated?

Show 24 quoted lines
>                 /* Nothing to do. */
>         } else if (opts->force_detach || !new_branch_info->path) {      /* No longer on any branch. */
>                 refs_update_ref(get_main_ref_store(the_repository), msg.buf,
> @@ -1014,7 +1018,9 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
>                                 REF_NO_DEREF, UPDATE_REFS_DIE_ON_ERR);
>                 if (!opts->quiet) {
>                         if (old_branch_info->path &&
> -                           advice_enabled(ADVICE_DETACHED_HEAD) && !opts->force_detach)
> +                           advice_enabled(ADVICE_DETACHED_HEAD) &&
> +                           !opts->force_detach &&
> +                           !opts->switch_detach)
>                                 detach_advice(new_branch_info->name);
>                         describe_detached_head(_("HEAD is now at"), new_branch_info->commit);
>                 }
> @@ -1541,8 +1547,11 @@ static void die_expecting_a_branch(const struct branch_info *branch_info)
>                  */
>                 code = die_message(_("a branch is expected, got '%s'"), branch_info->name);
>
> -       if (advice_enabled(ADVICE_SUGGEST_DETACHING_HEAD))
> +       if (advice_enabled(ADVICE_SUGGEST_DETACHING_HEAD)) {
>                 advise(_("If you want to detach HEAD at the commit, try again with the --detach option."));
> +               advise(_("Also you can detach by default by setting the config variable "
> +                        "checkout.switchDetach to true."));
> +       }
Nit "Also, you can..." or "You can also..."
Show 8 quoted lines
>
>         exit(code);
>  }
> @@ -1660,6 +1669,7 @@ static int checkout_branch(struct checkout_opts *opts,
>
>         if (!opts->implicit_detach &&
>             !opts->force_detach &&
> +           !opts->switch_detach &&
Again force_detach + switch_detach
Show 21 quoted lines
>             !opts->new_branch &&
>             !opts->new_branch_force &&
>             new_branch_info->name &&
> @@ -2119,6 +2129,8 @@ int cmd_switch(int argc,
>         options = add_common_options(&opts, options);
>         options = add_common_switch_branch_options(&opts, options);
>
> +       repo_config_get_bool(the_repository, "checkout.switchDetach", &opts.switch_detach);
> +
>         cb_option = 'c';
>
>         return checkout_main(argc, argv, prefix, &opts, options,
> diff --git a/t/t2060-switch.sh b/t/t2060-switch.sh
> index c91c4db9..3435ae98 100755
> --- a/t/t2060-switch.sh
> +++ b/t/t2060-switch.sh
> @@ -177,4 +177,31 @@ test_expect_success 'switch back when temporarily detached and checked out elsew
>         git -C wt2 switch --ignore-other-worktrees shared
>  '
>
> +test_expect_success 'switch with configured detach behavior from configuration ' '

Trailing space here at the end, also the title is kinda confusing with the configured + configuration, what about: "switch detaches on commit when switchDetach is set"

Show 16 quoted lines
> +       test_when_finished "
> +               test_config checkout.switchDetach false;
> +               git switch main
> +       " &&
> +       test_config checkout.switchDetach true &&
> +       git switch main &&
> +       git symbolic-ref HEAD &&
> +       git switch main~ &&
> +       test_must_fail git symbolic-ref HEAD &&
> +       git switch - &&
> +       git symbolic-ref HEAD &&
> +       git switch - &&
> +       test_must_fail git symbolic-ref HEAD
> +'
> +
> +test_expect_success 'switch without configured detach behavior from configuration ' '
Trailing space.
> +       test_when_finished "
> +               test_config checkout.switchDetach false;

test_config already has test_when_finished I would git config --unset.

> +               git switch main
> +       " &&
> +       test_config checkout.switchDetach false &&
This is already false.
Show 13 quoted lines
> +       git switch main &&
> +       git symbolic-ref HEAD &&
> +       test_must_fail git switch main~
> +'
> +
>  test_done
>
> base-commit: 2855562ca6a9c6b0e7bc780b050c1e83c9fcfbd0
> --
> Thibaud CANALE
> thican [at] thican [dot] net
> https://thican.net/
> GPG: rsa4096 2013-10-14 485EF628CB85CDD4CB7CFF0D52F5127650733A18

Overall seems reasonable, it might be a design choice having to explicitly use --detach, force_detach it's pretty clear about it but I dunno.

For now: I would rewrite the commit. Make sure that you need a new flag, force_detach looks very similar. Fix the style issue.

You might find Documentation/CodingGuidelines and Documentation/SubmittingPatches very helpful.

Pablo
D. Ben KnobleApr 4, 2026, 16:58 UTC in reply to Thibaud CANALE on lore

Re: [RFC PATCH] switch: provide configurable detach

On Sat, Apr 4, 2026 at 10:36 AM Thibaud CANALE <thican@thican.net> wrote:
>
> Its purpose is to provide for git-switch(1) same detach behavior on
> commit it than git-checkout(1) through configuration option
> `checkout.switchDetach`.

I considered contributing something similar at one point, though I would call it "switch.detach" ("<command>.<option>").

I think I eventually decided against sending a patch because I was concerned that such an option might make using git-switch in scripts more of a headache. If I'm using it intentionally because it will fail in some circumstances without --detach, now I've got to also do "-c switch.detach=false" or something to control for the new configuration.

On the one hand, I think that argument applies mostly to plumbing rather than porcelain commands. On the other, a command for switching branches whose behavior is reliable enough for scripts seems worthwhile. So, idk :)

-- 
D. Ben Knoble
Thibaud CANALEApr 4, 2026, 21:10 UTC in reply to Pablo on lore

Re: [RFC PATCH] switch: provide configurable detach

On Saturday 2026-04-04 17:23:46+0200, Pablo <pabloosabaterr@gmail.com> wrote:
> El sáb, 4 abr 2026 a las 16:36, Thibaud CANALE (<thican@thican.net>) escribió:
> 
> Hi Thibaud,
Hello,
Show 7 quoted lines
> > Its purpose is to provide for git-switch(1) same detach behavior on
> > commit it than git-checkout(1) through configuration option
> > `checkout.switchDetach`.
> 
> The commit needs some work, following Documentation/SubmittingPatches:
> The subject looks fine but the body, you should write imperatively, something
> like "Allow git-switch to..." instead of "Its purpose is to...".

It was a (almost) copy of the message I wrote in the documentation, I wanted to avoid giving different and possibly confusing explanations between the two of them. I just noticed the typo on "commit it". By the way, is "commit id" correct for constructs such as "${branch}~x" or "${commit_id}~y", or is there a better word for this?

Show 5 quoted lines
> >  same detach behavior on commit it than
> 
> This is confusing.
> 
> Also the body should explain WHY this change would be wanted.

May you tell me what you find confusing? I don’t see what else I can explain without over-explaining.

Maybe I can provide a real case example: Using `git checkout my_branch~2` works and automatically detach on the 2nd parent of the top commit of branch "my_branch", but the same argument with `git switch` does not. The command `git switch -` also does not work if the previous step was a detached commit while `git checkout -` can move back and forth between detached mode and branch.

About why force_detach flag is necessary, read below.

About why this feature, I know some users won’t change to switch/restore commands and keep relying on checkout command because of this “missing” feature, as its “disrupts” their workflow (humans, ’am I right?).

Show 25 quoted lines
> >
> > Signed-off-by: Thibaud CANALE <thican@thican.net>
> > ---
> >  Documentation/config/checkout.adoc |  4 ++++
> >  builtin/checkout.c                 | 18 +++++++++++++++---
> >  t/t2060-switch.sh                  | 27 +++++++++++++++++++++++++++
> >  3 files changed, 46 insertions(+), 3 deletions(-)
> >
> > diff --git a/Documentation/config/checkout.adoc b/Documentation/config/checkout.adoc
> > index e35d2129..3a6c1049 100644
> > --- a/Documentation/config/checkout.adoc
> > +++ b/Documentation/config/checkout.adoc
> > @@ -22,6 +22,10 @@ commands or functionality in the future.
> >         option in `git checkout` and `git switch`. See
> >         linkgit:git-switch[1] and linkgit:git-checkout[1].
> >
> > +`checkout.switchDetach`::
> > +       Provides for linkgit:git-switch[1] the same detach behavior on commit id
> > +       than linkgit:git-checkout[1] without the explicit --detach option.
> > +
> 
> I think it would be better to describe how it would be if set rather
> than directly
> the what it does. e.g.:
> "If set, detaches HEAD when..."
Rewritten with details and example.
Show 17 quoted lines
> >  `checkout.workers`::
> >         The number of parallel workers to use when updating the working tree.
> >         The default is one, i.e. sequential execution. If set to a value less
> > diff --git a/builtin/checkout.c b/builtin/checkout.c
> > index e031e618..1da1062e 100644
> > --- a/builtin/checkout.c
> > +++ b/builtin/checkout.c
> > @@ -53,6 +53,7 @@ struct checkout_opts {
> >         int force;
> >         int force_detach;
> >         int implicit_detach;
> > +       int switch_detach;  // For checkout.switchDetach configuration
> 
> // coments are not used in Git, if you need to add comments make them with
> /* */.
> Nit I don't think a comment here is necessary, it is very self
> explanatory already.

I agree, I originally didn’t, I thought we could tell me to describe it here to avoid looking for explanation somewhere else.

Show 16 quoted lines
> >         int writeout_stage;
> >         int overwrite_ignore;
> >         int ignore_skipworktree;
> > @@ -1005,7 +1006,10 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
> >         else
> >                 strbuf_insertstr(&msg, 0, reflog_msg);
> >
> > -       if (!strcmp(new_branch_info->name, "HEAD") && !new_branch_info->path && !opts->force_detach) {
> > +       if (!strcmp(new_branch_info->name, "HEAD") &&
> > +           !new_branch_info->path &&
> > +           !opts->force_detach &&
> > +           !opts->switch_detach) {
> 
> seems that you're checking force_detach and switch_detach together
> every time,they look very similar,
> sure  you need them separated?

Not every time. There is cases where switch_detach is not used such as using a branch.

Show 26 quoted lines
> >                 /* Nothing to do. */
> >         } else if (opts->force_detach || !new_branch_info->path) {      /* No longer on any branch. */
> >                 refs_update_ref(get_main_ref_store(the_repository), msg.buf,
> > @@ -1014,7 +1018,9 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
> >                                 REF_NO_DEREF, UPDATE_REFS_DIE_ON_ERR);
> >                 if (!opts->quiet) {
> >                         if (old_branch_info->path &&
> > -                           advice_enabled(ADVICE_DETACHED_HEAD) && !opts->force_detach)
> > +                           advice_enabled(ADVICE_DETACHED_HEAD) &&
> > +                           !opts->force_detach &&
> > +                           !opts->switch_detach)
> >                                 detach_advice(new_branch_info->name);
> >                         describe_detached_head(_("HEAD is now at"), new_branch_info->commit);
> >                 }
> > @@ -1541,8 +1547,11 @@ static void die_expecting_a_branch(const struct branch_info *branch_info)
> >                  */
> >                 code = die_message(_("a branch is expected, got '%s'"), branch_info->name);
> >
> > -       if (advice_enabled(ADVICE_SUGGEST_DETACHING_HEAD))
> > +       if (advice_enabled(ADVICE_SUGGEST_DETACHING_HEAD)) {
> >                 advise(_("If you want to detach HEAD at the commit, try again with the --detach option."));
> > +               advise(_("Also you can detach by default by setting the config variable "
> > +                        "checkout.switchDetach to true."));
> > +       }
> 
> Nit "Also, you can..." or "You can also..."
As you prefer.
Show 36 quoted lines
> >
> >         exit(code);
> >  }
> > @@ -1660,6 +1669,7 @@ static int checkout_branch(struct checkout_opts *opts,
> >
> >         if (!opts->implicit_detach &&
> >             !opts->force_detach &&
> > +           !opts->switch_detach &&
> 
> Again force_detach + switch_detach
> 
> >             !opts->new_branch &&
> >             !opts->new_branch_force &&
> >             new_branch_info->name &&
> > @@ -2119,6 +2129,8 @@ int cmd_switch(int argc,
> >         options = add_common_options(&opts, options);
> >         options = add_common_switch_branch_options(&opts, options);
> >
> > +       repo_config_get_bool(the_repository, "checkout.switchDetach", &opts.switch_detach);
> > +
> >         cb_option = 'c';
> >
> >         return checkout_main(argc, argv, prefix, &opts, options,
> > diff --git a/t/t2060-switch.sh b/t/t2060-switch.sh
> > index c91c4db9..3435ae98 100755
> > --- a/t/t2060-switch.sh
> > +++ b/t/t2060-switch.sh
> > @@ -177,4 +177,31 @@ test_expect_success 'switch back when temporarily detached and checked out elsew
> >         git -C wt2 switch --ignore-other-worktrees shared
> >  '
> >
> > +test_expect_success 'switch with configured detach behavior from configuration ' '
> 
> Trailing space here at the end, also the title is kinda confusing with the
> configured + configuration, what about:
> "switch detaches on commit when switchDetach is set"

For the trailing space I was mislead by the previous test, line 149; it has its trailing space, I thought it was mandatory somehow. Thank you for the rewording.

Show 24 quoted lines
> > +       test_when_finished "
> > +               test_config checkout.switchDetach false;
> > +               git switch main
> > +       " &&
> > +       test_config checkout.switchDetach true &&
> > +       git switch main &&
> > +       git symbolic-ref HEAD &&
> > +       git switch main~ &&
> > +       test_must_fail git symbolic-ref HEAD &&
> > +       git switch - &&
> > +       git symbolic-ref HEAD &&
> > +       git switch - &&
> > +       test_must_fail git symbolic-ref HEAD
> > +'
> > +
> > +test_expect_success 'switch without configured detach behavior from configuration ' '
> 
> Trailing space.
> 
> > +       test_when_finished "
> > +               test_config checkout.switchDetach false;
> 
> test_config already has test_when_finished
> I would git config --unset.
ACK.
Show 5 quoted lines
> > +               git switch main
> > +       " &&
> > +       test_config checkout.switchDetach false &&
> 
> This is already false.

I prefer to be explicit, to test the case where the user disables this feature on purpose.

Show 22 quoted lines
> > +       git switch main &&
> > +       git symbolic-ref HEAD &&
> > +       test_must_fail git switch main~
> > +'
> > +
> >  test_done
> >
> > base-commit: 2855562ca6a9c6b0e7bc780b050c1e83c9fcfbd0
> > --
> > Thibaud CANALE
> > thican [at] thican [dot] net
> > https://thican.net/
> > GPG: rsa4096 2013-10-14 485EF628CB85CDD4CB7CFF0D52F5127650733A18
> 
> Overall seems reasonable, it might be a design choice having to
> explicitly use --detach, force_detach it's pretty clear about it but
> I dunno.
> 
> For now:
> I would rewrite the commit.
> Make sure that you need a new flag, force_detach looks very similar.
> Fix the style issue.

It was already sure a new flag was necessary, using force detach flag is uncorrect as it will not anymore allow to use back a branch for example. We can see this new flag is not everywhere force_detach is. For testing, I tried only force_detach, the tests are failing.

V2 will follow shortly.
Best regards,
-- 
Thibaud CANALE
thican [at] thican [dot] net
https://thican.net/
GPG: rsa4096 2013-10-14 485EF628CB85CDD4CB7CFF0D52F5127650733A18
Thibaud CANALEApr 4, 2026, 21:14 UTC in reply to Thibaud CANALE on lore

[RFC PATCH v2] switch: provide configurable detach

Some users won’t change to switch/restore commands and keep relying on checkout command because of this “missing” feature:

For git-switch(1), configurable through option `checkout.switchDetach`, match git-checkout(1)’s detach behavior on non-branch references, such as commit id and "tilde" reference (i.e "${my_branch}~2"). No need to use explicit --detach option, automatically detach on non-branch reference, then reattach when going back on a branch, even implicitly through `git switch -` command. … like git-checkout(1) uses to do.

No impact on current behavior when option is unset or set to false.
Signed-off-by: Thibaud CANALE <thican@thican.net>
---
 Documentation/config/checkout.adoc | 14 ++++++++++++++
 builtin/checkout.c                 | 19 ++++++++++++++++---
 t/t2060-switch.sh                  | 27 +++++++++++++++++++++++++++
 3 files changed, 57 insertions(+), 3 deletions(-)
Show changes to 3 files +57 −3

Documentation/config/checkout.adoc, builtin/checkout.c, t/t2060-switch.sh

diff --git a/Documentation/config/checkout.adoc b/Documentation/config/checkout.adoc
index e35d2129..5309bed7 100644
--- a/Documentation/config/checkout.adoc
+++ b/Documentation/config/checkout.adoc
@@ -22,6 +22,20 @@ commands or functionality in the future.
 	option in `git checkout` and `git switch`. See
 	linkgit:git-switch[1] and linkgit:git-checkout[1].
 
+`checkout.switchDetach`::
+	For linkgit:git-switch[1], matches linkgit:git-checkout[1]’s detach behavior
+	on non-branch references, such as commit id and "tilde" reference (i.e
+	"${my_branch}~2").
++
+No need to use explicit --detach option, automatically detach on
+non-branch reference, then reattach when going back on a branch, even
+implicitly through `git switch -` command.
++
+… like linkgit:git-checkout[1] uses to do.
++
+No impact when option is unset or set to false, won't continue when trying to
+detach HEAD without explicit --detach option.
+
 `checkout.workers`::
 	The number of parallel workers to use when updating the working tree.
 	The default is one, i.e. sequential execution. If set to a value less
diff --git a/builtin/checkout.c b/builtin/checkout.c
index e031e618..cd7b0a60 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -53,6 +53,7 @@ struct checkout_opts {
 	int force;
 	int force_detach;
 	int implicit_detach;
+	int switch_detach;
 	int writeout_stage;
 	int overwrite_ignore;
 	int ignore_skipworktree;
@@ -1005,7 +1006,10 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 	else
 		strbuf_insertstr(&msg, 0, reflog_msg);
 
-	if (!strcmp(new_branch_info->name, "HEAD") && !new_branch_info->path && !opts->force_detach) {
+	if (!strcmp(new_branch_info->name, "HEAD") &&
+	    !new_branch_info->path &&
+	    !opts->force_detach &&
+	    !opts->switch_detach) {
 		/* Nothing to do. */
 	} else if (opts->force_detach || !new_branch_info->path) {	/* No longer on any branch. */
 		refs_update_ref(get_main_ref_store(the_repository), msg.buf,
@@ -1014,7 +1018,9 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 				REF_NO_DEREF, UPDATE_REFS_DIE_ON_ERR);
 		if (!opts->quiet) {
 			if (old_branch_info->path &&
-			    advice_enabled(ADVICE_DETACHED_HEAD) && !opts->force_detach)
+			    advice_enabled(ADVICE_DETACHED_HEAD) &&
+			    !opts->force_detach &&
+			    !opts->switch_detach)
 				detach_advice(new_branch_info->name);
 			describe_detached_head(_("HEAD is now at"), new_branch_info->commit);
 		}
@@ -1541,8 +1547,11 @@ static void die_expecting_a_branch(const struct branch_info *branch_info)
 		 */
 		code = die_message(_("a branch is expected, got '%s'"), branch_info->name);
 
-	if (advice_enabled(ADVICE_SUGGEST_DETACHING_HEAD))
+	if (advice_enabled(ADVICE_SUGGEST_DETACHING_HEAD)) {
 		advise(_("If you want to detach HEAD at the commit, try again with the --detach option."));
+		advise(_("You can also detach by default by setting the config "
+		         "variable checkout.switchDetach to true."));
+	}
 
 	exit(code);
 }
@@ -1660,6 +1669,7 @@ static int checkout_branch(struct checkout_opts *opts,
 
 	if (!opts->implicit_detach &&
 	    !opts->force_detach &&
+	    !opts->switch_detach &&
 	    !opts->new_branch &&
 	    !opts->new_branch_force &&
 	    new_branch_info->name &&
@@ -2119,6 +2129,9 @@ int cmd_switch(int argc,
 	options = add_common_options(&opts, options);
 	options = add_common_switch_branch_options(&opts, options);
 
+	repo_config_get_bool(the_repository, "checkout.switchDetach",
+	                     &opts.switch_detach);
+
 	cb_option = 'c';
 
 	return checkout_main(argc, argv, prefix, &opts, options,
diff --git a/t/t2060-switch.sh b/t/t2060-switch.sh
index c91c4db9..3732a826 100755
--- a/t/t2060-switch.sh
+++ b/t/t2060-switch.sh
@@ -177,4 +177,31 @@ test_expect_success 'switch back when temporarily detached and checked out elsew
 	git -C wt2 switch --ignore-other-worktrees shared
 '
 
+test_expect_success 'switch with configured detach behavior from configuration' '
+	test_when_finished "
+		git config --unset checkout.switchDetach;
+		git switch main
+	" &&
+	test_config checkout.switchDetach true &&
+	git switch main &&
+	git symbolic-ref HEAD &&
+	git switch main~ &&
+	test_must_fail git symbolic-ref HEAD &&
+	git switch - &&
+	git symbolic-ref HEAD &&
+	git switch - &&
+	test_must_fail git symbolic-ref HEAD
+'
+
+test_expect_success 'switch without configured detach behavior from configuration' '
+	test_when_finished "
+		git config --unset checkout.switchDetach;
+		git switch main
+	" &&
+	test_config checkout.switchDetach false &&
+	git switch main &&
+	git symbolic-ref HEAD &&
+	test_must_fail git switch main~
+'
+
 test_done

Range-diff against v1:
1:  d9a7ca4c ! 1:  ea7ea74c switch: provide configurable detach
    @@ Metadata
      ## Commit message ##
         switch: provide configurable detach
     
    -    Its purpose is to provide for git-switch(1) same detach behavior on
    -    commit it than git-checkout(1) through configuration option
    -    `checkout.switchDetach`.
    +    Some users won’t change to switch/restore commands and keep relying on
    +    checkout command because of this “missing” feature:
    +
    +    For git-switch(1), configurable through option `checkout.switchDetach`,
    +    match git-checkout(1)’s detach behavior on non-branch references, such
    +    as commit id and "tilde" reference (i.e "${my_branch}~2").
    +    No need to use explicit --detach option, automatically detach on
    +    non-branch reference, then reattach when going back on a branch, even
    +    implicitly through `git switch -` command.
    +    … like git-checkout(1) uses to do.
    +
    +    No impact on current behavior when option is unset or set to false.
     
         Signed-off-by: Thibaud CANALE <thican@thican.net>
     
    @@ Documentation/config/checkout.adoc: commands or functionality in the future.
      	linkgit:git-switch[1] and linkgit:git-checkout[1].
      
     +`checkout.switchDetach`::
    -+	Provides for linkgit:git-switch[1] the same detach behavior on commit id
    -+	than linkgit:git-checkout[1] without the explicit --detach option.
    ++	For linkgit:git-switch[1], matches linkgit:git-checkout[1]’s detach behavior
    ++	on non-branch references, such as commit id and "tilde" reference (i.e
    ++	"${my_branch}~2").
    +++
    ++No need to use explicit --detach option, automatically detach on
    ++non-branch reference, then reattach when going back on a branch, even
    ++implicitly through `git switch -` command.
    +++
    ++… like linkgit:git-checkout[1] uses to do.
    +++
    ++No impact when option is unset or set to false, won't continue when trying to
    ++detach HEAD without explicit --detach option.
     +
      `checkout.workers`::
      	The number of parallel workers to use when updating the working tree.
    @@ builtin/checkout.c: struct checkout_opts {
      	int force;
      	int force_detach;
      	int implicit_detach;
    -+	int switch_detach;  // For checkout.switchDetach configuration
    ++	int switch_detach;
      	int writeout_stage;
      	int overwrite_ignore;
      	int ignore_skipworktree;
    @@ builtin/checkout.c: static void die_expecting_a_branch(const struct branch_info
     -	if (advice_enabled(ADVICE_SUGGEST_DETACHING_HEAD))
     +	if (advice_enabled(ADVICE_SUGGEST_DETACHING_HEAD)) {
      		advise(_("If you want to detach HEAD at the commit, try again with the --detach option."));
    -+		advise(_("Also you can detach by default by setting the config variable "
    -+		         "checkout.switchDetach to true."));
    ++		advise(_("You can also detach by default by setting the config "
    ++		         "variable checkout.switchDetach to true."));
     +	}
      
      	exit(code);
    @@ builtin/checkout.c: int cmd_switch(int argc,
      	options = add_common_options(&opts, options);
      	options = add_common_switch_branch_options(&opts, options);
      
    -+	repo_config_get_bool(the_repository, "checkout.switchDetach", &opts.switch_detach);
    ++	repo_config_get_bool(the_repository, "checkout.switchDetach",
    ++	                     &opts.switch_detach);
     +
      	cb_option = 'c';
      
    @@ t/t2060-switch.sh: test_expect_success 'switch back when temporarily detached an
      	git -C wt2 switch --ignore-other-worktrees shared
      '
      
    -+test_expect_success 'switch with configured detach behavior from configuration ' '
    ++test_expect_success 'switch with configured detach behavior from configuration' '
     +	test_when_finished "
    -+		test_config checkout.switchDetach false;
    ++		git config --unset checkout.switchDetach;
     +		git switch main
     +	" &&
     +	test_config checkout.switchDetach true &&
    @@ t/t2060-switch.sh: test_expect_success 'switch back when temporarily detached an
     +	test_must_fail git symbolic-ref HEAD
     +'
     +
    -+test_expect_success 'switch without configured detach behavior from configuration ' '
    ++test_expect_success 'switch without configured detach behavior from configuration' '
     +	test_when_finished "
    -+		test_config checkout.switchDetach false;
    ++		git config --unset checkout.switchDetach;
     +		git switch main
     +	" &&
     +	test_config checkout.switchDetach false &&
-- 
Thibaud CANALE
thican [at] thican [dot] net
https://thican.net/
GPG: rsa4096 2013-10-14 485EF628CB85CDD4CB7CFF0D52F5127650733A18
Thibaud CANALEApr 4, 2026, 21:26 UTC in reply to D. Ben Knoble on lore

Re: [RFC PATCH] switch: provide configurable detach

On Saturday 2026-04-04 12:58:48-0400, D. Ben Knoble <ben.knoble@gmail.com> wrote:
Hello,
Show 15 quoted lines
> On Sat, Apr 4, 2026 at 10:36 AM Thibaud CANALE <thican@thican.net> wrote:
> >
> > Its purpose is to provide for git-switch(1) same detach behavior on
> > commit it than git-checkout(1) through configuration option
> > `checkout.switchDetach`.
> 
> I considered contributing something similar at one point, though I
> would call it "switch.detach" ("<command>.<option>").
> 
> I think I eventually decided against sending a patch because I was
> concerned that such an option might make using git-switch in scripts
> more of a headache. If I'm using it intentionally because it will fail
> in some circumstances without --detach, now I've got to also do "-c
> switch.detach=false" or something to control for the new
> configuration.

I see what you mean, I could argue it’s a very niche option, only concerning git-switch(1), and in case we rely on git to block further actions if the provided reference is not a branch or simply working on detached states, it should not have any impact. This is what the newly implemented test cases are designed to ensure.

As mentioned it only implements the same behavior than git-checkout(1)’s concerning non-branch references.

Full disclosure, I am not even concerned about this feature, as I use switch/restore daily since 2017, but not everyone I know does; so here I try to ease transition for them.

Show 7 quoted lines
> On the one hand, I think that argument applies mostly to plumbing
> rather than porcelain commands. On the other, a command for switching
> branches whose behavior is reliable enough for scripts seems
> worthwhile. So, idk :)
> 
> -- 
> D. Ben Knoble

Also the purpose of this request is to provide this feature for Windows environment, which is out of my abilities concerning compilation, cannot compile without such environment.

Best regards,
-- 
Thibaud CANALE
thican [at] thican [dot] net
https://thican.net/
GPG: rsa4096 2013-10-14 485EF628CB85CDD4CB7CFF0D52F5127650733A18
Junio C HamanoApr 6, 2026, 16:36 UTC in reply to Thibaud CANALE on lore

Re: [RFC PATCH] switch: provide configurable detach

Thibaud CANALE <thican@thican.net> writes:
Show 10 quoted lines
> Its purpose is to provide for git-switch(1) same detach behavior on
> commit it than git-checkout(1) through configuration option
> `checkout.switchDetach`.
>
> Signed-off-by: Thibaud CANALE <thican@thican.net>
> ---
>  Documentation/config/checkout.adoc |  4 ++++
>  builtin/checkout.c                 | 18 +++++++++++++++---
>  t/t2060-switch.sh                  | 27 +++++++++++++++++++++++++++
>  3 files changed, 46 insertions(+), 3 deletions(-)
Sorry, but I am fairly negative on this change.

"switch" was an attempt to give folks an improved experience over "checkout". The implied "--detach" was deliberately removed from the command and I do not think it was a bad move for our user base who wanted to have "switch" that can only be used to switch branches (as opposed to "checkout" that hecks out both files and branches). With fewer choices in a single command that does only a single thing, the hope was to make it simpler to teach.

And it is backwards to make "switch" configurable in that context. It defeats a major point of "git switch".

FWIW, "git checkout" that knows what is and what is not a branch name and does not require "--detach" when detaching to anything that is not a branch name is always available and will not be going away.

Kristoffer HaugsbakkApr 6, 2026, 17:48 UTC in reply to Junio C Hamano on lore

Re: [RFC PATCH] switch: provide configurable detach

On Mon, Apr 6, 2026, at 18:36, Junio C Hamano wrote:
Show 11 quoted lines
> Thibaud CANALE <thican@thican.net> writes:
>>[snip]
>>  3 files changed, 46 insertions(+), 3 deletions(-)
>
> Sorry, but I am fairly negative on this change.
>
>[snip]
>
> FWIW, "git checkout" that knows what is and what is not a branch
> name and does not require "--detach" when detaching to anything that
> is not a branch name is always available and will not be going away.

On the one hand, I personally mostly use git-checkout(1) because I never use it to check out files and I like the detach behavior. So from that perspective I don’t understand Git users who interject when you provide examples for something and you (incidental to the example and point) use git-checkout(1) because that’s what you are used to. And they interject because git-checkout(1) is “obsolete” or something and you are setting a bad example (or something).

On the other hand, the BreakingChanges document does call git-checkout(1) “superseded”. And in that light I do understand why people want to actively avoid git-checkout(1), including implementing replacements for all relevant checkout-use cases in git-switch(1).

    Superseded features that will not be deprecated
    [...]
    • The features git-checkout(1) offers are covered by the pair of
      commands git-restore(1) and git-switch(1). Because the use of
      git-checkout(1) is still widespread, and it is not expected that
      this will change anytime soon, all three commands will stay.
https://git-scm.com/docs/BreakingChanges
Junio C HamanoApr 7, 2026, 00:50 UTC in reply to Kristoffer Haugsbakk on lore

Re: [RFC PATCH] switch: provide configurable detach

"Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com> writes:
Show 13 quoted lines
> On the other hand, the BreakingChanges document does call
> git-checkout(1) “superseded”.  And in that light I do understand why
> people want to actively avoid git-checkout(1), including implementing
> replacements for all relevant checkout-use cases in git-switch(1).
>
>     Superseded features that will not be deprecated
>
>     [...]
>
>     • The features git-checkout(1) offers are covered by the pair of
>       commands git-restore(1) and git-switch(1). Because the use of
>       git-checkout(1) is still widespread, and it is not expected that
>       this will change anytime soon, all three commands will stay.

Perhaps a good first step would be to stop calling checkout "superseded". As they stand, all three are equally viable. In other words, I would not exactly call "switch/restore" a failed experiment, but it wasn't a clear success story, either.

I think "still" and "anytime soon" in the above paragraph are doing disservice by implying that somehow we want to remove checkout but we cannot yet, when the reality is that we wanted to be able to replace checkout with switch/restore in the distant past, but that was a misguided attempt and did not work very well.

It is not like switch/restore pair did not work well or anything, and in that sense switch/restore themselves are by no means failures, but it was a failed experiment to introduce these two as a way to replace and kill checkout. For those users to whom both "checking out files" and "checkout out a branch" appear as clear concepts, there is no reason to abandon checkout.

Back to recent threads