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:
commit
11fbc0b1e1
@ -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
|
||||||
|
@ -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
|
||||||
|
17
branch.c
17
branch.c
@ -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
|
||||||
|
@ -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
167
t/t2024-checkout-dwim.sh
Executable 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
|
@ -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' '
|
||||||
|
@ -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 &&
|
||||||
|
@ -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 &&
|
||||||
|
Loading…
Reference in New Issue
Block a user