Skip to content

Commit fa83a33

Browse files
jherlandgitster
authored andcommitted
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 <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent ec2764e commit fa83a33

File tree

3 files changed

+28
-26
lines changed

3 files changed

+28
-26
lines changed

Documentation/git-checkout.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,9 @@ entries; instead, unmerged entries are ignored.
131131
"--track" in linkgit:git-branch[1] for details.
132132
+
133133
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 "*".
137137
This would tell us to use "hack" as the local branch when branching
138138
off of "origin/hack" (or "remotes/origin/hack", or even
139139
"refs/remotes/origin/hack"). If the given name has no slash, or the above

builtin/checkout.c

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -822,38 +822,40 @@ static int git_checkout_config(const char *var, const char *value, void *cb)
822822
}
823823

824824
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;
827828
int unique;
828829
};
829830

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)
832832
{
833833
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))
840839
return 0;
841-
if (cb->remote) {
840+
if (cb->dst_ref) {
842841
cb->unique = 0;
843842
return 0;
844843
}
845-
cb->remote = xstrdup(refname);
844+
cb->dst_ref = xstrdup(query.dst);
846845
return 0;
847846
}
848847

849-
static const char *unique_tracking_name(const char *name)
848+
static const char *unique_tracking_name(const char *name, unsigned char *sha1)
850849
{
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);
854856
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);
857859
return NULL;
858860
}
859861

@@ -916,8 +918,8 @@ static int parse_branchname_arg(int argc, const char **argv,
916918
if (dwim_new_local_branch_ok &&
917919
!check_filename(NULL, arg) &&
918920
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)
921923
return argcount;
922924
*new_branch = arg;
923925
arg = remote;

t/t2024-checkout-dwim.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ test_expect_success 'setup more remotes with unconventional refspecs' '
126126
git fetch --all
127127
'
128128

129-
test_expect_failure 'checkout of branch from multiple remotes fails #2' '
129+
test_expect_success 'checkout of branch from multiple remotes fails #2' '
130130
git checkout -B master &&
131131
test_might_fail git branch -D bar &&
132132
@@ -135,7 +135,7 @@ test_expect_failure 'checkout of branch from multiple remotes fails #2' '
135135
test_branch master
136136
'
137137

138-
test_expect_failure 'checkout of branch from multiple remotes fails #3' '
138+
test_expect_success 'checkout of branch from multiple remotes fails #3' '
139139
git checkout -B master &&
140140
test_might_fail git branch -D baz &&
141141
@@ -144,7 +144,7 @@ test_expect_failure 'checkout of branch from multiple remotes fails #3' '
144144
test_branch master
145145
'
146146

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' '
148148
git checkout -B master &&
149149
test_might_fail git branch -D spam &&
150150

0 commit comments

Comments
 (0)