summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2017-12-19 11:33:57 -0800
committerJunio C Hamano <gitster@pobox.com>2017-12-19 11:33:57 -0800
commit66d3f19324ac9eeae8ef0d2ae2067ae7b18f8ac8 (patch)
treee94fa30407c0068170f6c19a94adadd66fcbeca7
parent1974f4791af2b8720f2983149defe13e571b3884 (diff)
parente92445a73173cf21a11b8e79cb7be16485deec69 (diff)
downloadgit-66d3f19324ac9eeae8ef0d2ae2067ae7b18f8ac8.tar.gz
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
-rw-r--r--Documentation/config.txt10
-rw-r--r--Documentation/git-worktree.txt44
-rw-r--r--Makefile1
-rw-r--r--builtin/checkout.c41
-rw-r--r--builtin/worktree.c46
-rw-r--r--checkout.c43
-rw-r--r--checkout.h13
-rwxr-xr-xt/t2025-worktree-add.sh130
8 files changed, 277 insertions, 51 deletions
diff --git a/Documentation/config.txt b/Documentation/config.txt
index c1598ee703..9fac2f2b88 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -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.
diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index b472acc356..f850e8ffb6 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -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`,
diff --git a/Makefile b/Makefile
index fef9c8d272..9dc5a588e2 100644
--- a/Makefile
+++ b/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
diff --git a/builtin/checkout.c b/builtin/checkout.c
index e1e157d205..9b886356bf 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -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,
diff --git a/builtin/worktree.c b/builtin/worktree.c
index ed043d5f1c..002a569a11 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -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);
diff --git a/checkout.c b/checkout.c
new file mode 100644
index 0000000000..ac42630f74
--- /dev/null
+++ b/checkout.c
@@ -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;
+}
diff --git a/checkout.h b/checkout.h
new file mode 100644
index 0000000000..9980711179
--- /dev/null
+++ b/checkout.h
@@ -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 */
diff --git a/t/t2025-worktree-add.sh b/t/t2025-worktree-add.sh
index b5c47ac602..6ce9b9c070 100755
--- a/t/t2025-worktree-add.sh
+++ b/t/t2025-worktree-add.sh
@@ -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