Skip to content

Commit 77eb44b

Browse files
committed
Merge branch 'jh/checkout-auto-tracking'
Update "git checkout foo" that DWIMs the intended "upstream" and turns it into "git checkout -t -b foo remotes/origin/foo" to correctly take existing remote definitions into account. The remote "origin" may be what uniquely map its own branch to remotes/some/where/foo but that some/where may not be "origin". * jh/checkout-auto-tracking: glossary: Update and rephrase the definition of a remote-tracking branch branch.c: Validate tracking branches with refspecs instead of refs/remotes/* t9114.2: Don't use --track option against "svn-remote"-tracking branches t7201.24: Add refspec to keep --track working t3200.39: tracking setup should fail if there is no matching refspec. checkout: Use remote refspecs when DWIMming tracking branches t2024: Show failure to use refspec when DWIMming remote branch names t2024: Add tests verifying current DWIM behavior of 'git checkout <branch>'
2 parents 3e1e762 + 229177a commit 77eb44b

File tree

8 files changed

+221
-35
lines changed

8 files changed

+221
-35
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

Documentation/glossary-content.txt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -400,12 +400,13 @@ should not be combined with other pathspec.
400400
<<def_ref,ref>> and local ref.
401401

402402
[[def_remote_tracking_branch]]remote-tracking branch::
403-
A regular Git <<def_branch,branch>> that is used to follow changes from
404-
another <<def_repository,repository>>. A remote-tracking
405-
branch should not contain direct modifications or have local commits
406-
made to it. A remote-tracking branch can usually be
407-
identified as the right-hand-side <<def_ref,ref>> in a Pull:
408-
<<def_refspec,refspec>>.
403+
A <<def_ref,ref>> that is used to follow changes from another
404+
<<def_repository,repository>>. It typically looks like
405+
'refs/remotes/foo/bar' (indicating that it tracks a branch named
406+
'bar' in a remote named 'foo'), and matches the right-hand-side of
407+
a configured fetch <<def_refspec,refspec>>. A remote-tracking
408+
branch should not contain direct modifications or have local
409+
commits made to it.
409410

410411
[[def_repository]]repository::
411412
A collection of <<def_ref,refs>> together with an

branch.c

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,21 @@ int validate_new_branchname(const char *name, struct strbuf *ref,
197197
return 1;
198198
}
199199

200+
static int check_tracking_branch(struct remote *remote, void *cb_data)
201+
{
202+
char *tracking_branch = cb_data;
203+
struct refspec query;
204+
memset(&query, 0, sizeof(struct refspec));
205+
query.dst = tracking_branch;
206+
return !(remote_find_tracking(remote, &query) ||
207+
prefixcmp(query.src, "refs/heads/"));
208+
}
209+
210+
static int validate_remote_tracking_branch(char *ref)
211+
{
212+
return !for_each_remote(check_tracking_branch, ref);
213+
}
214+
200215
static const char upstream_not_branch[] =
201216
N_("Cannot setup tracking information; starting point '%s' is not a branch.");
202217
static const char upstream_missing[] =
@@ -259,7 +274,7 @@ void create_branch(const char *head,
259274
case 1:
260275
/* Unique completion -- good, only if it is a real branch */
261276
if (prefixcmp(real_ref, "refs/heads/") &&
262-
prefixcmp(real_ref, "refs/remotes/")) {
277+
validate_remote_tracking_branch(real_ref)) {
263278
if (explicit_tracking)
264279
die(_(upstream_not_branch), start_name);
265280
else

builtin/checkout.c

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

827827
struct tracking_name_data {
828-
const char *name;
829-
char *remote;
828+
/* const */ char *src_ref;
829+
char *dst_ref;
830+
unsigned char *dst_sha1;
830831
int unique;
831832
};
832833

833-
static int check_tracking_name(const char *refname, const unsigned char *sha1,
834-
int flags, void *cb_data)
834+
static int check_tracking_name(struct remote *remote, void *cb_data)
835835
{
836836
struct tracking_name_data *cb = cb_data;
837-
const char *slash;
838-
839-
if (prefixcmp(refname, "refs/remotes/"))
840-
return 0;
841-
slash = strchr(refname + 13, '/');
842-
if (!slash || strcmp(slash + 1, cb->name))
837+
struct refspec query;
838+
memset(&query, 0, sizeof(struct refspec));
839+
query.src = cb->src_ref;
840+
if (remote_find_tracking(remote, &query) ||
841+
get_sha1(query.dst, cb->dst_sha1))
843842
return 0;
844-
if (cb->remote) {
843+
if (cb->dst_ref) {
845844
cb->unique = 0;
846845
return 0;
847846
}
848-
cb->remote = xstrdup(refname);
847+
cb->dst_ref = xstrdup(query.dst);
849848
return 0;
850849
}
851850

852-
static const char *unique_tracking_name(const char *name)
851+
static const char *unique_tracking_name(const char *name, unsigned char *sha1)
853852
{
854-
struct tracking_name_data cb_data = { NULL, NULL, 1 };
855-
cb_data.name = name;
856-
for_each_ref(check_tracking_name, &cb_data);
853+
struct tracking_name_data cb_data = { NULL, NULL, NULL, 1 };
854+
char src_ref[PATH_MAX];
855+
snprintf(src_ref, PATH_MAX, "refs/heads/%s", name);
856+
cb_data.src_ref = src_ref;
857+
cb_data.dst_sha1 = sha1;
858+
for_each_remote(check_tracking_name, &cb_data);
857859
if (cb_data.unique)
858-
return cb_data.remote;
859-
free(cb_data.remote);
860+
return cb_data.dst_ref;
861+
free(cb_data.dst_ref);
860862
return NULL;
861863
}
862864

@@ -919,8 +921,8 @@ static int parse_branchname_arg(int argc, const char **argv,
919921
if (dwim_new_local_branch_ok &&
920922
!check_filename(NULL, arg) &&
921923
argc == 1) {
922-
const char *remote = unique_tracking_name(arg);
923-
if (!remote || get_sha1(remote, rev))
924+
const char *remote = unique_tracking_name(arg, rev);
925+
if (!remote)
924926
return argcount;
925927
*new_branch = arg;
926928
arg = remote;

t/t2024-checkout-dwim.sh

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/bin/sh
2+
3+
test_description='checkout <branch>
4+
5+
Ensures that checkout on an unborn branch does what the user expects'
6+
7+
. ./test-lib.sh
8+
9+
# Is the current branch "refs/heads/$1"?
10+
test_branch () {
11+
printf "%s\n" "refs/heads/$1" >expect.HEAD &&
12+
git symbolic-ref HEAD >actual.HEAD &&
13+
test_cmp expect.HEAD actual.HEAD
14+
}
15+
16+
# Is branch "refs/heads/$1" set to pull from "$2/$3"?
17+
test_branch_upstream () {
18+
printf "%s\n" "$2" "refs/heads/$3" >expect.upstream &&
19+
{
20+
git config "branch.$1.remote" &&
21+
git config "branch.$1.merge"
22+
} >actual.upstream &&
23+
test_cmp expect.upstream actual.upstream
24+
}
25+
26+
test_expect_success 'setup' '
27+
test_commit my_master &&
28+
git init repo_a &&
29+
(
30+
cd repo_a &&
31+
test_commit a_master &&
32+
git checkout -b foo &&
33+
test_commit a_foo &&
34+
git checkout -b bar &&
35+
test_commit a_bar
36+
) &&
37+
git init repo_b &&
38+
(
39+
cd repo_b &&
40+
test_commit b_master &&
41+
git checkout -b foo &&
42+
test_commit b_foo &&
43+
git checkout -b baz &&
44+
test_commit b_baz
45+
) &&
46+
git remote add repo_a repo_a &&
47+
git remote add repo_b repo_b &&
48+
git config remote.repo_b.fetch \
49+
"+refs/heads/*:refs/remotes/other_b/*" &&
50+
git fetch --all
51+
'
52+
53+
test_expect_success 'checkout of non-existing branch fails' '
54+
git checkout -B master &&
55+
test_might_fail git branch -D xyzzy &&
56+
57+
test_must_fail git checkout xyzzy &&
58+
test_must_fail git rev-parse --verify refs/heads/xyzzy &&
59+
test_branch master
60+
'
61+
62+
test_expect_success 'checkout of branch from multiple remotes fails #1' '
63+
git checkout -B master &&
64+
test_might_fail git branch -D foo &&
65+
66+
test_must_fail git checkout foo &&
67+
test_must_fail git rev-parse --verify refs/heads/foo &&
68+
test_branch master
69+
'
70+
71+
test_expect_success 'checkout of branch from a single remote succeeds #1' '
72+
git checkout -B master &&
73+
test_might_fail git branch -D bar &&
74+
75+
git checkout bar &&
76+
test_branch bar &&
77+
test_cmp_rev remotes/repo_a/bar HEAD &&
78+
test_branch_upstream bar repo_a bar
79+
'
80+
81+
test_expect_success 'checkout of branch from a single remote succeeds #2' '
82+
git checkout -B master &&
83+
test_might_fail git branch -D baz &&
84+
85+
git checkout baz &&
86+
test_branch baz &&
87+
test_cmp_rev remotes/other_b/baz HEAD &&
88+
test_branch_upstream baz repo_b baz
89+
'
90+
91+
test_expect_success '--no-guess suppresses branch auto-vivification' '
92+
git checkout -B master &&
93+
test_might_fail git branch -D bar &&
94+
95+
test_must_fail git checkout --no-guess bar &&
96+
test_must_fail git rev-parse --verify refs/heads/bar &&
97+
test_branch master
98+
'
99+
100+
test_expect_success 'setup more remotes with unconventional refspecs' '
101+
git checkout -B master &&
102+
git init repo_c &&
103+
(
104+
cd repo_c &&
105+
test_commit c_master &&
106+
git checkout -b bar &&
107+
test_commit c_bar
108+
git checkout -b spam &&
109+
test_commit c_spam
110+
) &&
111+
git init repo_d &&
112+
(
113+
cd repo_d &&
114+
test_commit d_master &&
115+
git checkout -b baz &&
116+
test_commit f_baz
117+
git checkout -b eggs &&
118+
test_commit c_eggs
119+
) &&
120+
git remote add repo_c repo_c &&
121+
git config remote.repo_c.fetch \
122+
"+refs/heads/*:refs/remotes/extra_dir/repo_c/extra_dir/*" &&
123+
git remote add repo_d repo_d &&
124+
git config remote.repo_d.fetch \
125+
"+refs/heads/*:refs/repo_d/*" &&
126+
git fetch --all
127+
'
128+
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+
133+
test_must_fail git checkout bar &&
134+
test_must_fail git rev-parse --verify refs/heads/bar &&
135+
test_branch master
136+
'
137+
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+
142+
test_must_fail git checkout baz &&
143+
test_must_fail git rev-parse --verify refs/heads/baz &&
144+
test_branch master
145+
'
146+
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+
151+
git checkout spam &&
152+
test_branch spam &&
153+
test_cmp_rev refs/remotes/extra_dir/repo_c/extra_dir/spam HEAD &&
154+
test_branch_upstream spam repo_c spam
155+
'
156+
157+
test_expect_success 'checkout of branch from a single remote succeeds #4' '
158+
git checkout -B master &&
159+
test_might_fail git branch -D eggs &&
160+
161+
git checkout eggs &&
162+
test_branch eggs &&
163+
test_cmp_rev refs/repo_d/eggs HEAD &&
164+
test_branch_upstream eggs repo_d eggs
165+
'
166+
167+
test_done

t/t3200-branch.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -317,13 +317,13 @@ test_expect_success 'test tracking setup (non-wildcard, matching)' '
317317
test $(git config branch.my4.merge) = refs/heads/master
318318
'
319319

320-
test_expect_success 'test tracking setup (non-wildcard, not matching)' '
320+
test_expect_success 'tracking setup fails on non-matching refspec' '
321321
git config remote.local.url . &&
322322
git config remote.local.fetch refs/heads/s:refs/remotes/local/s &&
323323
(git show-ref -q refs/remotes/local/master || git fetch local) &&
324-
git branch --track my5 local/master &&
325-
! test "$(git config branch.my5.remote)" = local &&
326-
! test "$(git config branch.my5.merge)" = refs/heads/master
324+
test_must_fail git branch --track my5 local/master &&
325+
test_must_fail git config branch.my5.remote &&
326+
test_must_fail git config branch.my5.merge
327327
'
328328

329329
test_expect_success 'test tracking setup via config' '

t/t7201-co.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ test_expect_success 'detach a symbolic link HEAD' '
431431

432432
test_expect_success \
433433
'checkout with --track fakes a sensible -b <name>' '
434+
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" &&
434435
git update-ref refs/remotes/origin/koala/bear renamer &&
435436
436437
git checkout --track origin/koala/bear &&

t/t9114-git-svn-dcommit-merge.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ test_expect_success 'setup svn repository' '
4848
test_expect_success 'setup git mirror and merge' '
4949
git svn init "$svnrepo" -t tags -T trunk -b branches &&
5050
git svn fetch &&
51-
git checkout --track -b svn remotes/trunk &&
51+
git checkout -b svn remotes/trunk &&
5252
git checkout -b merge &&
5353
echo new file > new_file &&
5454
git add new_file &&

0 commit comments

Comments
 (0)