diff --git a/Documentation/config/worktree.adoc b/Documentation/config/worktree.adoc index a248076ea50bd5..0930183b91cb8f 100644 --- a/Documentation/config/worktree.adoc +++ b/Documentation/config/worktree.adoc @@ -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 diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc index fbf8426cd974e4..32787eacc3c17d 100644 --- a/Documentation/git-worktree.adoc +++ b/Documentation/git-worktree.adoc @@ -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 __, 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. diff --git a/builtin/checkout.c b/builtin/checkout.c index 55e3a89a852712..2bc21aa49be05c 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -1343,13 +1343,51 @@ enum checkout_command { 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 ,\n" + "you can do so by fully qualifying the name with the --track option:\n" + "\n" + " git %s --track /%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, struct object_id *rev, int could_be_checkout_paths, enum checkout_command which_command) { 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) { 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 (advice_enabled(ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME)) { - const char *cmdname; - - switch (which_command) { - case CHECKOUT_CHECKOUT: - 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/\n" - "\n" - "If you'd like to always have checkouts of an ambiguous 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); + 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; } diff --git a/builtin/worktree.c b/builtin/worktree.c index 654d27c3e1ce99..07163bf9b72211 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -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" + ", you can do so by:\n" + "\n" + " git worktree add -b %s %s /%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; @@ -898,17 +931,29 @@ static int add(int ac, const char **av, const char *prefix, /* DWIM: Infer --orphan when repo has no refs. */ opts.orphan = (!s) && dwim_orphan(&opts, !!opt_track, 1); } 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")) diff --git a/checkout.c b/checkout.c index 1588b116eedf06..a0d0229435d2e0 100644 --- a/checkout.c +++ b/checkout.c @@ -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; diff --git a/checkout.h b/checkout.h index 55920e7aeb243d..0b185a0fc934eb 100644 --- a/checkout.h +++ b/checkout.h @@ -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 */ diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh index 87b926728ad8cf..a37137042d549b 100755 --- a/t/t2400-worktree-add.sh +++ b/t/t2400-worktree-add.sh @@ -624,12 +624,12 @@ test_expect_success '"add" dwims' ' test_expect_success '"add" 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 @@ -669,6 +669,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 &&