Merge branch 'tg/worktree-create-tracking'
The way "git worktree add" determines what branch to create from where and checkout in the new worktree has been updated a bit. * tg/worktree-create-tracking: add worktree.guessRemote config option worktree: add --guess-remote flag to add subcommand worktree: make add <path> <branch> dwim worktree: add --[no-]track option to the add subcommand worktree: add can be created from any commit-ish checkout: factor out functions to new lib file
This commit is contained in:
commit
66d3f19324
@ -3468,3 +3468,13 @@ web.browser::
|
||||
Specify a web browser that may be used by some commands.
|
||||
Currently only linkgit:git-instaweb[1] and linkgit:git-help[1]
|
||||
may use it.
|
||||
|
||||
worktree.guessRemote::
|
||||
With `add`, if no branch argument, and neither of `-b` nor
|
||||
`-B` nor `--detach` are given, the command defaults to
|
||||
creating a new branch from HEAD. If `worktree.guessRemote` is
|
||||
set to true, `worktree add` tries to find a remote-tracking
|
||||
branch whose name uniquely matches the new branch name. If
|
||||
such a branch exists, it is checked out and set as "upstream"
|
||||
for the new branch. If no such match can be found, it falls
|
||||
back to creating a new branch from the current HEAD.
|
||||
|
@ -9,7 +9,7 @@ git-worktree - Manage multiple working trees
|
||||
SYNOPSIS
|
||||
--------
|
||||
[verse]
|
||||
'git worktree add' [-f] [--detach] [--checkout] [--lock] [-b <new-branch>] <path> [<branch>]
|
||||
'git worktree add' [-f] [--detach] [--checkout] [--lock] [-b <new-branch>] <path> [<commit-ish>]
|
||||
'git worktree list' [--porcelain]
|
||||
'git worktree lock' [--reason <string>] <worktree>
|
||||
'git worktree prune' [-n] [-v] [--expire <expire>]
|
||||
@ -45,14 +45,22 @@ specifying `--reason` to explain why the working tree is locked.
|
||||
|
||||
COMMANDS
|
||||
--------
|
||||
add <path> [<branch>]::
|
||||
add <path> [<commit-ish>]::
|
||||
|
||||
Create `<path>` and checkout `<branch>` into it. The new working directory
|
||||
Create `<path>` and checkout `<commit-ish>` into it. The new working directory
|
||||
is linked to the current repository, sharing everything except working
|
||||
directory specific files such as HEAD, index, etc. `-` may also be
|
||||
specified as `<branch>`; it is synonymous with `@{-1}`.
|
||||
specified as `<commit-ish>`; it is synonymous with `@{-1}`.
|
||||
+
|
||||
If `<branch>` is omitted and neither `-b` nor `-B` nor `--detach` used,
|
||||
If <commit-ish> is a branch name (call it `<branch>` and is not found,
|
||||
and neither `-b` nor `-B` nor `--detach` are used, but there does
|
||||
exist a tracking branch in exactly one remote (call it `<remote>`)
|
||||
with a matching name, treat as equivalent to
|
||||
------------
|
||||
$ git worktree add --track -b <branch> <path> <remote>/<branch>
|
||||
------------
|
||||
+
|
||||
If `<commit-ish>` is omitted and neither `-b` nor `-B` nor `--detach` used,
|
||||
then, as a convenience, a new branch based at HEAD is created automatically,
|
||||
as if `-b $(basename <path>)` was specified.
|
||||
|
||||
@ -84,29 +92,45 @@ OPTIONS
|
||||
|
||||
-f::
|
||||
--force::
|
||||
By default, `add` refuses to create a new working tree when `<branch>`
|
||||
By default, `add` refuses to create a new working tree when `<commit-ish>` is a branch name and
|
||||
is already checked out by another working tree. This option overrides
|
||||
that safeguard.
|
||||
|
||||
-b <new-branch>::
|
||||
-B <new-branch>::
|
||||
With `add`, create a new branch named `<new-branch>` starting at
|
||||
`<branch>`, and check out `<new-branch>` into the new working tree.
|
||||
If `<branch>` is omitted, it defaults to HEAD.
|
||||
`<commit-ish>`, and check out `<new-branch>` into the new working tree.
|
||||
If `<commit-ish>` is omitted, it defaults to HEAD.
|
||||
By default, `-b` refuses to create a new branch if it already
|
||||
exists. `-B` overrides this safeguard, resetting `<new-branch>` to
|
||||
`<branch>`.
|
||||
`<commit-ish>`.
|
||||
|
||||
--detach::
|
||||
With `add`, detach HEAD in the new working tree. See "DETACHED HEAD"
|
||||
in linkgit:git-checkout[1].
|
||||
|
||||
--[no-]checkout::
|
||||
By default, `add` checks out `<branch>`, however, `--no-checkout` can
|
||||
By default, `add` checks out `<commit-ish>`, however, `--no-checkout` can
|
||||
be used to suppress checkout in order to make customizations,
|
||||
such as configuring sparse-checkout. See "Sparse checkout"
|
||||
in linkgit:git-read-tree[1].
|
||||
|
||||
--[no-]guess-remote::
|
||||
With `worktree add <path>`, without `<commit-ish>`, instead
|
||||
of creating a new branch from HEAD, if there exists a tracking
|
||||
branch in exactly one remote matching the basename of `<path>,
|
||||
base the new branch on the remote-tracking branch, and mark
|
||||
the remote-tracking branch as "upstream" from the new branch.
|
||||
+
|
||||
This can also be set up as the default behaviour by using the
|
||||
`worktree.guessRemote` config option.
|
||||
|
||||
--[no-]track::
|
||||
When creating a new branch, if `<commit-ish>` is a branch,
|
||||
mark it as "upstream" from the new branch. This is the
|
||||
default if `<commit-ish>` is a remote-tracking branch. See
|
||||
"--track" in linkgit:git-branch[1] for details.
|
||||
|
||||
--lock::
|
||||
Keep the working tree locked after creation. This is the
|
||||
equivalent of `git worktree lock` after `git worktree add`,
|
||||
|
1
Makefile
1
Makefile
@ -759,6 +759,7 @@ LIB_OBJS += branch.o
|
||||
LIB_OBJS += bulk-checkin.o
|
||||
LIB_OBJS += bundle.o
|
||||
LIB_OBJS += cache-tree.o
|
||||
LIB_OBJS += checkout.o
|
||||
LIB_OBJS += color.o
|
||||
LIB_OBJS += column.o
|
||||
LIB_OBJS += combine-diff.o
|
||||
|
@ -1,5 +1,6 @@
|
||||
#include "builtin.h"
|
||||
#include "config.h"
|
||||
#include "checkout.h"
|
||||
#include "lockfile.h"
|
||||
#include "parse-options.h"
|
||||
#include "refs.h"
|
||||
@ -872,46 +873,6 @@ static int git_checkout_config(const char *var, const char *value, void *cb)
|
||||
return git_xmerge_config(var, value, NULL);
|
||||
}
|
||||
|
||||
struct tracking_name_data {
|
||||
/* const */ char *src_ref;
|
||||
char *dst_ref;
|
||||
struct object_id *dst_oid;
|
||||
int unique;
|
||||
};
|
||||
|
||||
static int check_tracking_name(struct remote *remote, void *cb_data)
|
||||
{
|
||||
struct tracking_name_data *cb = cb_data;
|
||||
struct refspec query;
|
||||
memset(&query, 0, sizeof(struct refspec));
|
||||
query.src = cb->src_ref;
|
||||
if (remote_find_tracking(remote, &query) ||
|
||||
get_oid(query.dst, cb->dst_oid)) {
|
||||
free(query.dst);
|
||||
return 0;
|
||||
}
|
||||
if (cb->dst_ref) {
|
||||
free(query.dst);
|
||||
cb->unique = 0;
|
||||
return 0;
|
||||
}
|
||||
cb->dst_ref = query.dst;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const char *unique_tracking_name(const char *name, struct object_id *oid)
|
||||
{
|
||||
struct tracking_name_data cb_data = { NULL, NULL, NULL, 1 };
|
||||
cb_data.src_ref = xstrfmt("refs/heads/%s", name);
|
||||
cb_data.dst_oid = oid;
|
||||
for_each_remote(check_tracking_name, &cb_data);
|
||||
free(cb_data.src_ref);
|
||||
if (cb_data.unique)
|
||||
return cb_data.dst_ref;
|
||||
free(cb_data.dst_ref);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int parse_branchname_arg(int argc, const char **argv,
|
||||
int dwim_new_local_branch_ok,
|
||||
struct branch_info *new,
|
||||
|
@ -1,4 +1,5 @@
|
||||
#include "cache.h"
|
||||
#include "checkout.h"
|
||||
#include "config.h"
|
||||
#include "builtin.h"
|
||||
#include "dir.h"
|
||||
@ -32,8 +33,19 @@ struct add_opts {
|
||||
|
||||
static int show_only;
|
||||
static int verbose;
|
||||
static int guess_remote;
|
||||
static timestamp_t expire;
|
||||
|
||||
static int git_worktree_config(const char *var, const char *value, void *cb)
|
||||
{
|
||||
if (!strcmp(var, "worktree.guessremote")) {
|
||||
guess_remote = git_config_bool(var, value);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return git_default_config(var, value, cb);
|
||||
}
|
||||
|
||||
static int prune_worktree(const char *id, struct strbuf *reason)
|
||||
{
|
||||
struct stat st;
|
||||
@ -341,6 +353,7 @@ static int add(int ac, const char **av, const char *prefix)
|
||||
const char *new_branch_force = NULL;
|
||||
char *path;
|
||||
const char *branch;
|
||||
const char *opt_track = NULL;
|
||||
struct option options[] = {
|
||||
OPT__FORCE(&opts.force, N_("checkout <branch> even if already checked out in other worktree")),
|
||||
OPT_STRING('b', NULL, &opts.new_branch, N_("branch"),
|
||||
@ -350,6 +363,11 @@ static int add(int ac, const char **av, const char *prefix)
|
||||
OPT_BOOL(0, "detach", &opts.detach, N_("detach HEAD at named commit")),
|
||||
OPT_BOOL(0, "checkout", &opts.checkout, N_("populate the new working tree")),
|
||||
OPT_BOOL(0, "lock", &opts.keep_locked, N_("keep the new working tree locked")),
|
||||
OPT_PASSTHRU(0, "track", &opt_track, NULL,
|
||||
N_("set up tracking mode (see git-branch(1))"),
|
||||
PARSE_OPT_NOARG | PARSE_OPT_OPTARG),
|
||||
OPT_BOOL(0, "guess-remote", &guess_remote,
|
||||
N_("try to match the new branch name with a remote-tracking branch")),
|
||||
OPT_END()
|
||||
};
|
||||
|
||||
@ -384,6 +402,28 @@ static int add(int ac, const char **av, const char *prefix)
|
||||
int n;
|
||||
const char *s = worktree_basename(path, &n);
|
||||
opts.new_branch = xstrndup(s, n);
|
||||
if (guess_remote) {
|
||||
struct object_id oid;
|
||||
const char *remote =
|
||||
unique_tracking_name(opts.new_branch, &oid);
|
||||
if (remote)
|
||||
branch = remote;
|
||||
}
|
||||
}
|
||||
|
||||
if (ac == 2 && !opts.new_branch && !opts.detach) {
|
||||
struct object_id oid;
|
||||
struct commit *commit;
|
||||
const char *remote;
|
||||
|
||||
commit = lookup_commit_reference_by_name(branch);
|
||||
if (!commit) {
|
||||
remote = unique_tracking_name(branch, &oid);
|
||||
if (remote) {
|
||||
opts.new_branch = branch;
|
||||
branch = remote;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.new_branch) {
|
||||
@ -394,9 +434,13 @@ static int add(int ac, const char **av, const char *prefix)
|
||||
argv_array_push(&cp.args, "--force");
|
||||
argv_array_push(&cp.args, opts.new_branch);
|
||||
argv_array_push(&cp.args, branch);
|
||||
if (opt_track)
|
||||
argv_array_push(&cp.args, opt_track);
|
||||
if (run_command(&cp))
|
||||
return -1;
|
||||
branch = opts.new_branch;
|
||||
} else if (opt_track) {
|
||||
die(_("--[no-]track can only be used if a new branch is created"));
|
||||
}
|
||||
|
||||
UNLEAK(path);
|
||||
@ -557,7 +601,7 @@ int cmd_worktree(int ac, const char **av, const char *prefix)
|
||||
OPT_END()
|
||||
};
|
||||
|
||||
git_config(git_default_config, NULL);
|
||||
git_config(git_worktree_config, NULL);
|
||||
|
||||
if (ac < 2)
|
||||
usage_with_options(worktree_usage, options);
|
||||
|
43
checkout.c
Normal file
43
checkout.c
Normal file
@ -0,0 +1,43 @@
|
||||
#include "cache.h"
|
||||
#include "remote.h"
|
||||
#include "checkout.h"
|
||||
|
||||
struct tracking_name_data {
|
||||
/* const */ char *src_ref;
|
||||
char *dst_ref;
|
||||
struct object_id *dst_oid;
|
||||
int unique;
|
||||
};
|
||||
|
||||
static int check_tracking_name(struct remote *remote, void *cb_data)
|
||||
{
|
||||
struct tracking_name_data *cb = cb_data;
|
||||
struct refspec query;
|
||||
memset(&query, 0, sizeof(struct refspec));
|
||||
query.src = cb->src_ref;
|
||||
if (remote_find_tracking(remote, &query) ||
|
||||
get_oid(query.dst, cb->dst_oid)) {
|
||||
free(query.dst);
|
||||
return 0;
|
||||
}
|
||||
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)
|
||||
{
|
||||
struct tracking_name_data cb_data = { NULL, NULL, NULL, 1 };
|
||||
cb_data.src_ref = xstrfmt("refs/heads/%s", name);
|
||||
cb_data.dst_oid = oid;
|
||||
for_each_remote(check_tracking_name, &cb_data);
|
||||
free(cb_data.src_ref);
|
||||
if (cb_data.unique)
|
||||
return cb_data.dst_ref;
|
||||
free(cb_data.dst_ref);
|
||||
return NULL;
|
||||
}
|
13
checkout.h
Normal file
13
checkout.h
Normal file
@ -0,0 +1,13 @@
|
||||
#ifndef CHECKOUT_H
|
||||
#define CHECKOUT_H
|
||||
|
||||
#include "cache.h"
|
||||
|
||||
/*
|
||||
* Check if the branch name uniquely matches a branch name on a remote
|
||||
* 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);
|
||||
|
||||
#endif /* CHECKOUT_H */
|
@ -313,5 +313,135 @@ test_expect_success 'checkout a branch under bisect' '
|
||||
test_expect_success 'rename a branch under bisect not allowed' '
|
||||
test_must_fail git branch -M under-bisect bisect-with-new-name
|
||||
'
|
||||
# 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 '--track sets up tracking' '
|
||||
test_when_finished rm -rf track &&
|
||||
git worktree add --track -b track track master &&
|
||||
test_branch_upstream track . master
|
||||
'
|
||||
|
||||
# setup remote repository $1 and repository $2 with $1 set up as
|
||||
# remote. The remote has two branches, master and foo.
|
||||
setup_remote_repo () {
|
||||
git init $1 &&
|
||||
(
|
||||
cd $1 &&
|
||||
test_commit $1_master &&
|
||||
git checkout -b foo &&
|
||||
test_commit upstream_foo
|
||||
) &&
|
||||
git init $2 &&
|
||||
(
|
||||
cd $2 &&
|
||||
test_commit $2_master &&
|
||||
git remote add $1 ../$1 &&
|
||||
git config remote.$1.fetch \
|
||||
"refs/heads/*:refs/remotes/$1/*" &&
|
||||
git fetch --all
|
||||
)
|
||||
}
|
||||
|
||||
test_expect_success '--no-track avoids setting up tracking' '
|
||||
test_when_finished rm -rf repo_upstream repo_local foo &&
|
||||
setup_remote_repo repo_upstream repo_local &&
|
||||
(
|
||||
cd repo_local &&
|
||||
git worktree add --no-track -b foo ../foo repo_upstream/foo
|
||||
) &&
|
||||
(
|
||||
cd foo &&
|
||||
test_must_fail git config "branch.foo.remote" &&
|
||||
test_must_fail git config "branch.foo.merge" &&
|
||||
test_cmp_rev refs/remotes/repo_upstream/foo refs/heads/foo
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success '"add" <path> <non-existent-branch> fails' '
|
||||
test_must_fail git worktree add foo non-existent
|
||||
'
|
||||
|
||||
test_expect_success '"add" <path> <branch> dwims' '
|
||||
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 worktree add ../foo foo
|
||||
) &&
|
||||
(
|
||||
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 &&
|
||||
(
|
||||
cd repo_b &&
|
||||
git worktree add ../foo
|
||||
) &&
|
||||
(
|
||||
cd foo &&
|
||||
test_must_fail git config "branch.foo.remote" &&
|
||||
test_must_fail git config "branch.foo.merge" &&
|
||||
! test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'git worktree add --guess-remote sets up tracking' '
|
||||
test_when_finished rm -rf repo_a repo_b foo &&
|
||||
setup_remote_repo repo_a repo_b &&
|
||||
(
|
||||
cd repo_b &&
|
||||
git worktree add --guess-remote ../foo
|
||||
) &&
|
||||
(
|
||||
cd foo &&
|
||||
test_branch_upstream foo repo_a foo &&
|
||||
test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'git worktree add with worktree.guessRemote sets up tracking' '
|
||||
test_when_finished rm -rf repo_a repo_b foo &&
|
||||
setup_remote_repo repo_a repo_b &&
|
||||
(
|
||||
cd repo_b &&
|
||||
git config worktree.guessRemote true &&
|
||||
git worktree add ../foo
|
||||
) &&
|
||||
(
|
||||
cd foo &&
|
||||
test_branch_upstream foo repo_a foo &&
|
||||
test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'git worktree --no-guess-remote option overrides config' '
|
||||
test_when_finished rm -rf repo_a repo_b foo &&
|
||||
setup_remote_repo repo_a repo_b &&
|
||||
(
|
||||
cd repo_b &&
|
||||
git config worktree.guessRemote true &&
|
||||
git worktree add --no-guess-remote ../foo
|
||||
) &&
|
||||
(
|
||||
cd foo &&
|
||||
test_must_fail git config "branch.foo.remote" &&
|
||||
test_must_fail git config "branch.foo.merge" &&
|
||||
! test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo
|
||||
)
|
||||
'
|
||||
|
||||
test_done
|
||||
|
Loading…
Reference in New Issue
Block a user