Merge branch 'jh/checkout-auto-tracking' into maint

* 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>'
This commit is contained in:
Junio C Hamano 2013-06-27 14:37:21 -07:00
commit 11fbc0b1e1
8 changed files with 221 additions and 35 deletions

View File

@ -131,9 +131,9 @@ entries; instead, unmerged entries are ignored.
"--track" in linkgit:git-branch[1] for details. "--track" in linkgit:git-branch[1] for details.
+ +
If no '-b' option is given, the name of the new branch will be 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/" derived from the remote-tracking branch, by looking at the local part of
is prefixed it is stripped away, and then the part up to the the refspec configured for the corresponding remote, and then stripping
next slash (which would be the nickname of the remote) is removed. the initial part up to the "*".
This would tell us to use "hack" as the local branch when branching This would tell us to use "hack" as the local branch when branching
off of "origin/hack" (or "remotes/origin/hack", or even off of "origin/hack" (or "remotes/origin/hack", or even
"refs/remotes/origin/hack"). If the given name has no slash, or the above "refs/remotes/origin/hack"). If the given name has no slash, or the above

View File

@ -400,12 +400,13 @@ should not be combined with other pathspec.
<<def_ref,ref>> and local ref. <<def_ref,ref>> and local ref.
[[def_remote_tracking_branch]]remote-tracking branch:: [[def_remote_tracking_branch]]remote-tracking branch::
A regular Git <<def_branch,branch>> that is used to follow changes from A <<def_ref,ref>> that is used to follow changes from another
another <<def_repository,repository>>. A remote-tracking <<def_repository,repository>>. It typically looks like
branch should not contain direct modifications or have local commits 'refs/remotes/foo/bar' (indicating that it tracks a branch named
made to it. A remote-tracking branch can usually be 'bar' in a remote named 'foo'), and matches the right-hand-side of
identified as the right-hand-side <<def_ref,ref>> in a Pull: a configured fetch <<def_refspec,refspec>>. A remote-tracking
<<def_refspec,refspec>>. branch should not contain direct modifications or have local
commits made to it.
[[def_repository]]repository:: [[def_repository]]repository::
A collection of <<def_ref,refs>> together with an A collection of <<def_ref,refs>> together with an

View File

@ -197,6 +197,21 @@ int validate_new_branchname(const char *name, struct strbuf *ref,
return 1; return 1;
} }
static int check_tracking_branch(struct remote *remote, void *cb_data)
{
char *tracking_branch = cb_data;
struct refspec query;
memset(&query, 0, sizeof(struct refspec));
query.dst = tracking_branch;
return !(remote_find_tracking(remote, &query) ||
prefixcmp(query.src, "refs/heads/"));
}
static int validate_remote_tracking_branch(char *ref)
{
return !for_each_remote(check_tracking_branch, ref);
}
static const char upstream_not_branch[] = static const char upstream_not_branch[] =
N_("Cannot setup tracking information; starting point '%s' is not a branch."); N_("Cannot setup tracking information; starting point '%s' is not a branch.");
static const char upstream_missing[] = static const char upstream_missing[] =
@ -259,7 +274,7 @@ void create_branch(const char *head,
case 1: case 1:
/* Unique completion -- good, only if it is a real branch */ /* Unique completion -- good, only if it is a real branch */
if (prefixcmp(real_ref, "refs/heads/") && if (prefixcmp(real_ref, "refs/heads/") &&
prefixcmp(real_ref, "refs/remotes/")) { validate_remote_tracking_branch(real_ref)) {
if (explicit_tracking) if (explicit_tracking)
die(_(upstream_not_branch), start_name); die(_(upstream_not_branch), start_name);
else else

View File

@ -825,38 +825,40 @@ static int git_checkout_config(const char *var, const char *value, void *cb)
} }
struct tracking_name_data { struct tracking_name_data {
const char *name; /* const */ char *src_ref;
char *remote; char *dst_ref;
unsigned char *dst_sha1;
int unique; int unique;
}; };
static int check_tracking_name(const char *refname, const unsigned char *sha1, static int check_tracking_name(struct remote *remote, void *cb_data)
int flags, void *cb_data)
{ {
struct tracking_name_data *cb = cb_data; struct tracking_name_data *cb = cb_data;
const char *slash; struct refspec query;
memset(&query, 0, sizeof(struct refspec));
if (prefixcmp(refname, "refs/remotes/")) query.src = cb->src_ref;
if (remote_find_tracking(remote, &query) ||
get_sha1(query.dst, cb->dst_sha1))
return 0; return 0;
slash = strchr(refname + 13, '/'); if (cb->dst_ref) {
if (!slash || strcmp(slash + 1, cb->name))
return 0;
if (cb->remote) {
cb->unique = 0; cb->unique = 0;
return 0; return 0;
} }
cb->remote = xstrdup(refname); cb->dst_ref = xstrdup(query.dst);
return 0; 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 }; struct tracking_name_data cb_data = { NULL, NULL, NULL, 1 };
cb_data.name = name; char src_ref[PATH_MAX];
for_each_ref(check_tracking_name, &cb_data); 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) if (cb_data.unique)
return cb_data.remote; return cb_data.dst_ref;
free(cb_data.remote); free(cb_data.dst_ref);
return NULL; return NULL;
} }
@ -919,8 +921,8 @@ static int parse_branchname_arg(int argc, const char **argv,
if (dwim_new_local_branch_ok && if (dwim_new_local_branch_ok &&
!check_filename(NULL, arg) && !check_filename(NULL, arg) &&
argc == 1) { argc == 1) {
const char *remote = unique_tracking_name(arg); const char *remote = unique_tracking_name(arg, rev);
if (!remote || get_sha1(remote, rev)) if (!remote)
return argcount; return argcount;
*new_branch = arg; *new_branch = arg;
arg = remote; arg = remote;

167
t/t2024-checkout-dwim.sh Executable file
View File

@ -0,0 +1,167 @@
#!/bin/sh
test_description='checkout <branch>
Ensures that checkout on an unborn branch does what the user expects'
. ./test-lib.sh
# Is the current branch "refs/heads/$1"?
test_branch () {
printf "%s\n" "refs/heads/$1" >expect.HEAD &&
git symbolic-ref HEAD >actual.HEAD &&
test_cmp expect.HEAD actual.HEAD
}
# Is branch "refs/heads/$1" set to pull from "$2/$3"?
test_branch_upstream () {
printf "%s\n" "$2" "refs/heads/$3" >expect.upstream &&
{
git config "branch.$1.remote" &&
git config "branch.$1.merge"
} >actual.upstream &&
test_cmp expect.upstream actual.upstream
}
test_expect_success 'setup' '
test_commit my_master &&
git init repo_a &&
(
cd repo_a &&
test_commit a_master &&
git checkout -b foo &&
test_commit a_foo &&
git checkout -b bar &&
test_commit a_bar
) &&
git init repo_b &&
(
cd repo_b &&
test_commit b_master &&
git checkout -b foo &&
test_commit b_foo &&
git checkout -b baz &&
test_commit b_baz
) &&
git remote add repo_a repo_a &&
git remote add repo_b repo_b &&
git config remote.repo_b.fetch \
"+refs/heads/*:refs/remotes/other_b/*" &&
git fetch --all
'
test_expect_success 'checkout of non-existing branch fails' '
git checkout -B master &&
test_might_fail git branch -D xyzzy &&
test_must_fail git checkout xyzzy &&
test_must_fail git rev-parse --verify refs/heads/xyzzy &&
test_branch master
'
test_expect_success 'checkout of branch from multiple remotes fails #1' '
git checkout -B master &&
test_might_fail git branch -D foo &&
test_must_fail git checkout foo &&
test_must_fail git rev-parse --verify refs/heads/foo &&
test_branch master
'
test_expect_success 'checkout of branch from a single remote succeeds #1' '
git checkout -B master &&
test_might_fail git branch -D bar &&
git checkout bar &&
test_branch bar &&
test_cmp_rev remotes/repo_a/bar HEAD &&
test_branch_upstream bar repo_a bar
'
test_expect_success 'checkout of branch from a single remote succeeds #2' '
git checkout -B master &&
test_might_fail git branch -D baz &&
git checkout baz &&
test_branch baz &&
test_cmp_rev remotes/other_b/baz HEAD &&
test_branch_upstream baz repo_b baz
'
test_expect_success '--no-guess suppresses branch auto-vivification' '
git checkout -B master &&
test_might_fail git branch -D bar &&
test_must_fail git checkout --no-guess bar &&
test_must_fail git rev-parse --verify refs/heads/bar &&
test_branch master
'
test_expect_success 'setup more remotes with unconventional refspecs' '
git checkout -B master &&
git init repo_c &&
(
cd repo_c &&
test_commit c_master &&
git checkout -b bar &&
test_commit c_bar
git checkout -b spam &&
test_commit c_spam
) &&
git init repo_d &&
(
cd repo_d &&
test_commit d_master &&
git checkout -b baz &&
test_commit f_baz
git checkout -b eggs &&
test_commit c_eggs
) &&
git remote add repo_c repo_c &&
git config remote.repo_c.fetch \
"+refs/heads/*:refs/remotes/extra_dir/repo_c/extra_dir/*" &&
git remote add repo_d repo_d &&
git config remote.repo_d.fetch \
"+refs/heads/*:refs/repo_d/*" &&
git fetch --all
'
test_expect_success 'checkout of branch from multiple remotes fails #2' '
git checkout -B master &&
test_might_fail git branch -D bar &&
test_must_fail git checkout bar &&
test_must_fail git rev-parse --verify refs/heads/bar &&
test_branch master
'
test_expect_success 'checkout of branch from multiple remotes fails #3' '
git checkout -B master &&
test_might_fail git branch -D baz &&
test_must_fail git checkout baz &&
test_must_fail git rev-parse --verify refs/heads/baz &&
test_branch master
'
test_expect_success 'checkout of branch from a single remote succeeds #3' '
git checkout -B master &&
test_might_fail git branch -D spam &&
git checkout spam &&
test_branch spam &&
test_cmp_rev refs/remotes/extra_dir/repo_c/extra_dir/spam HEAD &&
test_branch_upstream spam repo_c spam
'
test_expect_success 'checkout of branch from a single remote succeeds #4' '
git checkout -B master &&
test_might_fail git branch -D eggs &&
git checkout eggs &&
test_branch eggs &&
test_cmp_rev refs/repo_d/eggs HEAD &&
test_branch_upstream eggs repo_d eggs
'
test_done

View File

@ -317,13 +317,13 @@ test_expect_success 'test tracking setup (non-wildcard, matching)' '
test $(git config branch.my4.merge) = refs/heads/master test $(git config branch.my4.merge) = refs/heads/master
' '
test_expect_success 'test tracking setup (non-wildcard, not matching)' ' test_expect_success 'tracking setup fails on non-matching refspec' '
git config remote.local.url . && git config remote.local.url . &&
git config remote.local.fetch refs/heads/s:refs/remotes/local/s && git config remote.local.fetch refs/heads/s:refs/remotes/local/s &&
(git show-ref -q refs/remotes/local/master || git fetch local) && (git show-ref -q refs/remotes/local/master || git fetch local) &&
git branch --track my5 local/master && test_must_fail git branch --track my5 local/master &&
! test "$(git config branch.my5.remote)" = local && test_must_fail git config branch.my5.remote &&
! test "$(git config branch.my5.merge)" = refs/heads/master test_must_fail git config branch.my5.merge
' '
test_expect_success 'test tracking setup via config' ' test_expect_success 'test tracking setup via config' '

View File

@ -431,6 +431,7 @@ test_expect_success 'detach a symbolic link HEAD' '
test_expect_success \ test_expect_success \
'checkout with --track fakes a sensible -b <name>' ' 'checkout with --track fakes a sensible -b <name>' '
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" &&
git update-ref refs/remotes/origin/koala/bear renamer && git update-ref refs/remotes/origin/koala/bear renamer &&
git checkout --track origin/koala/bear && git checkout --track origin/koala/bear &&

View File

@ -48,7 +48,7 @@ test_expect_success 'setup svn repository' '
test_expect_success 'setup git mirror and merge' ' test_expect_success 'setup git mirror and merge' '
git svn init "$svnrepo" -t tags -T trunk -b branches && git svn init "$svnrepo" -t tags -T trunk -b branches &&
git svn fetch && git svn fetch &&
git checkout --track -b svn remotes/trunk && git checkout -b svn remotes/trunk &&
git checkout -b merge && git checkout -b merge &&
echo new file > new_file && echo new file > new_file &&
git add new_file && git add new_file &&