checkout & worktree: introduce checkout.defaultRemote

Introduce a checkout.defaultRemote setting which can be used to
designate a remote to prefer (via checkout.defaultRemote=origin) when
running e.g. "git checkout master" to mean origin/master, even though
there's other remotes that have the "master" branch.

I want this because it's very handy to use this workflow to checkout a
repository and create a topic branch, then get back to a "master" as
retrieved from upstream:

    (
        cd /tmp &&
        rm -rf tbdiff &&
        git clone git@github.com:trast/tbdiff.git &&
        cd tbdiff &&
        git branch -m topic &&
        git checkout master
    )

That will output:

    Branch 'master' set up to track remote branch 'master' from 'origin'.
    Switched to a new branch 'master'

But as soon as a new remote is added (e.g. just to inspect something
from someone else) the DWIMery goes away:

    (
        cd /tmp &&
        rm -rf tbdiff &&
        git clone git@github.com:trast/tbdiff.git &&
        cd tbdiff &&
        git branch -m topic &&
        git remote add avar git@github.com:avar/tbdiff.git &&
        git fetch avar &&
        git checkout master
    )

Will output (without the advice output added earlier in this series):

    error: pathspec 'master' did not match any file(s) known to git.

The new checkout.defaultRemote config allows me to say that whenever
that ambiguity comes up I'd like to prefer "origin", and it'll still
work as though the only remote I had was "origin".

Also adjust the advice.checkoutAmbiguousRemoteBranchName message to
mention this new config setting to the user, the full output on my
git.git is now (the last paragraph is new):

    $ ./git --exec-path=$PWD checkout master
    error: pathspec 'master' did not match any file(s) known to git.
    hint: 'master' matched more than one remote tracking branch.
    hint: We found 26 remotes with a reference that matched. So we fell back
    hint: on trying to resolve the argument as a path, but failed there too!
    hint:
    hint: If you meant to check out a remote tracking branch on, e.g. 'origin',
    hint: you can do so by fully qualifying the name with the --track option:
    hint:
    hint:     git checkout --track origin/<name>
    hint:
    hint: If you'd like to always have checkouts of an ambiguous <name> prefer
    hint: one remote, e.g. the 'origin' remote, consider setting
    hint: checkout.defaultRemote=origin in your config.

I considered splitting this into checkout.defaultRemote and
worktree.defaultRemote, but it's probably less confusing to break our
own rules that anything shared between config should live in core.*
than have two config settings, and I couldn't come up with a short
name under core.* that made sense (core.defaultRemoteForCheckout?).

See also 70c9ac2f19 ("DWIM "git checkout frotz" to "git checkout -b
frotz origin/frotz"", 2009-10-18) which introduced this DWIM feature
to begin with, and 4e85333197 ("worktree: make add <path> <branch>
dwim", 2017-11-26) which added it to git-worktree.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Ævar Arnfjörð Bjarmason 2018-06-05 14:40:49 +00:00 committed by Junio C Hamano
parent ad8d5104b4
commit 8d7b558bae
7 changed files with 109 additions and 7 deletions

View File

@ -350,7 +350,10 @@ advice.*::
remote tracking branch on more than one remote in remote tracking branch on more than one remote in
situations where an unambiguous argument would have situations where an unambiguous argument would have
otherwise caused a remote-tracking branch to be otherwise caused a remote-tracking branch to be
checked out. checked out. See the `checkout.defaultRemote`
configuration variable for how to set a given remote
to used by default in some situations where this
advice would be printed.
amWorkDir:: amWorkDir::
Advice that shows the location of the patch file when Advice that shows the location of the patch file when
linkgit:git-am[1] fails to apply it. linkgit:git-am[1] fails to apply it.
@ -1105,6 +1108,22 @@ browser.<tool>.path::
browse HTML help (see `-w` option in linkgit:git-help[1]) or a browse HTML help (see `-w` option in linkgit:git-help[1]) or a
working repository in gitweb (see linkgit:git-instaweb[1]). working repository in gitweb (see linkgit:git-instaweb[1]).
checkout.defaultRemote::
When you run 'git checkout <something>' and only have one
remote, it may implicitly fall back on checking out and
tracking e.g. 'origin/<something>'. This stops working as soon
as you have more than one remote with a '<something>'
reference. This setting allows for setting the name of a
preferred remote that should always win when it comes to
disambiguation. The typical use-case is to set this to
`origin`.
+
Currently this is used by linkgit:git-checkout[1] when 'git checkout
<something>' will checkout the '<something>' branch on another remote,
and by linkgit:git-worktree[1] when 'git worktree add' refers to a
remote branch. This setting might be used for other checkout-like
commands or functionality in the future.
clean.requireForce:: clean.requireForce::
A boolean to make git-clean do nothing unless given -f, A boolean to make git-clean do nothing unless given -f,
-i or -n. Defaults to true. -i or -n. Defaults to true.

View File

@ -38,6 +38,15 @@ equivalent to
$ git checkout -b <branch> --track <remote>/<branch> $ git checkout -b <branch> --track <remote>/<branch>
------------ ------------
+ +
If the branch exists in multiple remotes and one of them is named by
the `checkout.defaultRemote` configuration variable, we'll use that
one for the purposes of disambiguation, even if the `<branch>` isn't
unique across all remotes. Set it to
e.g. `checkout.defaultRemote=origin` to always checkout remote
branches from there if `<branch>` is ambiguous but exists on the
'origin' remote. See also `checkout.defaultRemote` in
linkgit:git-config[1].
+
You could omit <branch>, in which case the command degenerates to You could omit <branch>, in which case the command degenerates to
"check out the current branch", which is a glorified no-op with "check out the current branch", which is a glorified no-op with
rather expensive side-effects to show only the tracking information, rather expensive side-effects to show only the tracking information,

View File

@ -60,6 +60,15 @@ with a matching name, treat as equivalent to:
$ git worktree add --track -b <branch> <path> <remote>/<branch> $ git worktree add --track -b <branch> <path> <remote>/<branch>
------------ ------------
+ +
If the branch exists in multiple remotes and one of them is named by
the `checkout.defaultRemote` configuration variable, we'll use that
one for the purposes of disambiguation, even if the `<branch>` isn't
unique across all remotes. Set it to
e.g. `checkout.defaultRemote=origin` to always checkout remote
branches from there if `<branch>` is ambiguous but exists on the
'origin' remote. See also `checkout.defaultRemote` in
linkgit:git-config[1].
+
If `<commit-ish>` is omitted and neither `-b` nor `-B` nor `--detach` used, If `<commit-ish>` is omitted and neither `-b` nor `-B` nor `--detach` used,
then, as a convenience, the new worktree is associated with a branch then, as a convenience, the new worktree is associated with a branch
(call it `<branch>`) named after `$(basename <path>)`. If `<branch>` (call it `<branch>`) named after `$(basename <path>)`. If `<branch>`

View File

@ -912,8 +912,10 @@ static int parse_branchname_arg(int argc, const char **argv,
* (b) If <something> is _not_ a commit, either "--" is present * (b) If <something> is _not_ a commit, either "--" is present
* or <something> is not a path, no -t or -b was given, and * or <something> is not a path, no -t or -b was given, and
* and there is a tracking branch whose name is <something> * and there is a tracking branch whose name is <something>
* in one and only one remote, then this is a short-hand to * in one and only one remote (or if the branch exists on the
* fork local <something> from that remote-tracking branch. * remote named in checkout.defaultRemote), then this is a
* short-hand to fork local <something> from that
* remote-tracking branch.
* *
* (c) Otherwise, if "--" is present, treat it like case (1). * (c) Otherwise, if "--" is present, treat it like case (1).
* *
@ -1277,7 +1279,11 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
"If you meant to check out a remote tracking branch on, e.g. 'origin',\n" "If you meant to check out a remote tracking branch on, e.g. 'origin',\n"
"you can do so by fully qualifying the name with the --track option:\n" "you can do so by fully qualifying the name with the --track option:\n"
"\n" "\n"
" git checkout --track origin/<name>"), " git checkout --track origin/<name>\n"
"\n"
"If you'd like to always have checkouts of an ambiguous <name> prefer\n"
"one remote, e.g. the 'origin' remote, consider setting\n"
"checkout.defaultRemote=origin in your config."),
argv[0], argv[0],
dwim_remotes_matched); dwim_remotes_matched);
return ret; return ret;

View File

@ -2,15 +2,19 @@
#include "remote.h" #include "remote.h"
#include "refspec.h" #include "refspec.h"
#include "checkout.h" #include "checkout.h"
#include "config.h"
struct tracking_name_data { struct tracking_name_data {
/* const */ char *src_ref; /* const */ char *src_ref;
char *dst_ref; char *dst_ref;
struct object_id *dst_oid; struct object_id *dst_oid;
int num_matches; int num_matches;
const char *default_remote;
char *default_dst_ref;
struct object_id *default_dst_oid;
}; };
#define TRACKING_NAME_DATA_INIT { NULL, NULL, NULL, 0 } #define TRACKING_NAME_DATA_INIT { NULL, NULL, NULL, 0, NULL, NULL, NULL }
static int check_tracking_name(struct remote *remote, void *cb_data) static int check_tracking_name(struct remote *remote, void *cb_data)
{ {
@ -24,6 +28,12 @@ static int check_tracking_name(struct remote *remote, void *cb_data)
return 0; return 0;
} }
cb->num_matches++; cb->num_matches++;
if (cb->default_remote && !strcmp(remote->name, cb->default_remote)) {
struct object_id *dst = xmalloc(sizeof(*cb->default_dst_oid));
cb->default_dst_ref = xstrdup(query.dst);
oidcpy(dst, cb->dst_oid);
cb->default_dst_oid = dst;
}
if (cb->dst_ref) { if (cb->dst_ref) {
free(query.dst); free(query.dst);
return 0; return 0;
@ -36,14 +46,26 @@ const char *unique_tracking_name(const char *name, struct object_id *oid,
int *dwim_remotes_matched) int *dwim_remotes_matched)
{ {
struct tracking_name_data cb_data = TRACKING_NAME_DATA_INIT; struct tracking_name_data cb_data = TRACKING_NAME_DATA_INIT;
const char *default_remote = NULL;
if (!git_config_get_string_const("checkout.defaultremote", &default_remote))
cb_data.default_remote = default_remote;
cb_data.src_ref = xstrfmt("refs/heads/%s", name); cb_data.src_ref = xstrfmt("refs/heads/%s", name);
cb_data.dst_oid = oid; cb_data.dst_oid = oid;
for_each_remote(check_tracking_name, &cb_data); for_each_remote(check_tracking_name, &cb_data);
if (dwim_remotes_matched) if (dwim_remotes_matched)
*dwim_remotes_matched = cb_data.num_matches; *dwim_remotes_matched = cb_data.num_matches;
free(cb_data.src_ref); free(cb_data.src_ref);
if (cb_data.num_matches == 1) free((char *)default_remote);
if (cb_data.num_matches == 1) {
free(cb_data.default_dst_ref);
free(cb_data.default_dst_oid);
return cb_data.dst_ref; return cb_data.dst_ref;
}
free(cb_data.dst_ref); free(cb_data.dst_ref);
if (cb_data.default_dst_ref) {
oidcpy(oid, cb_data.default_dst_oid);
free(cb_data.default_dst_oid);
return cb_data.default_dst_ref;
}
return NULL; return NULL;
} }

View File

@ -87,7 +87,23 @@ test_expect_success 'checkout of branch from multiple remotes fails with advice'
checkout foo 2>stderr && checkout foo 2>stderr &&
test_branch master && test_branch master &&
status_uno_is_clean && status_uno_is_clean &&
test_i18ngrep ! "^hint: " stderr test_i18ngrep ! "^hint: " stderr &&
# Make sure the likes of checkout -p do not print this hint
git checkout -p foo 2>stderr &&
test_i18ngrep ! "^hint: " stderr &&
status_uno_is_clean
'
test_expect_success 'checkout of branch from multiple remotes succeeds with checkout.defaultRemote #1' '
git checkout -B master &&
status_uno_is_clean &&
test_might_fail git branch -D foo &&
git -c checkout.defaultRemote=repo_a checkout foo &&
status_uno_is_clean &&
test_branch foo &&
test_cmp_rev remotes/repo_a/foo HEAD &&
test_branch_upstream foo repo_a foo
' '
test_expect_success 'checkout of branch from a single remote succeeds #1' ' test_expect_success 'checkout of branch from a single remote succeeds #1' '

View File

@ -402,6 +402,27 @@ test_expect_success '"add" <path> <branch> dwims' '
) )
' '
test_expect_success '"add" <path> <branch> dwims with checkout.defaultRemote' '
test_when_finished rm -rf repo_upstream repo_dwim foo &&
setup_remote_repo repo_upstream repo_dwim &&
git init repo_dwim &&
(
cd repo_dwim &&
git remote add repo_upstream2 ../repo_upstream &&
git fetch repo_upstream2 &&
test_must_fail git worktree add ../foo foo &&
git -c checkout.defaultRemote=repo_upstream worktree add ../foo foo &&
>status.expect &&
git status -uno --porcelain >status.actual &&
test_cmp status.expect status.actual
) &&
(
cd foo &&
test_branch_upstream foo repo_upstream foo &&
test_cmp_rev refs/remotes/repo_upstream/foo refs/heads/foo
)
'
test_expect_success 'git worktree add does not match remote' ' test_expect_success 'git worktree add does not match remote' '
test_when_finished rm -rf repo_a repo_b foo && test_when_finished rm -rf repo_a repo_b foo &&
setup_remote_repo repo_a repo_b && setup_remote_repo repo_a repo_b &&