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


`worktree.useRelativePaths`:: `worktree.useRelativePaths`::
Link worktrees using relative paths (when "`true`") or absolute 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 of creating a new branch from `HEAD`, if there exists a tracking
branch in exactly one remote matching the basename of _<path>_, branch in exactly one remote matching the basename of _<path>_,
base the new branch on the remote-tracking branch, and mark 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 This can also be set up as the default behaviour by using the
`worktree.guessRemote` config option. `worktree.guessRemote` config option.

View File

@ -1343,13 +1343,51 @@ enum checkout_command {
CHECKOUT_RESTORE = 3, CHECKOUT_RESTORE = 3,
}; };


static void advise_disambiguating_remotes(enum checkout_command which_command,
const char *branch,
const struct string_list *matched_remote_names)
{
const char *cmdname;
struct string_list_item *item;

switch (which_command) {
case CHECKOUT_CHECKOUT:
cmdname = "checkout";
break;
case CHECKOUT_SWITCH:
cmdname = "switch";
break;
default:
BUG("command <%d> should not reach advise_disambiguating_remotes",
which_command);
break;
}

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 <remote>/%s\n"
"\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, branch);
}

static char *parse_remote_branch(const char *arg, static char *parse_remote_branch(const char *arg,
struct object_id *rev, struct object_id *rev,
int could_be_checkout_paths, int could_be_checkout_paths,
enum checkout_command which_command) enum checkout_command which_command)
{ {
int num_matches = 0; int num_matches = 0;
char *remote = unique_tracking_name(arg, rev, &num_matches); 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) { if (remote && could_be_checkout_paths) {
die(_("'%s' could be both a local file and a tracking branch.\n" die(_("'%s' could be both a local file and a tracking branch.\n"
@ -1358,37 +1396,15 @@ static char *parse_remote_branch(const char *arg,
} }


if (!remote && num_matches > 1) { if (!remote && num_matches > 1) {
if (advice_enabled(ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME)) { if (advice_enabled(ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME))
const char *cmdname; advise_disambiguating_remotes(which_command, arg,

&matched_remote_names);
switch (which_command) { die(_("'%s' matched multiple (%d) remote tracking branches"),
case CHECKOUT_CHECKOUT: arg, num_matches);
cmdname = "checkout";
break;
case CHECKOUT_SWITCH:
cmdname = "switch";
break;
default:
BUG("command <%d> should not reach parse_remote_branch",
which_command);
break;
}

advise(_("If you meant to check out a remote tracking branch on, e.g. 'origin',\n"
"you can do so by fully qualifying the name with the --track option:\n"
"\n"
" git %s --track origin/<name>\n"
"\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);
}

die(_("'%s' matched multiple (%d) remote tracking branches"),
arg, num_matches);
} }


string_list_clear(&matched_remote_names, 0);

return remote; return remote;
} }



View File

@ -764,7 +764,26 @@ static int dwim_orphan(const struct add_opts *opts, int opt_track, int remote)
return 1; 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 n;
int branch_exists; int branch_exists;
@ -782,7 +801,21 @@ static char *dwim_branch(const char *path, char **new_branch)
*new_branch = branchname; *new_branch = branchname;
if (guess_remote) { if (guess_remote) {
struct object_id oid; 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 remote;
} }
return NULL; 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); opts.orphan = dwim_orphan(&opts, !!opt_track, 0);
} else if (ac < 2) { } else if (ac < 2) {
/* DWIM: Guess branch name from path. */ /* 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) if (s)
branch = branch_to_free = s; branch = branch_to_free = s;
new_branch = new_branch_to_free; 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")) if (!strcmp(branch, "HEAD"))
can_use_local_refs(&opts); can_use_local_refs(&opts);
} else if (ac == 2) { } else if (ac == 2) {
struct object_id oid;
struct commit *commit; struct commit *commit;
char *remote;


commit = lookup_commit_reference_by_name(branch); commit = lookup_commit_reference_by_name(branch);
if (!commit) { 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) { if (remote) {
new_branch = branch; new_branch = branch;
branch = new_branch_to_free = remote; 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")) if (!strcmp(branch, "HEAD"))

View File

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


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


#define TRACKING_NAME_DATA_INIT { 0 } #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); oidcpy(dst, cb->dst_oid);
cb->default_dst_oid = dst; cb->default_dst_oid = dst;
} }
if (cb->remote_names)
string_list_append(cb->remote_names, remote->name);
if (cb->dst_ref) { if (cb->dst_ref) {
free(query.dst); free(query.dst);
return 0; 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, 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; struct tracking_name_data cb_data = TRACKING_NAME_DATA_INIT;
const char *default_remote = NULL; 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.default_remote = default_remote;
cb_data.src_ref = xstrfmt("refs/heads/%s", name); cb_data.src_ref = xstrfmt("refs/heads/%s", name);
cb_data.dst_oid = oid; cb_data.dst_oid = oid;
cb_data.remote_names = dwim_remote_names;
for_each_remote(check_tracking_name, &cb_data); for_each_remote(check_tracking_name, &cb_data);
if (dwim_remotes_matched) if (dwim_remotes_matched)
*dwim_remotes_matched = cb_data.num_matches; *dwim_remotes_matched = cb_data.num_matches;

View File

@ -3,6 +3,8 @@


#include "hash.h" #include "hash.h"


struct string_list;

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


#endif /* CHECKOUT_H */ #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_expect_success '"add" <path> <branch> dwims with checkout.defaultRemote' '
test_when_finished rm -rf repo_upstream repo_dwim foo && test_when_finished rm -rf repo_upstream repo_dwim foo &&
setup_remote_repo repo_upstream repo_dwim && setup_remote_repo repo_upstream repo_dwim &&
git init repo_dwim &&
( (
cd repo_dwim && cd repo_dwim &&
git remote add repo_upstream2 ../repo_upstream && git remote add repo_upstream2 ../repo_upstream &&
git fetch repo_upstream2 && 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 -c checkout.defaultRemote=repo_upstream worktree add ../foo foo &&
git status -uno --porcelain >status.actual && git status -uno --porcelain >status.actual &&
test_must_be_empty 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_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_expect_success 'git worktree add --guess-remote sets up tracking (quiet)' '
test_when_finished rm -rf repo_a repo_b foo && test_when_finished rm -rf repo_a repo_b foo &&
setup_remote_repo repo_a repo_b && setup_remote_repo repo_a repo_b &&