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
maint
Junio C Hamano 2017-12-19 11:33:57 -08:00
commit 66d3f19324
8 changed files with 277 additions and 51 deletions

View File

@ -3468,3 +3468,13 @@ web.browser::
Specify a web browser that may be used by some commands. Specify a web browser that may be used by some commands.
Currently only linkgit:git-instaweb[1] and linkgit:git-help[1] Currently only linkgit:git-instaweb[1] and linkgit:git-help[1]
may use it. 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.

View File

@ -9,7 +9,7 @@ git-worktree - Manage multiple working trees
SYNOPSIS SYNOPSIS
-------- --------
[verse] [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 list' [--porcelain]
'git worktree lock' [--reason <string>] <worktree> 'git worktree lock' [--reason <string>] <worktree>
'git worktree prune' [-n] [-v] [--expire <expire>] 'git worktree prune' [-n] [-v] [--expire <expire>]
@ -45,14 +45,22 @@ specifying `--reason` to explain why the working tree is locked.


COMMANDS 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 is linked to the current repository, sharing everything except working
directory specific files such as HEAD, index, etc. `-` may also be 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, then, as a convenience, a new branch based at HEAD is created automatically,
as if `-b $(basename <path>)` was specified. as if `-b $(basename <path>)` was specified.


@ -84,29 +92,45 @@ OPTIONS


-f:: -f::
--force:: --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 is already checked out by another working tree. This option overrides
that safeguard. that safeguard.


-b <new-branch>:: -b <new-branch>::
-B <new-branch>:: -B <new-branch>::
With `add`, create a new branch named `<new-branch>` starting at With `add`, create a new branch named `<new-branch>` starting at
`<branch>`, and check out `<new-branch>` into the new working tree. `<commit-ish>`, and check out `<new-branch>` into the new working tree.
If `<branch>` is omitted, it defaults to HEAD. If `<commit-ish>` is omitted, it defaults to HEAD.
By default, `-b` refuses to create a new branch if it already By default, `-b` refuses to create a new branch if it already
exists. `-B` overrides this safeguard, resetting `<new-branch>` to exists. `-B` overrides this safeguard, resetting `<new-branch>` to
`<branch>`. `<commit-ish>`.


--detach:: --detach::
With `add`, detach HEAD in the new working tree. See "DETACHED HEAD" With `add`, detach HEAD in the new working tree. See "DETACHED HEAD"
in linkgit:git-checkout[1]. in linkgit:git-checkout[1].


--[no-]checkout:: --[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, be used to suppress checkout in order to make customizations,
such as configuring sparse-checkout. See "Sparse checkout" such as configuring sparse-checkout. See "Sparse checkout"
in linkgit:git-read-tree[1]. 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:: --lock::
Keep the working tree locked after creation. This is the Keep the working tree locked after creation. This is the
equivalent of `git worktree lock` after `git worktree add`, equivalent of `git worktree lock` after `git worktree add`,

View File

@ -759,6 +759,7 @@ LIB_OBJS += branch.o
LIB_OBJS += bulk-checkin.o LIB_OBJS += bulk-checkin.o
LIB_OBJS += bundle.o LIB_OBJS += bundle.o
LIB_OBJS += cache-tree.o LIB_OBJS += cache-tree.o
LIB_OBJS += checkout.o
LIB_OBJS += color.o LIB_OBJS += color.o
LIB_OBJS += column.o LIB_OBJS += column.o
LIB_OBJS += combine-diff.o LIB_OBJS += combine-diff.o

View File

@ -1,5 +1,6 @@
#include "builtin.h" #include "builtin.h"
#include "config.h" #include "config.h"
#include "checkout.h"
#include "lockfile.h" #include "lockfile.h"
#include "parse-options.h" #include "parse-options.h"
#include "refs.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); 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, static int parse_branchname_arg(int argc, const char **argv,
int dwim_new_local_branch_ok, int dwim_new_local_branch_ok,
struct branch_info *new, struct branch_info *new,

View File

@ -1,4 +1,5 @@
#include "cache.h" #include "cache.h"
#include "checkout.h"
#include "config.h" #include "config.h"
#include "builtin.h" #include "builtin.h"
#include "dir.h" #include "dir.h"
@ -32,8 +33,19 @@ struct add_opts {


static int show_only; static int show_only;
static int verbose; static int verbose;
static int guess_remote;
static timestamp_t expire; 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) static int prune_worktree(const char *id, struct strbuf *reason)
{ {
struct stat st; struct stat st;
@ -341,6 +353,7 @@ static int add(int ac, const char **av, const char *prefix)
const char *new_branch_force = NULL; const char *new_branch_force = NULL;
char *path; char *path;
const char *branch; const char *branch;
const char *opt_track = NULL;
struct option options[] = { struct option options[] = {
OPT__FORCE(&opts.force, N_("checkout <branch> even if already checked out in other worktree")), OPT__FORCE(&opts.force, N_("checkout <branch> even if already checked out in other worktree")),
OPT_STRING('b', NULL, &opts.new_branch, N_("branch"), 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, "detach", &opts.detach, N_("detach HEAD at named commit")),
OPT_BOOL(0, "checkout", &opts.checkout, N_("populate the new working tree")), 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_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() OPT_END()
}; };


@ -384,6 +402,28 @@ static int add(int ac, const char **av, const char *prefix)
int n; int n;
const char *s = worktree_basename(path, &n); const char *s = worktree_basename(path, &n);
opts.new_branch = xstrndup(s, 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) { 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, "--force");
argv_array_push(&cp.args, opts.new_branch); argv_array_push(&cp.args, opts.new_branch);
argv_array_push(&cp.args, branch); argv_array_push(&cp.args, branch);
if (opt_track)
argv_array_push(&cp.args, opt_track);
if (run_command(&cp)) if (run_command(&cp))
return -1; return -1;
branch = opts.new_branch; branch = opts.new_branch;
} else if (opt_track) {
die(_("--[no-]track can only be used if a new branch is created"));
} }


UNLEAK(path); UNLEAK(path);
@ -557,7 +601,7 @@ int cmd_worktree(int ac, const char **av, const char *prefix)
OPT_END() OPT_END()
}; };


git_config(git_default_config, NULL); git_config(git_worktree_config, NULL);


if (ac < 2) if (ac < 2)
usage_with_options(worktree_usage, options); usage_with_options(worktree_usage, options);

43
checkout.c Normal file
View 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
View 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 */

View File

@ -313,5 +313,135 @@ test_expect_success 'checkout a branch under bisect' '
test_expect_success 'rename a branch under bisect not allowed' ' test_expect_success 'rename a branch under bisect not allowed' '
test_must_fail git branch -M under-bisect bisect-with-new-name 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 test_done