* [PATCH] switch: add --ensure option
@ 2026-06-09 9:23 Lei Zhu via GitGitGadget
2026-06-09 12:59 ` Junio C Hamano
0 siblings, 1 reply; 2+ messages in thread
From: Lei Zhu via GitGitGadget @ 2026-06-09 9:23 UTC (permalink / raw)
To: git; +Cc: Lei Zhu, Korov
From: Korov <korov9.c@gmail.com>
Add a new `git switch --ensure` (`-e`) option that behaves like an
idempotent form of branch switching.
Users who often switch between topic branches may not know whether the
local branch already exists. Without this option, they need to check
for the branch first and then choose between `git switch <branch>` and
`git switch -c <branch>`. The new option folds that workflow into a
single command.
When the target branch does not exist, `git switch -e <branch>`
behaves like `git switch -c <branch>`, including existing `--track`
and `--no-track` handling.
When the target branch already exists, `git switch -e <branch>`
switches to it without resetting the branch tip. If `--track` is
given, update the branch's upstream configuration using the explicit
start-point, or the current branch when no start-point is provided.
Fail in detached HEAD state when no start-point is available for
tracking setup.
Document the new option and add tests covering create-branch tracking,
existing-branch tracking updates, and detached-HEAD failure cases.
Signed-off-by: Korov <korov9.c@gmail.com>
---
switch: add --ensure option
Add a new git switch --ensure (-e) option that behaves like an
idempotent form of branch switching.
Users who often switch between topic branches may not know whether the
local branch already exists. Without this option, they need to check for
the branch first and then choose between git switch <branch> and git
switch -c <branch>. The new option folds that workflow into a single
command.
When the target branch does not exist, git switch -e <branch> behaves
like git switch -c <branch>, including existing --track and --no-track
handling.
When the target branch already exists, git switch -e <branch> switches
to it without resetting the branch tip. If --track is given, update the
branch's upstream configuration using the explicit start-point, or the
current branch when no start-point is provided. Fail in detached HEAD
state when no start-point is available for tracking setup.
Document the new option and add tests covering create-branch tracking,
existing-branch tracking updates, and detached-HEAD failure cases.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2324%2FKorov%2Fdev3-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2324/Korov/dev3-v1
Pull-Request: https://github.com/git/git/pull/2324
Documentation/git-switch.adoc | 16 +++++++
builtin/checkout.c | 86 ++++++++++++++++++++++++++++++++++-
t/t2060-switch.sh | 46 +++++++++++++++++++
3 files changed, 146 insertions(+), 2 deletions(-)
diff --git a/Documentation/git-switch.adoc b/Documentation/git-switch.adoc
index d6c4f229a5..a0ac31fa23 100644
--- a/Documentation/git-switch.adoc
+++ b/Documentation/git-switch.adoc
@@ -11,6 +11,7 @@ SYNOPSIS
git switch [<options>] [--no-guess] <branch>
git switch [<options>] --detach [<start-point>]
git switch [<options>] (-c|-C) <new-branch> [<start-point>]
+git switch [<options>] -e <branch> [<start-point>]
git switch [<options>] --orphan <new-branch>
DESCRIPTION
@@ -81,6 +82,21 @@ $ git branch -f _<new-branch>_
$ git switch _<new-branch>_
------------
+`-e <branch>`::
+`--ensure <branch>`::
+ Switch to _<branch>_ if it already exists, or create it from
+ _<start-point>_ before switching to it if it does not.
++
+When _<branch>_ does not already exist, this behaves like
+`git switch -c <branch> [<start-point>]`, including any `--track`
+or `--no-track` options.
++
+When _<branch>_ already exists, the branch tip is not changed. If
+`--track[=(direct|inherit)]` is given, the existing branch's upstream
+configuration is updated using _<start-point>_ when one is provided,
+or the current branch when _<start-point>_ is omitted. This form fails
+when `HEAD` is detached and no _<start-point>_ is given.
+
`-d`::
`--detach`::
Switch to a commit for inspection and discardable
diff --git a/builtin/checkout.c b/builtin/checkout.c
index b78b3a1d16..f56935bfe2 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -81,6 +81,8 @@ struct checkout_opts {
const char *new_branch;
const char *new_branch_force;
const char *new_orphan_branch;
+ const char *ensure_branch;
+ const char *ensure_branch_start;
int new_branch_log;
enum branch_track track;
struct diff_options diff_options;
@@ -988,6 +990,15 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
free(new_branch_info->refname);
new_branch_info->name = xstrdup(opts->new_branch);
setup_branch_path(new_branch_info);
+ } else if (opts->ensure_branch && opts->branch_exists &&
+ opts->track != BRANCH_TRACK_UNSPECIFIED) {
+ const char *tracking_source = opts->ensure_branch_start ?
+ opts->ensure_branch_start :
+ old_branch_info->name;
+ dwim_and_setup_tracking(the_repository, opts->ensure_branch,
+ tracking_source, opts->track,
+ opts->quiet);
+ remote_state_clear(the_repository->remote_state);
}
old_desc = old_branch_info->name;
@@ -1927,6 +1938,52 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
die(_("options '-%c', '-%c', and '%s' cannot be used together"),
cb_option, toupper(cb_option), "--orphan");
+ if (opts->ensure_branch) {
+ struct strbuf ref = STRBUF_INIT;
+ int exists;
+
+ if (opts->new_branch || opts->new_branch_force || opts->new_orphan_branch)
+ die(_("'%s' cannot be used with '%s'"), "-e", "-c/-C/--orphan");
+ if (opts->force_detach)
+ die(_("'%s' cannot be used with '%s'"), "-e", "--detach");
+
+ exists = validate_branchname(opts->ensure_branch, &ref);
+ strbuf_release(&ref);
+
+ /* Save an explicit start point for tracking setup. */
+ if (argc > 0 && opts->track != BRANCH_TRACK_UNSPECIFIED)
+ opts->ensure_branch_start = argv[0];
+
+ if (exists) {
+ /*
+ * Branch exists: just switch to it, don't reset.
+ * We'll set up tracking after the switch if --track was given.
+ */
+ opts->branch_exists = 1;
+ } else {
+ /* Branch doesn't exist: create it like -c */
+ opts->new_branch = opts->ensure_branch;
+ }
+ }
+
+ if (opts->ensure_branch && opts->branch_exists &&
+ opts->track != BRANCH_TRACK_UNSPECIFIED &&
+ !opts->ensure_branch_start) {
+ struct object_id head_oid;
+ char *head = refs_resolve_refdup(get_main_ref_store(the_repository),
+ "HEAD", 0, &head_oid, NULL);
+ const char *branch;
+
+ if (!head)
+ die(_("failed to resolve HEAD as a valid ref"));
+ if (!strcmp(head, "HEAD"))
+ die(_("cannot set up tracking information; starting point '%s' is not a branch"),
+ "HEAD");
+ if (!skip_prefix(head, "refs/heads/", &branch))
+ die(_("HEAD not found below refs/heads!"));
+ free(head);
+ }
+
if (opts->overlay_mode == 1 && opts->patch_mode)
die(_("options '%s' and '%s' cannot be used together"), "-p", "--overlay");
@@ -1961,8 +2018,9 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
if (opts->new_orphan_branch)
opts->new_branch = opts->new_orphan_branch;
- /* --track without -c/-C/-b/-B/--orphan should DWIM */
- if (opts->track != BRANCH_TRACK_UNSPECIFIED && !opts->new_branch) {
+ /* --track without -c/-C/-b/-B/--orphan/-e should DWIM */
+ if (opts->track != BRANCH_TRACK_UNSPECIFIED && !opts->new_branch &&
+ !(opts->ensure_branch && opts->branch_exists)) {
const char *argv0 = argv[0];
if (!argc || !strcmp(argv0, "--"))
die(_("--track needs a branch name"));
@@ -2012,6 +2070,28 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
die(_("reference is not a tree: %s"), opts->from_treeish);
}
+ /*
+ * Handle -e with existing branch: set up new_branch_info to switch
+ * to the existing branch.
+ */
+ if (opts->ensure_branch && opts->branch_exists) {
+ struct object_id rev;
+
+ branch_info_release(&new_branch_info);
+ memset(&new_branch_info, 0, sizeof(new_branch_info));
+ new_branch_info.name = xstrdup(opts->ensure_branch);
+ setup_branch_path(&new_branch_info);
+
+ if (new_branch_info.path &&
+ !refs_read_ref(get_main_ref_store(the_repository),
+ new_branch_info.path, &rev)) {
+ new_branch_info.commit = lookup_commit_reference_gently(
+ the_repository, &rev, 1);
+ if (new_branch_info.commit)
+ parse_commit_or_die(new_branch_info.commit);
+ }
+ }
+
if (argc) {
parse_pathspec(&opts->pathspec, 0,
opts->patch_mode ? PATHSPEC_PREFIX_ORIGIN : 0,
@@ -2150,6 +2230,8 @@ int cmd_switch(int argc,
N_("create and switch to a new branch")),
OPT_STRING('C', "force-create", &opts.new_branch_force, N_("branch"),
N_("create/reset and switch to a branch")),
+ OPT_STRING('e', "ensure", &opts.ensure_branch, N_("branch"),
+ N_("create if needed and switch to branch")),
OPT_BOOL(0, "guess", &opts.dwim_new_local_branch,
N_("second guess 'git switch <no-such-branch>'")),
OPT_BOOL(0, "discard-changes", &opts.discard_changes,
diff --git a/t/t2060-switch.sh b/t/t2060-switch.sh
index c91c4db936..c0bff7caab 100755
--- a/t/t2060-switch.sh
+++ b/t/t2060-switch.sh
@@ -146,6 +146,52 @@ test_expect_success 'tracking info copied with autoSetupMerge=inherit' '
test_cmp_config "" --default "" branch.main2.merge
'
+test_expect_success 'switch -e --track creates branch from current branch' '
+ test_when_finished "
+ git switch main || :
+ git branch -D ensure-new-current || :
+ " &&
+ git switch main &&
+ git switch -e ensure-new-current --track &&
+ test_cmp_rev refs/heads/main refs/heads/ensure-new-current &&
+ test_cmp_config . branch.ensure-new-current.remote &&
+ test_cmp_config refs/heads/main branch.ensure-new-current.merge
+'
+
+test_expect_success 'switch -e --track creates branch from remote-tracking branch' '
+ test_when_finished "
+ git switch main || :
+ git branch -D ensure-new || :
+ " &&
+ git switch -e ensure-new --track origin/foo &&
+ test_cmp_rev refs/remotes/origin/foo refs/heads/ensure-new &&
+ test_cmp_config origin branch.ensure-new.remote &&
+ test_cmp_config refs/heads/foo branch.ensure-new.merge
+'
+
+test_expect_success 'switch -e --track uses current branch for existing branch' '
+ test_when_finished "
+ git switch main || :
+ git branch -D ensure-existing source-for-track || :
+ " &&
+ git switch -c source-for-track main &&
+ git branch ensure-existing main &&
+ git switch -e ensure-existing --track &&
+ test_cmp_config . branch.ensure-existing.remote &&
+ test_cmp_config refs/heads/source-for-track branch.ensure-existing.merge
+'
+
+test_expect_success 'switch -e --track fails from detached HEAD without start-point' '
+ test_when_finished "
+ git switch main || :
+ git branch -D detached-target || :
+ " &&
+ git branch detached-target main &&
+ git switch --detach main &&
+ test_must_fail git switch -e detached-target --track 2>stderr &&
+ test_grep "cannot set up tracking information; starting point '\''HEAD'\'' is not a branch" stderr
+'
+
test_expect_success 'switch back when temporarily detached and checked out elsewhere ' '
test_when_finished "
git worktree remove wt1 ||:
base-commit: 600fe743028cbfb640855f659e9851522214bc0b
--
gitgitgadget
^ permalink raw reply related [flat|nested] 2+ messages in thread* Re: [PATCH] switch: add --ensure option
2026-06-09 9:23 [PATCH] switch: add --ensure option Lei Zhu via GitGitGadget
@ 2026-06-09 12:59 ` Junio C Hamano
0 siblings, 0 replies; 2+ messages in thread
From: Junio C Hamano @ 2026-06-09 12:59 UTC (permalink / raw)
To: Lei Zhu via GitGitGadget; +Cc: git, Lei Zhu
"Lei Zhu via GitGitGadget" <gitgitgadget@gmail.com> writes:
> Users who often switch between topic branches may not know whether the
> local branch already exists. Without this option, they need to check
> for the branch first and then choose between `git switch <branch>` and
> `git switch -c <branch>`. The new option folds that workflow into a
> single command.
Quite honestly, I am not sure if the use case should be supported
with a new option, or we should actively discourage it by rejecting
any patch that takes us in this direction, as the actions the user
would take after seeing the result of "git checkout -b" or "git
switch -c" are quite different among (just off the top of my head):
(1) Ah we already had the branch created exactly to work on this;
instead of forking a new effort, switch to the existing branch
and build on the effort we made previously, as it forks from an
acceptable base, which might have been different from where we
wanted to start at when we said "git switch -c <branch> <base>".
(2) Ah we already had the branch created exactly to work on this.
Unfortunately, it was forked from way too new base before we
realized that this is also an important bugfix that needs to be
mergeable to the maintenance track. Let's create a new branch
that is a copy of the existing one with "-maint" in its name,
rebase it on the maintenance track, and work there.
(3) Ah we already had a branch that happened to have the same name,
but created for totally different reasons. We do want to fork
a new branch but need to give it a different name.
(4) There wasn't a branch with the given name, so we created a new
branch at the right starting point we just picked when we ran
"git checkout -b"/"git switch -c". Let's start working on the
topic.
You cover *only* case (4) perfectly. When your "-c <branch>" picks
an existing branch, the user still needs to figure out which among
situations (1)-(3) (of course, there may be others) they are in, and
act accordingly. "git checkout -b" and "git switch -c" that fails,
reminding that there is an existing branch with the same name, gives
users a stronger form of reminder than switching blindly to the
existing branch, which may (in case (1)) or may not (in cases (2)
and (3)) be where the user wants to be when taking the next action.
Having said that.
* The option name "-e" would make all users expect that this has
something to do with "--editor". Start with a longer name,
perhaps "--create-if-missing" or something, and then see if
others can come up with a better short-hand. Obviously whoever
chooses "-e" is not equipped well to do so (yet), and the
reviewer who pointed out "-e" is not a good idea without being
able to offer a better alternative is not, either ;-).
* Adding a new flag only to "switch" without "checkout" will
unnecessary confuse users. This is because, even though
"switch/restore" started as an experiment to _supersede_
"checkout", they were not successful, not in the sense that
"switch/restore" were harder to use than the original, but in the
sense that the userbase and teaching materials are already used
to the original and removing it is practically infeasible.
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-06-09 12:59 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-09 9:23 [PATCH] switch: add --ensure option Lei Zhu via GitGitGadget
2026-06-09 12:59 ` Junio C Hamano
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox