summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar Junio C Hamano <gitster@pobox.com>2013-05-29 14:23:10 -0700
committerLibravatar Junio C Hamano <gitster@pobox.com>2013-05-29 14:23:10 -0700
commit77eb44b8ed601a17a5ec9b1fb8c4c53ba10aaa56 (patch)
tree9f4d26a64befc377a299f2b98ac551cc824e28a9
parentMerge branch 'jc/prune-all' (diff)
parentglossary: Update and rephrase the definition of a remote-tracking branch (diff)
downloadtgif-77eb44b8ed601a17a5ec9b1fb8c4c53ba10aaa56.tar.xz
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>'
-rw-r--r--Documentation/git-checkout.txt6
-rw-r--r--Documentation/glossary-content.txt13
-rw-r--r--branch.c17
-rw-r--r--builtin/checkout.c42
-rwxr-xr-xt/t2024-checkout-dwim.sh167
-rwxr-xr-xt/t3200-branch.sh8
-rwxr-xr-xt/t7201-co.sh1
-rwxr-xr-xt/t9114-git-svn-dcommit-merge.sh2
8 files changed, 221 insertions, 35 deletions
diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index 23a9413525..ca118ac6bf 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -131,9 +131,9 @@ entries; instead, unmerged entries are ignored.
"--track" in linkgit:git-branch[1] for details.
+
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/"
-is prefixed it is stripped away, and then the part up to the
-next slash (which would be the nickname of the remote) is removed.
+derived from the remote-tracking branch, by looking at the local part of
+the refspec configured for the corresponding remote, and then stripping
+the initial part up to the "*".
This would tell us to use "hack" as the local branch when branching
off of "origin/hack" (or "remotes/origin/hack", or even
"refs/remotes/origin/hack"). If the given name has no slash, or the above
diff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt
index 68a18e1497..db2a74df93 100644
--- a/Documentation/glossary-content.txt
+++ b/Documentation/glossary-content.txt
@@ -400,12 +400,13 @@ should not be combined with other pathspec.
<<def_ref,ref>> and local ref.
[[def_remote_tracking_branch]]remote-tracking branch::
- A regular Git <<def_branch,branch>> that is used to follow changes from
- another <<def_repository,repository>>. A remote-tracking
- branch should not contain direct modifications or have local commits
- made to it. A remote-tracking branch can usually be
- identified as the right-hand-side <<def_ref,ref>> in a Pull:
- <<def_refspec,refspec>>.
+ A <<def_ref,ref>> that is used to follow changes from another
+ <<def_repository,repository>>. It typically looks like
+ 'refs/remotes/foo/bar' (indicating that it tracks a branch named
+ 'bar' in a remote named 'foo'), and matches the right-hand-side of
+ a configured fetch <<def_refspec,refspec>>. A remote-tracking
+ branch should not contain direct modifications or have local
+ commits made to it.
[[def_repository]]repository::
A collection of <<def_ref,refs>> together with an
diff --git a/branch.c b/branch.c
index 97c72bfe70..c5c6984cb5 100644
--- a/branch.c
+++ b/branch.c
@@ -197,6 +197,21 @@ int validate_new_branchname(const char *name, struct strbuf *ref,
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[] =
N_("Cannot setup tracking information; starting point '%s' is not a branch.");
static const char upstream_missing[] =
@@ -259,7 +274,7 @@ void create_branch(const char *head,
case 1:
/* Unique completion -- good, only if it is a real branch */
if (prefixcmp(real_ref, "refs/heads/") &&
- prefixcmp(real_ref, "refs/remotes/")) {
+ validate_remote_tracking_branch(real_ref)) {
if (explicit_tracking)
die(_(upstream_not_branch), start_name);
else
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 81b4419da5..f5b50e520f 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -825,38 +825,40 @@ static int git_checkout_config(const char *var, const char *value, void *cb)
}
struct tracking_name_data {
- const char *name;
- char *remote;
+ /* const */ char *src_ref;
+ char *dst_ref;
+ unsigned char *dst_sha1;
int unique;
};
-static int check_tracking_name(const char *refname, const unsigned char *sha1,
- int flags, void *cb_data)
+static int check_tracking_name(struct remote *remote, void *cb_data)
{
struct tracking_name_data *cb = cb_data;
- const char *slash;
-
- if (prefixcmp(refname, "refs/remotes/"))
- return 0;
- slash = strchr(refname + 13, '/');
- if (!slash || strcmp(slash + 1, cb->name))
+ struct refspec query;
+ memset(&query, 0, sizeof(struct refspec));
+ query.src = cb->src_ref;
+ if (remote_find_tracking(remote, &query) ||
+ get_sha1(query.dst, cb->dst_sha1))
return 0;
- if (cb->remote) {
+ if (cb->dst_ref) {
cb->unique = 0;
return 0;
}
- cb->remote = xstrdup(refname);
+ cb->dst_ref = xstrdup(query.dst);
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 };
- cb_data.name = name;
- for_each_ref(check_tracking_name, &cb_data);
+ struct tracking_name_data cb_data = { NULL, NULL, NULL, 1 };
+ char src_ref[PATH_MAX];
+ 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)
- return cb_data.remote;
- free(cb_data.remote);
+ return cb_data.dst_ref;
+ free(cb_data.dst_ref);
return NULL;
}
@@ -919,8 +921,8 @@ static int parse_branchname_arg(int argc, const char **argv,
if (dwim_new_local_branch_ok &&
!check_filename(NULL, arg) &&
argc == 1) {
- const char *remote = unique_tracking_name(arg);
- if (!remote || get_sha1(remote, rev))
+ const char *remote = unique_tracking_name(arg, rev);
+ if (!remote)
return argcount;
*new_branch = arg;
arg = remote;
diff --git a/t/t2024-checkout-dwim.sh b/t/t2024-checkout-dwim.sh
new file mode 100755
index 0000000000..dee55e428f
--- /dev/null
+++ b/t/t2024-checkout-dwim.sh
@@ -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
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index d969f0ecd8..44ec6a45f4 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -317,13 +317,13 @@ test_expect_success 'test tracking setup (non-wildcard, matching)' '
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.fetch refs/heads/s:refs/remotes/local/s &&
(git show-ref -q refs/remotes/local/master || git fetch local) &&
- git branch --track my5 local/master &&
- ! test "$(git config branch.my5.remote)" = local &&
- ! test "$(git config branch.my5.merge)" = refs/heads/master
+ test_must_fail git branch --track my5 local/master &&
+ test_must_fail git config branch.my5.remote &&
+ test_must_fail git config branch.my5.merge
'
test_expect_success 'test tracking setup via config' '
diff --git a/t/t7201-co.sh b/t/t7201-co.sh
index be9672e5a0..0c9ec0ad44 100755
--- a/t/t7201-co.sh
+++ b/t/t7201-co.sh
@@ -431,6 +431,7 @@ test_expect_success 'detach a symbolic link HEAD' '
test_expect_success \
'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 checkout --track origin/koala/bear &&
diff --git a/t/t9114-git-svn-dcommit-merge.sh b/t/t9114-git-svn-dcommit-merge.sh
index 3077851015..f524d2f383 100755
--- a/t/t9114-git-svn-dcommit-merge.sh
+++ b/t/t9114-git-svn-dcommit-merge.sh
@@ -48,7 +48,7 @@ test_expect_success 'setup svn repository' '
test_expect_success 'setup git mirror and merge' '
git svn init "$svnrepo" -t tags -T trunk -b branches &&
git svn fetch &&
- git checkout --track -b svn remotes/trunk &&
+ git checkout -b svn remotes/trunk &&
git checkout -b merge &&
echo new file > new_file &&
git add new_file &&