Merge branch 'yn/worktree-ambiguous-remote-advice' into jch

'git worktree add' did not prevent DWIM behavior when '-b' or '-B' was
specified, which has been corrected.

* yn/worktree-ambiguous-remote-advice:
  worktree add: treat multiple matches with --guess-remote as an error
  worktree add: improve message for ambiguous remote branch name
  checkout: improve message for ambiguous remote branch name
  checkout: extract function to display advice for ambiguous remotes
jch
Junio C Hamano 2026-08-31 11:11:58 -07:00
commit 411769ef6f
7 changed files with 133 additions and 44 deletions

View File

@ -5,8 +5,9 @@
set to true, `worktree add` tries to find a remote-tracking
branch whose name uniquely matches the new branch name. If
such a branch exists, it is checked out and set as "upstream"
for the new branch. If no such match can be found, it falls
back to creating a new branch from the current `HEAD`.
for the new branch. If multiple matches are found, the command
fails. If no such match can be found, it falls back to
creating a new branch from the current `HEAD`.

`worktree.useRelativePaths`::
Link worktrees using relative paths (when "`true`") or absolute

View File

@ -219,7 +219,9 @@ To remove a locked worktree, specify `--force` twice.
of creating a new branch from `HEAD`, if there exists a tracking
branch in exactly one remote matching the basename of _<path>_,
base the new branch on the remote-tracking branch, and mark
the remote-tracking branch as "upstream" from the new branch.
the remote-tracking branch as "upstream" from the new branch. If
there are multiple matches, the command fails. If there is no
match, the command falls back to creating a new branch from `HEAD`.
+
This can also be set up as the default behaviour by using the
`worktree.guessRemote` config option.

View File

@ -1343,23 +1343,12 @@ enum checkout_command {
CHECKOUT_RESTORE = 3,
};

static char *parse_remote_branch(const char *arg,
struct object_id *rev,
int could_be_checkout_paths,
enum checkout_command which_command)
static void advise_disambiguating_remotes(enum checkout_command which_command,
const char *branch,
const struct string_list *matched_remote_names)
{
int num_matches = 0;
char *remote = unique_tracking_name(arg, rev, &num_matches);

if (remote && could_be_checkout_paths) {
die(_("'%s' could be both a local file and a tracking branch.\n"
"Please use -- (and optionally --no-guess) to disambiguate"),
arg);
}

if (!remote && num_matches > 1) {
if (advice_enabled(ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME)) {
const char *cmdname;
struct string_list_item *item;

switch (which_command) {
case CHECKOUT_CHECKOUT:
@ -1369,26 +1358,53 @@ static char *parse_remote_branch(const char *arg,
cmdname = "switch";
break;
default:
BUG("command <%d> should not reach parse_remote_branch",
BUG("command <%d> should not reach advise_disambiguating_remotes",
which_command);
break;
}

advise(_("If you meant to check out a remote tracking branch on, e.g. 'origin',\n"
advise(_("Branch name '%s' appears in multiple remotes:"), branch);
for_each_string_list_item(item, matched_remote_names) {
advise(_(" %s"), item->string);
}
advise(_("If you meant to check out a remote tracking branch on <remote>,\n"
"you can do so by fully qualifying the name with the --track option:\n"
"\n"
" git %s --track origin/<name>\n"
" git %s --track <remote>/%s\n"
"\n"
"If you'd like to always have checkouts of an ambiguous <name> prefer\n"
"If you'd like to always have checkouts of an ambiguous name prefer\n"
"one remote, e.g. the 'origin' remote, consider setting\n"
"checkout.defaultRemote=origin in your config."),
cmdname);
cmdname, branch);
}

static char *parse_remote_branch(const char *arg,
struct object_id *rev,
int could_be_checkout_paths,
enum checkout_command which_command)
{
int num_matches = 0;
struct string_list matched_remote_names = STRING_LIST_INIT_DUP;

char *remote = unique_tracking_name(arg, rev, &num_matches,
&matched_remote_names);

if (remote && could_be_checkout_paths) {
die(_("'%s' could be both a local file and a tracking branch.\n"
"Please use -- (and optionally --no-guess) to disambiguate"),
arg);
}

if (!remote && num_matches > 1) {
if (advice_enabled(ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME))
advise_disambiguating_remotes(which_command, arg,
&matched_remote_names);
die(_("'%s' matched multiple (%d) remote tracking branches"),
arg, num_matches);
}

string_list_clear(&matched_remote_names, 0);

return remote;
}


View File

@ -764,7 +764,26 @@ static int dwim_orphan(const struct add_opts *opts, int opt_track, int remote)
return 1;
}

static char *dwim_branch(const char *path, char **new_branch)
static void advise_disambiguating_remotes(const char *path, const char *branch,
const struct string_list *matched_remote_names)
{
struct string_list_item *item;

advise(_("Branch name '%s' appears in multiple remotes:"), branch);
for_each_string_list_item(item, matched_remote_names) {
advise(_(" %s"), item->string);
}
advise(_("If you meant to create a worktree from a remote tracking branch on\n"
"<remote>, you can do so by:\n"
"\n"
" git worktree add -b %s %s <remote>/%s\n"
"\n"
"If you'd like to always prefer some remote, e.g. 'origin',\n"
"consider setting checkout.defaultRemote=origin in your config."),
branch, path, branch);
}

static char *dwim_branch(const struct add_opts *opts, const char *path, char **new_branch)
{
int n;
int branch_exists;
@ -782,7 +801,21 @@ static char *dwim_branch(const char *path, char **new_branch)
*new_branch = branchname;
if (guess_remote) {
struct object_id oid;
char *remote = unique_tracking_name(*new_branch, &oid, NULL);
char *remote;
int num_matches = 0;
struct string_list matched_remote_names = STRING_LIST_INIT_DUP;

remote = unique_tracking_name(*new_branch, &oid, &num_matches,
&matched_remote_names);
if (!remote && num_matches > 1) {
if (!opts->quiet &&
advice_enabled(ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME))
advise_disambiguating_remotes(path, *new_branch,
&matched_remote_names);
die(_("'%s' matched multiple (%d) remote tracking branches"),
*new_branch, num_matches);
}
string_list_clear(&matched_remote_names, 0);
return remote;
}
return NULL;
@ -890,7 +923,7 @@ static int add(int ac, const char **av, const char *prefix,
opts.orphan = dwim_orphan(&opts, !!opt_track, 0);
} else if (ac < 2) {
/* DWIM: Guess branch name from path. */
char *s = dwim_branch(path, &new_branch_to_free);
char *s = dwim_branch(&opts, path, &new_branch_to_free);
if (s)
branch = branch_to_free = s;
new_branch = new_branch_to_free;
@ -901,17 +934,29 @@ static int add(int ac, const char **av, const char *prefix,
if (!strcmp(branch, "HEAD"))
can_use_local_refs(&opts);
} else if (ac == 2) {
struct object_id oid;
struct commit *commit;
char *remote;

commit = lookup_commit_reference_by_name(branch);
if (!commit) {
remote = unique_tracking_name(branch, &oid, NULL);
struct object_id oid;
char *remote;
int num_matches = 0;
struct string_list matched_remote_names = STRING_LIST_INIT_DUP;

remote = unique_tracking_name(branch, &oid, &num_matches,
&matched_remote_names);
if (remote) {
new_branch = branch;
branch = new_branch_to_free = remote;
} else if (num_matches > 1) {
if (!opts.quiet &&
advice_enabled(ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME))
advise_disambiguating_remotes(path, branch,
&matched_remote_names);
die(_("'%s' matched multiple (%d) remote tracking branches"),
branch, num_matches);
}
string_list_clear(&matched_remote_names, 0);
}

if (!strcmp(branch, "HEAD"))

View File

@ -8,6 +8,7 @@
#include "checkout.h"
#include "config.h"
#include "strbuf.h"
#include "string-list.h"

struct tracking_name_data {
/* const */ char *src_ref;
@ -17,6 +18,7 @@ struct tracking_name_data {
const char *default_remote;
char *default_dst_ref;
struct object_id *default_dst_oid;
struct string_list *remote_names;
};

#define TRACKING_NAME_DATA_INIT { 0 }
@ -39,6 +41,8 @@ static int check_tracking_name(struct remote *remote, void *cb_data)
oidcpy(dst, cb->dst_oid);
cb->default_dst_oid = dst;
}
if (cb->remote_names)
string_list_append(cb->remote_names, remote->name);
if (cb->dst_ref) {
free(query.dst);
return 0;
@ -48,14 +52,19 @@ static int check_tracking_name(struct remote *remote, void *cb_data)
}

char *unique_tracking_name(const char *name, struct object_id *oid,
int *dwim_remotes_matched)
int *dwim_remotes_matched,
struct string_list *dwim_remote_names)
{
struct tracking_name_data cb_data = TRACKING_NAME_DATA_INIT;
const char *default_remote = NULL;
if (!repo_config_get_string_tmp(the_repository, "checkout.defaultremote", &default_remote))

if (!repo_config_get_string_tmp(the_repository,
"checkout.defaultremote",
&default_remote))
cb_data.default_remote = default_remote;
cb_data.src_ref = xstrfmt("refs/heads/%s", name);
cb_data.dst_oid = oid;
cb_data.remote_names = dwim_remote_names;
for_each_remote(check_tracking_name, &cb_data);
if (dwim_remotes_matched)
*dwim_remotes_matched = cb_data.num_matches;

View File

@ -3,6 +3,8 @@

#include "hash.h"

struct string_list;

/*
* Check if the branch name uniquely matches a branch name on a remote
* tracking branch. Return the name of the remote if such a branch
@ -10,6 +12,7 @@
*/
char *unique_tracking_name(const char *name,
struct object_id *oid,
int *dwim_remotes_matched);
int *dwim_remotes_matched,
struct string_list *dwim_remote_names);

#endif /* CHECKOUT_H */

View File

@ -634,12 +634,12 @@ test_expect_success '"add" <path> <branch> does not dwim with -b' '
test_expect_success '"add" <path> <branch> dwims with checkout.defaultRemote' '
test_when_finished rm -rf repo_upstream repo_dwim foo &&
setup_remote_repo repo_upstream repo_dwim &&
git init repo_dwim &&
(
cd repo_dwim &&
git remote add repo_upstream2 ../repo_upstream &&
git fetch repo_upstream2 &&
test_must_fail git worktree add ../foo foo &&
test_must_fail git worktree add ../foo foo 2>error.actual &&
test_grep "matched multiple (2) remote tracking branches" error.actual &&
git -c checkout.defaultRemote=repo_upstream worktree add ../foo foo &&
git status -uno --porcelain >status.actual &&
test_must_be_empty status.actual
@ -679,6 +679,19 @@ test_expect_success 'git worktree add --guess-remote sets up tracking' '
test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo
)
'

test_expect_success 'git worktree add --guess-remote fails if there are multiple matches' '
test_when_finished rm -rf repo_a repo_b foo &&
setup_remote_repo repo_a repo_b &&
(
cd repo_b &&
git remote add repo_a2 ../repo_a &&
git fetch repo_a2 &&
test_must_fail git worktree add --guess-remote ../foo 2>actual &&
test_grep "matched multiple (2) remote tracking branches" actual
)
'

test_expect_success 'git worktree add --guess-remote sets up tracking (quiet)' '
test_when_finished rm -rf repo_a repo_b foo &&
setup_remote_repo repo_a repo_b &&