The DWIM mode of checkout allows you to run "git checkout foo" when there is no existing local ref or path called "foo", and there is exactly one remote with a remote-tracking branch called "foo". Git will then automatically create a new local branch called "foo" using the remote-tracking "foo" as its starting point and configured upstream. However, the current code hardcodes the assumption that all remote-tracking branches are located at refs/remotes/$remote/*, and that "git checkout foo" must find exactly one ref matching "refs/remotes/*/foo" to succeed. This approach fails if a user has customized the refspec of a given remote to place remote-tracking branches elsewhere. The better way to find a tracking branch is to use the fetch refspecs for the configured remotes to deduce the available candidate remote-tracking branches corresponding to a remote branch of the requested name, and if exactly one of these exists (and points to a valid SHA1), then that is the remote-tracking branch we will use. For example, in the case of "git checkout foo", we map "refs/heads/foo" through each remote's refspec to find the available candidate remote-tracking branches, and if exactly one of these candidates exist in our local repo, then we have found the upstream for the new local branch "foo". This fixes most of the failing tests introduced in the previous patch. Signed-off-by: Johan Herland <johan@xxxxxxxxxxx> --- Documentation/git-checkout.txt | 6 +++--- builtin/checkout.c | 42 ++++++++++++++++++++++-------------------- t/t2024-checkout-dwim.sh | 6 +++--- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt index 8edcdca..bf0c99c 100644 --- a/Documentation/git-checkout.txt +++ b/Documentation/git-checkout.txt @@ -131,9 +131,9 @@ entries; instead, unmerged entries are ignored. "--track" in linkgit:git-branch[1] for details. + If no '-b' option is given, the name of the new branch will be -derived from the remote-tracking branch. If "remotes/" or "refs/remotes/" -is prefixed it is stripped away, and then the part up to the -next slash (which would be the nickname of the remote) is removed. +derived from the remote-tracking branch, by looking at the local part of +the refspec configured for the corresponding remote, and then stripping +the initial part up to the "*". This would tell us to use "hack" as the local branch when branching off of "origin/hack" (or "remotes/origin/hack", or even "refs/remotes/origin/hack"). If the given name has no slash, or the above diff --git a/builtin/checkout.c b/builtin/checkout.c index f8033f4..d6f9c01 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -822,38 +822,40 @@ static int git_checkout_config(const char *var, const char *value, void *cb) } struct tracking_name_data { - const char *name; - char *remote; + /* const */ char *src_ref; + char *dst_ref; + unsigned char *dst_sha1; int unique; }; -static int check_tracking_name(const char *refname, const unsigned char *sha1, - int flags, void *cb_data) +static int check_tracking_name(struct remote *remote, void *cb_data) { struct tracking_name_data *cb = cb_data; - const char *slash; - - if (prefixcmp(refname, "refs/remotes/")) - return 0; - slash = strchr(refname + 13, '/'); - if (!slash || strcmp(slash + 1, cb->name)) + struct refspec query; + memset(&query, 0, sizeof(struct refspec)); + query.src = cb->src_ref; + if (remote_find_tracking(remote, &query) || + get_sha1(query.dst, cb->dst_sha1)) return 0; - if (cb->remote) { + if (cb->dst_ref) { cb->unique = 0; return 0; } - cb->remote = xstrdup(refname); + cb->dst_ref = xstrdup(query.dst); return 0; } -static const char *unique_tracking_name(const char *name) +static const char *unique_tracking_name(const char *name, unsigned char *sha1) { - struct tracking_name_data cb_data = { NULL, NULL, 1 }; - cb_data.name = name; - for_each_ref(check_tracking_name, &cb_data); + struct tracking_name_data cb_data = { NULL, NULL, NULL, 1 }; + char src_ref[PATH_MAX]; + snprintf(src_ref, PATH_MAX, "refs/heads/%s", name); + cb_data.src_ref = src_ref; + cb_data.dst_sha1 = sha1; + for_each_remote(check_tracking_name, &cb_data); if (cb_data.unique) - return cb_data.remote; - free(cb_data.remote); + return cb_data.dst_ref; + free(cb_data.dst_ref); return NULL; } @@ -916,8 +918,8 @@ static int parse_branchname_arg(int argc, const char **argv, if (dwim_new_local_branch_ok && !check_filename(NULL, arg) && argc == 1) { - const char *remote = unique_tracking_name(arg); - if (!remote || get_sha1(remote, rev)) + const char *remote = unique_tracking_name(arg, rev); + if (!remote) return argcount; *new_branch = arg; arg = remote; diff --git a/t/t2024-checkout-dwim.sh b/t/t2024-checkout-dwim.sh index 36bf52f..fc6edc9 100755 --- a/t/t2024-checkout-dwim.sh +++ b/t/t2024-checkout-dwim.sh @@ -95,15 +95,15 @@ test_expect_success 'setup more remotes with unconventional refspecs' ' git fetch repo_d ' -test_expect_failure 'checkout of branch from multiple remotes fails #2' ' +test_expect_success 'checkout of branch from multiple remotes fails #2' ' test_must_fail git checkout bar ' -test_expect_failure 'checkout of branch from multiple remotes fails #3' ' +test_expect_success 'checkout of branch from multiple remotes fails #3' ' test_must_fail git checkout baz ' -test_expect_failure 'checkout of branch from a single remote succeeds #3' ' +test_expect_success 'checkout of branch from a single remote succeeds #3' ' git checkout spam && test_tracking_branch spam repo_c refs/remotes/extra_dir/repo_c/extra_dir/spam ' -- 1.8.1.3.704.g33f7d4f -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html