Browse Source

Merge branch 'ab/checkout-default-remote'

"git checkout" and "git worktree add" learned to honor
checkout.defaultRemote when auto-vivifying a local branch out of a
remote tracking branch in a repository with multiple remotes that
have tracking branches that share the same names.

* ab/checkout-default-remote:
  checkout & worktree: introduce checkout.defaultRemote
  checkout: add advice for ambiguous "checkout <branch>"
  builtin/checkout.c: use "ret" variable for return
  checkout: pass the "num_matches" up to callers
  checkout.c: change "unique" member to "num_matches"
  checkout.c: introduce an *_INIT macro
  checkout.h: wrap the arguments to unique_tracking_name()
  checkout tests: index should be clean after dwim checkout
maint
Junio C Hamano 6 years ago
parent
commit
50858edd1a
  1. 26
      Documentation/config.txt
  2. 9
      Documentation/git-checkout.txt
  3. 9
      Documentation/git-worktree.txt
  4. 2
      advice.c
  5. 1
      advice.h
  6. 41
      builtin/checkout.c
  7. 4
      builtin/worktree.c
  8. 37
      checkout.c
  9. 4
      checkout.h
  10. 59
      t/t2024-checkout-dwim.sh
  11. 21
      t/t2025-worktree-add.sh

26
Documentation/config.txt

@ -344,6 +344,16 @@ advice.*:: @@ -344,6 +344,16 @@ advice.*::
Advice shown when you used linkgit:git-checkout[1] to
move to the detach HEAD state, to instruct how to create
a local branch after the fact.
checkoutAmbiguousRemoteBranchName::
Advice shown when the argument to
linkgit:git-checkout[1] ambiguously resolves to a
remote tracking branch on more than one remote in
situations where an unambiguous argument would have
otherwise caused a remote-tracking branch to be
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::
Advice that shows the location of the patch file when
linkgit:git-am[1] fails to apply it.
@ -1104,6 +1114,22 @@ browser.<tool>.path:: @@ -1104,6 +1114,22 @@ browser.<tool>.path::
browse HTML help (see `-w` option in linkgit:git-help[1]) or a
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::
A boolean to make git-clean do nothing unless given -f,
-i or -n. Defaults to true.

9
Documentation/git-checkout.txt

@ -38,6 +38,15 @@ equivalent to @@ -38,6 +38,15 @@ equivalent to
$ 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
"check out the current branch", which is a glorified no-op with
rather expensive side-effects to show only the tracking information,

9
Documentation/git-worktree.txt

@ -60,6 +60,15 @@ with a matching name, treat as equivalent to: @@ -60,6 +60,15 @@ with a matching name, treat as equivalent to:
$ 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,
then, as a convenience, the new worktree is associated with a branch
(call it `<branch>`) named after `$(basename <path>)`. If `<branch>`

2
advice.c

@ -23,6 +23,7 @@ int advice_add_embedded_repo = 1; @@ -23,6 +23,7 @@ int advice_add_embedded_repo = 1;
int advice_ignored_hook = 1;
int advice_waiting_for_editor = 1;
int advice_graft_file_deprecated = 1;
int advice_checkout_ambiguous_remote_branch_name = 1;

static int advice_use_color = -1;
static char advice_colors[][COLOR_MAXLEN] = {
@ -75,6 +76,7 @@ static struct { @@ -75,6 +76,7 @@ static struct {
{ "ignoredHook", &advice_ignored_hook },
{ "waitingForEditor", &advice_waiting_for_editor },
{ "graftFileDeprecated", &advice_graft_file_deprecated },
{ "checkoutAmbiguousRemoteBranchName", &advice_checkout_ambiguous_remote_branch_name },

/* make this an alias for backward compatibility */
{ "pushNonFastForward", &advice_push_update_rejected }

1
advice.h

@ -23,6 +23,7 @@ extern int advice_add_embedded_repo; @@ -23,6 +23,7 @@ extern int advice_add_embedded_repo;
extern int advice_ignored_hook;
extern int advice_waiting_for_editor;
extern int advice_graft_file_deprecated;
extern int advice_checkout_ambiguous_remote_branch_name;

int git_default_advice_config(const char *var, const char *value);
__attribute__((format (printf, 1, 2)))

41
builtin/checkout.c

@ -23,6 +23,7 @@ @@ -23,6 +23,7 @@
#include "resolve-undo.h"
#include "submodule-config.h"
#include "submodule.h"
#include "advice.h"

static const char * const checkout_usage[] = {
N_("git checkout [<options>] <branch>"),
@ -879,7 +880,8 @@ static int parse_branchname_arg(int argc, const char **argv, @@ -879,7 +880,8 @@ static int parse_branchname_arg(int argc, const char **argv,
int dwim_new_local_branch_ok,
struct branch_info *new_branch_info,
struct checkout_opts *opts,
struct object_id *rev)
struct object_id *rev,
int *dwim_remotes_matched)
{
struct tree **source_tree = &opts->source_tree;
const char **new_branch = &opts->new_branch;
@ -911,8 +913,10 @@ static int parse_branchname_arg(int argc, const char **argv, @@ -911,8 +913,10 @@ static int parse_branchname_arg(int argc, const char **argv,
* (b) If <something> is _not_ a commit, either "--" is present
* or <something> is not a path, no -t or -b was given, and
* and there is a tracking branch whose name is <something>
* in one and only one remote, then this is a short-hand to
* fork local <something> from that remote-tracking branch.
* in one and only one remote (or if the branch exists on the
* 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).
*
@ -973,7 +977,8 @@ static int parse_branchname_arg(int argc, const char **argv, @@ -973,7 +977,8 @@ static int parse_branchname_arg(int argc, const char **argv,
recover_with_dwim = 0;

if (recover_with_dwim) {
const char *remote = unique_tracking_name(arg, rev);
const char *remote = unique_tracking_name(arg, rev,
dwim_remotes_matched);
if (remote) {
*new_branch = arg;
arg = remote;
@ -1110,6 +1115,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) @@ -1110,6 +1115,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
struct branch_info new_branch_info;
char *conflict_style = NULL;
int dwim_new_local_branch = 1;
int dwim_remotes_matched = 0;
struct option options[] = {
OPT__QUIET(&opts.quiet, N_("suppress progress reporting")),
OPT_STRING('b', NULL, &opts.new_branch, N_("branch"),
@ -1222,7 +1228,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) @@ -1222,7 +1228,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
opts.track == BRANCH_TRACK_UNSPECIFIED &&
!opts.new_branch;
int n = parse_branchname_arg(argc, argv, dwim_ok,
&new_branch_info, &opts, &rev);
&new_branch_info, &opts, &rev,
&dwim_remotes_matched);
argv += n;
argc -= n;
}
@ -1264,8 +1271,26 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) @@ -1264,8 +1271,26 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
}

UNLEAK(opts);
if (opts.patch_mode || opts.pathspec.nr)
return checkout_paths(&opts, new_branch_info.name);
else
if (opts.patch_mode || opts.pathspec.nr) {
int ret = checkout_paths(&opts, new_branch_info.name);
if (ret && dwim_remotes_matched > 1 &&
advice_checkout_ambiguous_remote_branch_name)
advise(_("'%s' matched more than one remote tracking branch.\n"
"We found %d remotes with a reference that matched. So we fell back\n"
"on trying to resolve the argument as a path, but failed there too!\n"
"\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"
"\n"
" 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],
dwim_remotes_matched);
return ret;
} else {
return checkout_branch(&opts, &new_branch_info);
}
}

4
builtin/worktree.c

@ -412,7 +412,7 @@ static const char *dwim_branch(const char *path, const char **new_branch) @@ -412,7 +412,7 @@ static const char *dwim_branch(const char *path, const char **new_branch)
if (guess_remote) {
struct object_id oid;
const char *remote =
unique_tracking_name(*new_branch, &oid);
unique_tracking_name(*new_branch, &oid, NULL);
return remote;
}
return NULL;
@ -484,7 +484,7 @@ static int add(int ac, const char **av, const char *prefix) @@ -484,7 +484,7 @@ static int add(int ac, const char **av, const char *prefix)

commit = lookup_commit_reference_by_name(branch);
if (!commit) {
remote = unique_tracking_name(branch, &oid);
remote = unique_tracking_name(branch, &oid, NULL);
if (remote) {
new_branch = branch;
branch = remote;

37
checkout.c

@ -2,14 +2,20 @@ @@ -2,14 +2,20 @@
#include "remote.h"
#include "refspec.h"
#include "checkout.h"
#include "config.h"

struct tracking_name_data {
/* const */ char *src_ref;
char *dst_ref;
struct object_id *dst_oid;
int unique;
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, NULL, NULL, NULL }

static int check_tracking_name(struct remote *remote, void *cb_data)
{
struct tracking_name_data *cb = cb_data;
@ -21,24 +27,45 @@ static int check_tracking_name(struct remote *remote, void *cb_data) @@ -21,24 +27,45 @@ static int check_tracking_name(struct remote *remote, void *cb_data)
free(query.dst);
return 0;
}
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) {
free(query.dst);
cb->unique = 0;
return 0;
}
cb->dst_ref = query.dst;
return 0;
}

const char *unique_tracking_name(const char *name, struct object_id *oid)
const char *unique_tracking_name(const char *name, struct object_id *oid,
int *dwim_remotes_matched)
{
struct tracking_name_data cb_data = { NULL, NULL, NULL, 1 };
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.dst_oid = oid;
for_each_remote(check_tracking_name, &cb_data);
if (dwim_remotes_matched)
*dwim_remotes_matched = cb_data.num_matches;
free(cb_data.src_ref);
if (cb_data.unique)
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;
}
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;
}

4
checkout.h

@ -8,6 +8,8 @@ @@ -8,6 +8,8 @@
* tracking branch. Return the name of the remote if such a branch
* exists, NULL otherwise.
*/
extern const char *unique_tracking_name(const char *name, struct object_id *oid);
extern const char *unique_tracking_name(const char *name,
struct object_id *oid,
int *dwim_remotes_matched);

#endif /* CHECKOUT_H */

59
t/t2024-checkout-dwim.sh

@ -23,6 +23,12 @@ test_branch_upstream () { @@ -23,6 +23,12 @@ test_branch_upstream () {
test_cmp expect.upstream actual.upstream
}

status_uno_is_clean () {
>status.expect &&
git status -uno --porcelain >status.actual &&
test_cmp status.expect status.actual
}

test_expect_success 'setup' '
test_commit my_master &&
git init repo_a &&
@ -55,6 +61,7 @@ test_expect_success 'checkout of non-existing branch fails' ' @@ -55,6 +61,7 @@ test_expect_success 'checkout of non-existing branch fails' '
test_might_fail git branch -D xyzzy &&

test_must_fail git checkout xyzzy &&
status_uno_is_clean &&
test_must_fail git rev-parse --verify refs/heads/xyzzy &&
test_branch master
'
@ -64,15 +71,47 @@ test_expect_success 'checkout of branch from multiple remotes fails #1' ' @@ -64,15 +71,47 @@ test_expect_success 'checkout of branch from multiple remotes fails #1' '
test_might_fail git branch -D foo &&

test_must_fail git checkout foo &&
status_uno_is_clean &&
test_must_fail git rev-parse --verify refs/heads/foo &&
test_branch master
'

test_expect_success 'checkout of branch from multiple remotes fails with advice' '
git checkout -B master &&
test_might_fail git branch -D foo &&
test_must_fail git checkout foo 2>stderr &&
test_branch master &&
status_uno_is_clean &&
test_i18ngrep "^hint: " stderr &&
test_must_fail git -c advice.checkoutAmbiguousRemoteBranchName=false \
checkout foo 2>stderr &&
test_branch master &&
status_uno_is_clean &&
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' '
git checkout -B master &&
test_might_fail git branch -D bar &&

git checkout bar &&
status_uno_is_clean &&
test_branch bar &&
test_cmp_rev remotes/repo_a/bar HEAD &&
test_branch_upstream bar repo_a bar
@ -83,6 +122,7 @@ test_expect_success 'checkout of branch from a single remote succeeds #2' ' @@ -83,6 +122,7 @@ test_expect_success 'checkout of branch from a single remote succeeds #2' '
test_might_fail git branch -D baz &&

git checkout baz &&
status_uno_is_clean &&
test_branch baz &&
test_cmp_rev remotes/other_b/baz HEAD &&
test_branch_upstream baz repo_b baz
@ -90,6 +130,7 @@ test_expect_success 'checkout of branch from a single remote succeeds #2' ' @@ -90,6 +130,7 @@ test_expect_success 'checkout of branch from a single remote succeeds #2' '

test_expect_success '--no-guess suppresses branch auto-vivification' '
git checkout -B master &&
status_uno_is_clean &&
test_might_fail git branch -D bar &&

test_must_fail git checkout --no-guess bar &&
@ -99,6 +140,7 @@ test_expect_success '--no-guess suppresses branch auto-vivification' ' @@ -99,6 +140,7 @@ test_expect_success '--no-guess suppresses branch auto-vivification' '

test_expect_success 'setup more remotes with unconventional refspecs' '
git checkout -B master &&
status_uno_is_clean &&
git init repo_c &&
(
cd repo_c &&
@ -128,27 +170,33 @@ test_expect_success 'setup more remotes with unconventional refspecs' ' @@ -128,27 +170,33 @@ test_expect_success 'setup more remotes with unconventional refspecs' '

test_expect_success 'checkout of branch from multiple remotes fails #2' '
git checkout -B master &&
status_uno_is_clean &&
test_might_fail git branch -D bar &&

test_must_fail git checkout bar &&
status_uno_is_clean &&
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 &&
status_uno_is_clean &&
test_might_fail git branch -D baz &&

test_must_fail git checkout baz &&
status_uno_is_clean &&
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 &&
status_uno_is_clean &&
test_might_fail git branch -D spam &&

git checkout spam &&
status_uno_is_clean &&
test_branch spam &&
test_cmp_rev refs/remotes/extra_dir/repo_c/extra_dir/spam HEAD &&
test_branch_upstream spam repo_c spam
@ -156,9 +204,11 @@ test_expect_success 'checkout of branch from a single remote succeeds #3' ' @@ -156,9 +204,11 @@ test_expect_success 'checkout of branch from a single remote succeeds #3' '

test_expect_success 'checkout of branch from a single remote succeeds #4' '
git checkout -B master &&
status_uno_is_clean &&
test_might_fail git branch -D eggs &&

git checkout eggs &&
status_uno_is_clean &&
test_branch eggs &&
test_cmp_rev refs/repo_d/eggs HEAD &&
test_branch_upstream eggs repo_d eggs
@ -166,32 +216,38 @@ test_expect_success 'checkout of branch from a single remote succeeds #4' ' @@ -166,32 +216,38 @@ test_expect_success 'checkout of branch from a single remote succeeds #4' '

test_expect_success 'checkout of branch with a file having the same name fails' '
git checkout -B master &&
status_uno_is_clean &&
test_might_fail git branch -D spam &&

>spam &&
test_must_fail git checkout spam &&
status_uno_is_clean &&
test_must_fail git rev-parse --verify refs/heads/spam &&
test_branch master
'

test_expect_success 'checkout of branch with a file in subdir having the same name fails' '
git checkout -B master &&
status_uno_is_clean &&
test_might_fail git branch -D spam &&

>spam &&
mkdir sub &&
mv spam sub/spam &&
test_must_fail git -C sub checkout spam &&
status_uno_is_clean &&
test_must_fail git rev-parse --verify refs/heads/spam &&
test_branch master
'

test_expect_success 'checkout <branch> -- succeeds, even if a file with the same name exists' '
git checkout -B master &&
status_uno_is_clean &&
test_might_fail git branch -D spam &&

>spam &&
git checkout spam -- &&
status_uno_is_clean &&
test_branch spam &&
test_cmp_rev refs/remotes/extra_dir/repo_c/extra_dir/spam HEAD &&
test_branch_upstream spam repo_c spam
@ -200,6 +256,7 @@ test_expect_success 'checkout <branch> -- succeeds, even if a file with the same @@ -200,6 +256,7 @@ test_expect_success 'checkout <branch> -- succeeds, even if a file with the same
test_expect_success 'loosely defined local base branch is reported correctly' '

git checkout master &&
status_uno_is_clean &&
git branch strict &&
git branch loose &&
git commit --allow-empty -m "a bit more" &&
@ -210,7 +267,9 @@ test_expect_success 'loosely defined local base branch is reported correctly' ' @@ -210,7 +267,9 @@ test_expect_success 'loosely defined local base branch is reported correctly' '
test_config branch.loose.merge master &&

git checkout strict | sed -e "s/strict/BRANCHNAME/g" >expect &&
status_uno_is_clean &&
git checkout loose | sed -e "s/loose/BRANCHNAME/g" >actual &&
status_uno_is_clean &&

test_cmp expect actual
'

21
t/t2025-worktree-add.sh

@ -402,6 +402,27 @@ test_expect_success '"add" <path> <branch> dwims' ' @@ -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_when_finished rm -rf repo_a repo_b foo &&
setup_remote_repo repo_a repo_b &&

Loading…
Cancel
Save