checkout: Use remote refspecs when DWIMming tracking branches

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 automatically create a new local branch called "foo" using the remote-tracking "foo" as its starting point and configured upstream. For example, consider the following unconventional (but perfectly valid) remote setup: [remote "origin"] fetch = refs/heads/*:refs/remotes/origin/* [remote "frotz"] fetch = refs/heads/*:refs/remotes/frotz/nitfol/* Case 1: Assume both "origin" and "frotz" have remote-tracking branches called "foo", at "refs/remotes/origin/foo" and "refs/remotes/frotz/nitfol/foo" respectively. In this case "git checkout foo" should fail, because there is more than one remote with a "foo" branch. Case 2: Assume only "frotz" have a remote-tracking branch called "foo". In this case "git checkout foo" should succeed, and create a local branch "foo" from "refs/remotes/frotz/nitfol/foo", using remote branch "foo" from "frotz" as its upstream. The current code hardcodes the assumption that all remote-tracking branches must match the "refs/remotes/$remote/*" pattern (which is true for remotes with "conventional" refspecs, but not true for the "frotz" remote above). When running "git checkout foo", the current code looks for exactly one ref matching "refs/remotes/*/foo", hence in the above example, it fails to find "refs/remotes/frotz/nitfol/foo", which causes it to fail both case #1 and #2. The better way to handle the above example is to actually study the fetch refspecs to deduce the candidate remote-tracking branches for "foo"; i.e. assume "foo" is a remote branch being fetched, and then map "refs/heads/foo" through the refspecs in order to get the corresponding remote-tracking branches "refs/remotes/origin/foo" and "refs/remotes/frotz/nitfol/foo". Finally we check which of these happens to exist in the local repo, and if there is exactly one, we have an unambiguous match for "git checkout foo", and may proceed. This fixes most of the failing tests introduced in the previous patch. Signed-off-by: Johan Herland <johan@herland.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Johan Herland committed Apr 21, 2013 at 23:52 UTC fa83a33b22283e465aafdacc53ad0fddea55bdf4
3 files changed +28 -26
Documentation/git-checkout.txt
+3 -3
@@ -131,9 +131,9 @@ entries; instead, unmerged entries are ignored.
131 "--track" in linkgit:git-branch[1] for details.
132 +
133 If no '-b' option is given, the name of the new branch will be
134 -derived from the remote-tracking branch. If "remotes/" or "refs/remotes/"
135 -is prefixed it is stripped away, and then the part up to the
136 -next slash (which would be the nickname of the remote) is removed.
134 +derived from the remote-tracking branch, by looking at the local part of
135 +the refspec configured for the corresponding remote, and then stripping
136 +the initial part up to the "*".
137 This would tell us to use "hack" as the local branch when branching
138 off of "origin/hack" (or "remotes/origin/hack", or even
139 "refs/remotes/origin/hack"). If the given name has no slash, or the above
builtin/checkout.c
+22 -20
@@ -822,38 +822,40 @@ static int git_checkout_config(const char *var, const char *value, void *cb)
822 }
823
824 struct tracking_name_data {
825 - const char *name;
826 - char *remote;
825 + /* const */ char *src_ref;
826 + char *dst_ref;
827 + unsigned char *dst_sha1;
828 int unique;
829 };
830
830 -static int check_tracking_name(const char *refname, const unsigned char *sha1,
831 - int flags, void *cb_data)
831 +static int check_tracking_name(struct remote *remote, void *cb_data)
832 {
833 struct tracking_name_data *cb = cb_data;
834 - const char *slash;
835 -
836 - if (prefixcmp(refname, "refs/remotes/"))
837 - return 0;
838 - slash = strchr(refname + 13, '/');
839 - if (!slash || strcmp(slash + 1, cb->name))
834 + struct refspec query;
835 + memset(&query, 0, sizeof(struct refspec));
836 + query.src = cb->src_ref;
837 + if (remote_find_tracking(remote, &query) ||
838 + get_sha1(query.dst, cb->dst_sha1))
839 return 0;
841 - if (cb->remote) {
840 + if (cb->dst_ref) {
841 cb->unique = 0;
842 return 0;
843 }
845 - cb->remote = xstrdup(refname);
844 + cb->dst_ref = xstrdup(query.dst);
845 return 0;
846 }
847
849 -static const char *unique_tracking_name(const char *name)
848 +static const char *unique_tracking_name(const char *name, unsigned char *sha1)
849 {
851 - struct tracking_name_data cb_data = { NULL, NULL, 1 };
852 - cb_data.name = name;
853 - for_each_ref(check_tracking_name, &cb_data);
850 + struct tracking_name_data cb_data = { NULL, NULL, NULL, 1 };
851 + char src_ref[PATH_MAX];
852 + snprintf(src_ref, PATH_MAX, "refs/heads/%s", name);
853 + cb_data.src_ref = src_ref;
854 + cb_data.dst_sha1 = sha1;
855 + for_each_remote(check_tracking_name, &cb_data);
856 if (cb_data.unique)
855 - return cb_data.remote;
856 - free(cb_data.remote);
857 + return cb_data.dst_ref;
858 + free(cb_data.dst_ref);
859 return NULL;
860 }
861
@@ -916,8 +918,8 @@ static int parse_branchname_arg(int argc, const char **argv,
918 if (dwim_new_local_branch_ok &&
919 !check_filename(NULL, arg) &&
920 argc == 1) {
919 - const char *remote = unique_tracking_name(arg);
920 - if (!remote || get_sha1(remote, rev))
921 + const char *remote = unique_tracking_name(arg, rev);
922 + if (!remote)
923 return argcount;
924 *new_branch = arg;
925 arg = remote;
t/t2024-checkout-dwim.sh
+3 -3
@@ -126,7 +126,7 @@ test_expect_success 'setup more remotes with unconventional refspecs' '
126 git fetch --all
127 '
128
129 -test_expect_failure 'checkout of branch from multiple remotes fails #2' '
129 +test_expect_success 'checkout of branch from multiple remotes fails #2' '
130 git checkout -B master &&
131 test_might_fail git branch -D bar &&
132
@@ -135,7 +135,7 @@ test_expect_failure 'checkout of branch from multiple remotes fails #2' '
135 test_branch master
136 '
137
138 -test_expect_failure 'checkout of branch from multiple remotes fails #3' '
138 +test_expect_success 'checkout of branch from multiple remotes fails #3' '
139 git checkout -B master &&
140 test_might_fail git branch -D baz &&
141
@@ -144,7 +144,7 @@ test_expect_failure 'checkout of branch from multiple remotes fails #3' '
144 test_branch master
145 '
146
147 -test_expect_failure 'checkout of branch from a single remote succeeds #3' '
147 +test_expect_success 'checkout of branch from a single remote succeeds #3' '
148 git checkout -B master &&
149 test_might_fail git branch -D spam &&
150