Merge branch 'ja/worktree-orphan'

'git worktree add' learned how to create a worktree based on an
orphaned branch with `--orphan`.

* ja/worktree-orphan:
  worktree add: emit warn when there is a bad HEAD
  worktree add: extend DWIM to infer --orphan
  worktree add: introduce "try --orphan" hint
  worktree add: add --orphan flag
  t2400: add tests to verify --quiet
  t2400: refactor "worktree add" opt exclusion tests
  t2400: cleanup created worktree in test
  worktree add: include -B in usage docs
This commit is contained in:
Junio C Hamano 2023-06-22 16:29:05 -07:00
commit 4dd0469328
6 changed files with 735 additions and 21 deletions

View file

@ -138,4 +138,8 @@ advice.*::
checkout. checkout.
diverging:: diverging::
Advice shown when a fast-forward is not possible. Advice shown when a fast-forward is not possible.
worktreeAddOrphan::
Advice shown when a user tries to create a worktree from an
invalid reference, to instruct how to create a new orphan
branch instead.
-- --

View file

@ -10,7 +10,7 @@ SYNOPSIS
-------- --------
[verse] [verse]
'git worktree add' [-f] [--detach] [--checkout] [--lock [--reason <string>]] 'git worktree add' [-f] [--detach] [--checkout] [--lock [--reason <string>]]
[-b <new-branch>] <path> [<commit-ish>] [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]
'git worktree list' [-v | --porcelain [-z]] 'git worktree list' [-v | --porcelain [-z]]
'git worktree lock' [--reason <string>] <worktree> 'git worktree lock' [--reason <string>] <worktree>
'git worktree move' <worktree> <new-path> 'git worktree move' <worktree> <new-path>
@ -95,6 +95,16 @@ exist, a new branch based on `HEAD` is automatically created as if
`-b <branch>` was given. If `<branch>` does exist, it will be checked out `-b <branch>` was given. If `<branch>` does exist, it will be checked out
in the new worktree, if it's not checked out anywhere else, otherwise the in the new worktree, if it's not checked out anywhere else, otherwise the
command will refuse to create the worktree (unless `--force` is used). command will refuse to create the worktree (unless `--force` is used).
+
If `<commit-ish>` is omitted, neither `--detach`, or `--orphan` is
used, and there are no valid local branches (or remote branches if
`--guess-remote` is specified) then, as a convenience, the new worktree is
associated with a new orphan branch named `<branch>` (after
`$(basename <path>)` if neither `-b` or `-B` is used) as if `--orphan` was
passed to the command. In the event the repository has a remote and
`--guess-remote` is used, but no remote or local branches exist, then the
command fails with a warning reminding the user to fetch from their remote
first (or override by using `-f/--force`).
list:: list::
@ -222,6 +232,10 @@ This can also be set up as the default behaviour by using the
With `prune`, do not remove anything; just report what it would With `prune`, do not remove anything; just report what it would
remove. remove.
--orphan::
With `add`, make the new worktree and index empty, associating
the worktree with a new orphan/unborn branch named `<new-branch>`.
--porcelain:: --porcelain::
With `list`, output in an easy-to-parse format for scripts. With `list`, output in an easy-to-parse format for scripts.
This format will remain stable across Git versions and regardless of user This format will remain stable across Git versions and regardless of user

View file

@ -78,6 +78,7 @@ static struct {
[ADVICE_SUBMODULES_NOT_UPDATED] = { "submodulesNotUpdated", 1 }, [ADVICE_SUBMODULES_NOT_UPDATED] = { "submodulesNotUpdated", 1 },
[ADVICE_UPDATE_SPARSE_PATH] = { "updateSparsePath", 1 }, [ADVICE_UPDATE_SPARSE_PATH] = { "updateSparsePath", 1 },
[ADVICE_WAITING_FOR_EDITOR] = { "waitingForEditor", 1 }, [ADVICE_WAITING_FOR_EDITOR] = { "waitingForEditor", 1 },
[ADVICE_WORKTREE_ADD_ORPHAN] = { "worktreeAddOrphan", 1 },
}; };
static const char turn_off_instructions[] = static const char turn_off_instructions[] =

View file

@ -49,6 +49,7 @@ struct string_list;
ADVICE_UPDATE_SPARSE_PATH, ADVICE_UPDATE_SPARSE_PATH,
ADVICE_WAITING_FOR_EDITOR, ADVICE_WAITING_FOR_EDITOR,
ADVICE_SKIPPED_CHERRY_PICKS, ADVICE_SKIPPED_CHERRY_PICKS,
ADVICE_WORKTREE_ADD_ORPHAN,
}; };
int git_default_advice_config(const char *var, const char *value); int git_default_advice_config(const char *var, const char *value);

View file

@ -1,5 +1,6 @@
#include "cache.h" #include "cache.h"
#include "abspath.h" #include "abspath.h"
#include "advice.h"
#include "checkout.h" #include "checkout.h"
#include "config.h" #include "config.h"
#include "copy.h" #include "copy.h"
@ -14,6 +15,7 @@
#include "strvec.h" #include "strvec.h"
#include "branch.h" #include "branch.h"
#include "refs.h" #include "refs.h"
#include "remote.h"
#include "repository.h" #include "repository.h"
#include "run-command.h" #include "run-command.h"
#include "hook.h" #include "hook.h"
@ -26,7 +28,8 @@
#define BUILTIN_WORKTREE_ADD_USAGE \ #define BUILTIN_WORKTREE_ADD_USAGE \
N_("git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]]\n" \ N_("git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]]\n" \
" [-b <new-branch>] <path> [<commit-ish>]") " [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]")
#define BUILTIN_WORKTREE_LIST_USAGE \ #define BUILTIN_WORKTREE_LIST_USAGE \
N_("git worktree list [-v | --porcelain [-z]]") N_("git worktree list [-v | --porcelain [-z]]")
#define BUILTIN_WORKTREE_LOCK_USAGE \ #define BUILTIN_WORKTREE_LOCK_USAGE \
@ -42,6 +45,23 @@
#define BUILTIN_WORKTREE_UNLOCK_USAGE \ #define BUILTIN_WORKTREE_UNLOCK_USAGE \
N_("git worktree unlock <worktree>") N_("git worktree unlock <worktree>")
#define WORKTREE_ADD_DWIM_ORPHAN_INFER_TEXT \
_("No possible source branch, inferring '--orphan'")
#define WORKTREE_ADD_ORPHAN_WITH_DASH_B_HINT_TEXT \
_("If you meant to create a worktree containing a new orphan branch\n" \
"(branch with no commits) for this repository, you can do so\n" \
"using the --orphan flag:\n" \
"\n" \
" git worktree add --orphan -b %s %s\n")
#define WORKTREE_ADD_ORPHAN_NO_DASH_B_HINT_TEXT \
_("If you meant to create a worktree containing a new orphan branch\n" \
"(branch with no commits) for this repository, you can do so\n" \
"using the --orphan flag:\n" \
"\n" \
" git worktree add --orphan %s\n")
static const char * const git_worktree_usage[] = { static const char * const git_worktree_usage[] = {
BUILTIN_WORKTREE_ADD_USAGE, BUILTIN_WORKTREE_ADD_USAGE,
BUILTIN_WORKTREE_LIST_USAGE, BUILTIN_WORKTREE_LIST_USAGE,
@ -99,6 +119,7 @@ struct add_opts {
int detach; int detach;
int quiet; int quiet;
int checkout; int checkout;
int orphan;
const char *keep_locked; const char *keep_locked;
}; };
@ -372,6 +393,22 @@ static int checkout_worktree(const struct add_opts *opts,
return run_command(&cp); return run_command(&cp);
} }
static int make_worktree_orphan(const char * ref, const struct add_opts *opts,
struct strvec *child_env)
{
struct strbuf symref = STRBUF_INIT;
struct child_process cp = CHILD_PROCESS_INIT;
validate_new_branchname(ref, &symref, 0);
strvec_pushl(&cp.args, "symbolic-ref", "HEAD", symref.buf, NULL);
if (opts->quiet)
strvec_push(&cp.args, "--quiet");
strvec_pushv(&cp.env, child_env->v);
strbuf_release(&symref);
cp.git_cmd = 1;
return run_command(&cp);
}
static int add_worktree(const char *path, const char *refname, static int add_worktree(const char *path, const char *refname,
const struct add_opts *opts) const struct add_opts *opts)
{ {
@ -401,7 +438,7 @@ static int add_worktree(const char *path, const char *refname,
die_if_checked_out(symref.buf, 0); die_if_checked_out(symref.buf, 0);
} }
commit = lookup_commit_reference_by_name(refname); commit = lookup_commit_reference_by_name(refname);
if (!commit) if (!commit && !opts->orphan)
die(_("invalid reference: %s"), refname); die(_("invalid reference: %s"), refname);
name = worktree_basename(path, &len); name = worktree_basename(path, &len);
@ -490,10 +527,10 @@ static int add_worktree(const char *path, const char *refname,
strvec_pushf(&child_env, "%s=%s", GIT_WORK_TREE_ENVIRONMENT, path); strvec_pushf(&child_env, "%s=%s", GIT_WORK_TREE_ENVIRONMENT, path);
cp.git_cmd = 1; cp.git_cmd = 1;
if (!is_branch) if (!is_branch && commit) {
strvec_pushl(&cp.args, "update-ref", "HEAD", strvec_pushl(&cp.args, "update-ref", "HEAD",
oid_to_hex(&commit->object.oid), NULL); oid_to_hex(&commit->object.oid), NULL);
else { } else {
strvec_pushl(&cp.args, "symbolic-ref", "HEAD", strvec_pushl(&cp.args, "symbolic-ref", "HEAD",
symref.buf, NULL); symref.buf, NULL);
if (opts->quiet) if (opts->quiet)
@ -505,6 +542,10 @@ static int add_worktree(const char *path, const char *refname,
if (ret) if (ret)
goto done; goto done;
if (opts->orphan &&
(ret = make_worktree_orphan(refname, opts, &child_env)))
goto done;
if (opts->checkout && if (opts->checkout &&
(ret = checkout_worktree(opts, &child_env))) (ret = checkout_worktree(opts, &child_env)))
goto done; goto done;
@ -524,7 +565,7 @@ static int add_worktree(const char *path, const char *refname,
* Hook failure does not warrant worktree deletion, so run hook after * Hook failure does not warrant worktree deletion, so run hook after
* is_junk is cleared, but do return appropriate code when hook fails. * is_junk is cleared, but do return appropriate code when hook fails.
*/ */
if (!ret && opts->checkout) { if (!ret && opts->checkout && !opts->orphan) {
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL); strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL);
@ -572,7 +613,7 @@ static void print_preparing_worktree_line(int detach,
else { else {
struct commit *commit = lookup_commit_reference_by_name(branch); struct commit *commit = lookup_commit_reference_by_name(branch);
if (!commit) if (!commit)
die(_("invalid reference: %s"), branch); BUG(_("unreachable: invalid reference: %s"), branch);
fprintf_ln(stderr, _("Preparing worktree (detached HEAD %s)"), fprintf_ln(stderr, _("Preparing worktree (detached HEAD %s)"),
repo_find_unique_abbrev(the_repository, &commit->object.oid, DEFAULT_ABBREV)); repo_find_unique_abbrev(the_repository, &commit->object.oid, DEFAULT_ABBREV));
} }
@ -580,6 +621,123 @@ static void print_preparing_worktree_line(int detach,
} }
} }
/**
* Callback to short circuit iteration over refs on the first reference
* corresponding to a valid oid.
*
* Returns 0 on failure and non-zero on success.
*/
static int first_valid_ref(const char *refname,
const struct object_id *oid,
int flags,
void *cb_data)
{
return 1;
}
/**
* Verifies HEAD and determines whether there exist any valid local references.
*
* - Checks whether HEAD points to a valid reference.
*
* - Checks whether any valid local branches exist.
*
* - Emits a warning if there exist any valid branches but HEAD does not point
* to a valid reference.
*
* Returns 1 if any of the previous checks are true, otherwise returns 0.
*/
static int can_use_local_refs(const struct add_opts *opts)
{
if (head_ref(first_valid_ref, NULL)) {
return 1;
} else if (for_each_branch_ref(first_valid_ref, NULL)) {
if (!opts->quiet) {
struct strbuf path = STRBUF_INIT;
struct strbuf contents = STRBUF_INIT;
strbuf_add_real_path(&path, get_worktree_git_dir(NULL));
strbuf_addstr(&path, "/HEAD");
strbuf_read_file(&contents, path.buf, 64);
strbuf_stripspace(&contents, 0);
strbuf_strip_suffix(&contents, "\n");
warning(_("HEAD points to an invalid (or orphaned) reference.\n"
"HEAD path: '%s'\n"
"HEAD contents: '%s'"),
path.buf, contents.buf);
strbuf_release(&path);
strbuf_release(&contents);
}
return 1;
}
return 0;
}
/**
* Reports whether the necessary flags were set and whether the repository has
* remote references to attempt DWIM tracking of upstream branches.
*
* 1. Checks that `--guess-remote` was used or `worktree.guessRemote = true`.
*
* 2. Checks whether any valid remote branches exist.
*
* 3. Checks that there exists at least one remote and emits a warning/error
* if both checks 1. and 2. are false (can be bypassed with `--force`).
*
* Returns 1 if checks 1. and 2. are true, otherwise 0.
*/
static int can_use_remote_refs(const struct add_opts *opts)
{
if (!guess_remote) {
return 0;
} else if (for_each_remote_ref(first_valid_ref, NULL)) {
return 1;
} else if (!opts->force && remote_get(NULL)) {
die(_("No local or remote refs exist despite at least one remote\n"
"present, stopping; use 'add -f' to overide or fetch a remote first"));
}
return 0;
}
/**
* Determines whether `--orphan` should be inferred in the evaluation of
* `worktree add path/` or `worktree add -b branch path/` and emits an error
* if the supplied arguments would produce an illegal combination when the
* `--orphan` flag is included.
*
* `opts` and `opt_track` contain the other options & flags supplied to the
* command.
*
* remote determines whether to check `can_use_remote_refs()` or not. This
* is primarily to differentiate between the basic `add` DWIM and `add -b`.
*
* Returns 1 when inferring `--orphan`, 0 otherwise, and emits an error when
* `--orphan` is inferred but doing so produces an illegal combination of
* options and flags. Additionally produces an error when remote refs are
* checked and the repo is in a state that looks like the user added a remote
* but forgot to fetch (and did not override the warning with -f).
*/
static int dwim_orphan(const struct add_opts *opts, int opt_track, int remote)
{
if (can_use_local_refs(opts)) {
return 0;
} else if (remote && can_use_remote_refs(opts)) {
return 0;
} else if (!opts->quiet) {
fprintf_ln(stderr, WORKTREE_ADD_DWIM_ORPHAN_INFER_TEXT);
}
if (opt_track) {
die(_("'%s' and '%s' cannot be used together"), "--orphan",
"--track");
} else if (!opts->checkout) {
die(_("'%s' and '%s' cannot be used together"), "--orphan",
"--no-checkout");
}
return 1;
}
static const char *dwim_branch(const char *path, const char **new_branch) static const char *dwim_branch(const char *path, const char **new_branch)
{ {
int n; int n;
@ -616,6 +774,7 @@ static int add(int ac, const char **av, const char *prefix)
const char *opt_track = NULL; const char *opt_track = NULL;
const char *lock_reason = NULL; const char *lock_reason = NULL;
int keep_locked = 0; int keep_locked = 0;
int used_new_branch_options;
struct option options[] = { struct option options[] = {
OPT__FORCE(&opts.force, OPT__FORCE(&opts.force,
N_("checkout <branch> even if already checked out in other worktree"), N_("checkout <branch> even if already checked out in other worktree"),
@ -624,6 +783,7 @@ static int add(int ac, const char **av, const char *prefix)
N_("create a new branch")), N_("create a new branch")),
OPT_STRING('B', NULL, &new_branch_force, N_("branch"), OPT_STRING('B', NULL, &new_branch_force, N_("branch"),
N_("create or reset a branch")), N_("create or reset a branch")),
OPT_BOOL(0, "orphan", &opts.orphan, N_("create unborn/orphaned branch")),
OPT_BOOL('d', "detach", &opts.detach, N_("detach HEAD at named commit")), OPT_BOOL('d', "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", &keep_locked, N_("keep the new working tree locked")), OPT_BOOL(0, "lock", &keep_locked, N_("keep the new working tree locked")),
@ -644,6 +804,17 @@ static int add(int ac, const char **av, const char *prefix)
ac = parse_options(ac, av, prefix, options, git_worktree_add_usage, 0); ac = parse_options(ac, av, prefix, options, git_worktree_add_usage, 0);
if (!!opts.detach + !!new_branch + !!new_branch_force > 1) if (!!opts.detach + !!new_branch + !!new_branch_force > 1)
die(_("options '%s', '%s', and '%s' cannot be used together"), "-b", "-B", "--detach"); die(_("options '%s', '%s', and '%s' cannot be used together"), "-b", "-B", "--detach");
if (opts.detach && opts.orphan)
die(_("options '%s', and '%s' cannot be used together"),
"--orphan", "--detach");
if (opts.orphan && opt_track)
die(_("'%s' and '%s' cannot be used together"), "--orphan", "--track");
if (opts.orphan && !opts.checkout)
die(_("'%s' and '%s' cannot be used together"), "--orphan",
"--no-checkout");
if (opts.orphan && ac == 2)
die(_("'%s' and '%s' cannot be used together"), "--orphan",
_("<commit-ish>"));
if (lock_reason && !keep_locked) if (lock_reason && !keep_locked)
die(_("the option '%s' requires '%s'"), "--reason", "--lock"); die(_("the option '%s' requires '%s'"), "--reason", "--lock");
if (lock_reason) if (lock_reason)
@ -656,6 +827,7 @@ static int add(int ac, const char **av, const char *prefix)
path = prefix_filename(prefix, av[0]); path = prefix_filename(prefix, av[0]);
branch = ac < 2 ? "HEAD" : av[1]; branch = ac < 2 ? "HEAD" : av[1];
used_new_branch_options = new_branch || new_branch_force;
if (!strcmp(branch, "-")) if (!strcmp(branch, "-"))
branch = "@{-1}"; branch = "@{-1}";
@ -672,13 +844,28 @@ static int add(int ac, const char **av, const char *prefix)
strbuf_release(&symref); strbuf_release(&symref);
} }
if (ac < 2 && !new_branch && !opts.detach) { if (opts.orphan && !new_branch) {
int n;
const char *s = worktree_basename(path, &n);
new_branch = xstrndup(s, n);
} else if (opts.orphan) {
// No-op
} else if (opts.detach) {
// Check HEAD
if (!strcmp(branch, "HEAD"))
can_use_local_refs(&opts);
} else if (ac < 2 && new_branch) {
// DWIM: Infer --orphan when repo has no refs.
opts.orphan = dwim_orphan(&opts, !!opt_track, 0);
} else if (ac < 2) {
// DWIM: Guess branch name from path.
const char *s = dwim_branch(path, &new_branch); const char *s = dwim_branch(path, &new_branch);
if (s) if (s)
branch = s; branch = s;
}
if (ac == 2 && !new_branch && !opts.detach) { // DWIM: Infer --orphan when repo has no refs.
opts.orphan = (!s) && dwim_orphan(&opts, !!opt_track, 1);
} else if (ac == 2) {
struct object_id oid; struct object_id oid;
struct commit *commit; struct commit *commit;
const char *remote; const char *remote;
@ -691,11 +878,31 @@ static int add(int ac, const char **av, const char *prefix)
branch = remote; branch = remote;
} }
} }
if (!strcmp(branch, "HEAD"))
can_use_local_refs(&opts);
} }
if (!opts.orphan && !lookup_commit_reference_by_name(branch)) {
int attempt_hint = !opts.quiet && (ac < 2);
if (attempt_hint && used_new_branch_options) {
advise_if_enabled(ADVICE_WORKTREE_ADD_ORPHAN,
WORKTREE_ADD_ORPHAN_WITH_DASH_B_HINT_TEXT,
new_branch, path);
} else if (attempt_hint) {
advise_if_enabled(ADVICE_WORKTREE_ADD_ORPHAN,
WORKTREE_ADD_ORPHAN_NO_DASH_B_HINT_TEXT, path);
}
die(_("invalid reference: %s"), branch);
}
if (!opts.quiet) if (!opts.quiet)
print_preparing_worktree_line(opts.detach, branch, new_branch, !!new_branch_force); print_preparing_worktree_line(opts.detach, branch, new_branch, !!new_branch_force);
if (new_branch) { if (opts.orphan) {
branch = new_branch;
} else if (new_branch) {
struct child_process cp = CHILD_PROCESS_INIT; struct child_process cp = CHILD_PROCESS_INIT;
cp.git_cmd = 1; cp.git_cmd = 1;
strvec_push(&cp.args, "branch"); strvec_push(&cp.args, "branch");

View file

@ -298,17 +298,24 @@ test_expect_success '"add" no auto-vivify with --detach and <branch> omitted' '
test_must_fail git -C mish/mash symbolic-ref HEAD test_must_fail git -C mish/mash symbolic-ref HEAD
' '
test_expect_success '"add" -b/-B mutually exclusive' ' # Helper function to test mutually exclusive options.
test_must_fail git worktree add -b poodle -B poodle bamboo main #
' # Note: Quoted arguments containing spaces are not supported.
test_wt_add_excl () {
local opts="$*" &&
test_expect_success "'worktree add' with '$opts' has mutually exclusive options" '
test_must_fail git worktree add $opts 2>actual &&
grep -E "fatal:( options)? .* cannot be used together" actual
'
}
test_expect_success '"add" -b/--detach mutually exclusive' ' test_wt_add_excl -b poodle -B poodle bamboo main
test_must_fail git worktree add -b poodle --detach bamboo main test_wt_add_excl -b poodle --detach bamboo main
' test_wt_add_excl -B poodle --detach bamboo main
test_wt_add_excl --orphan --detach bamboo
test_expect_success '"add" -B/--detach mutually exclusive' ' test_wt_add_excl --orphan --no-checkout bamboo
test_must_fail git worktree add -B poodle --detach bamboo main test_wt_add_excl --orphan bamboo main
' test_wt_add_excl --orphan -b bamboo wtdir/ main
test_expect_success '"add -B" fails if the branch is checked out' ' test_expect_success '"add -B" fails if the branch is checked out' '
git rev-parse newmain >before && git rev-parse newmain >before &&
@ -326,10 +333,111 @@ test_expect_success 'add -B' '
' '
test_expect_success 'add --quiet' ' test_expect_success 'add --quiet' '
test_when_finished "git worktree remove -f -f another-worktree" &&
git worktree add --quiet another-worktree main 2>actual && git worktree add --quiet another-worktree main 2>actual &&
test_must_be_empty actual test_must_be_empty actual
' '
test_expect_success 'add --quiet -b' '
test_when_finished "git branch -D quietnewbranch" &&
test_when_finished "git worktree remove -f -f another-worktree" &&
git worktree add --quiet -b quietnewbranch another-worktree 2>actual &&
test_must_be_empty actual
'
test_expect_success '"add --orphan"' '
test_when_finished "git worktree remove -f -f orphandir" &&
git worktree add --orphan -b neworphan orphandir &&
echo refs/heads/neworphan >expected &&
git -C orphandir symbolic-ref HEAD >actual &&
test_cmp expected actual
'
test_expect_success '"add --orphan (no -b)"' '
test_when_finished "git worktree remove -f -f neworphan" &&
git worktree add --orphan neworphan &&
echo refs/heads/neworphan >expected &&
git -C neworphan symbolic-ref HEAD >actual &&
test_cmp expected actual
'
test_expect_success '"add --orphan --quiet"' '
test_when_finished "git worktree remove -f -f orphandir" &&
git worktree add --quiet --orphan -b neworphan orphandir 2>log.actual &&
test_must_be_empty log.actual &&
echo refs/heads/neworphan >expected &&
git -C orphandir symbolic-ref HEAD >actual &&
test_cmp expected actual
'
test_expect_success '"add --orphan" fails if the branch already exists' '
test_when_finished "git branch -D existingbranch" &&
git worktree add -b existingbranch orphandir main &&
git worktree remove orphandir &&
test_must_fail git worktree add --orphan -b existingbranch orphandir
'
test_expect_success '"add --orphan" with empty repository' '
test_when_finished "rm -rf empty_repo" &&
echo refs/heads/newbranch >expected &&
GIT_DIR="empty_repo" git init --bare &&
git -C empty_repo worktree add --orphan -b newbranch worktreedir &&
git -C empty_repo/worktreedir symbolic-ref HEAD >actual &&
test_cmp expected actual
'
test_expect_success '"add" worktree with orphan branch and lock' '
git worktree add --lock --orphan -b orphanbr orphan-with-lock &&
test_when_finished "git worktree unlock orphan-with-lock || :" &&
test -f .git/worktrees/orphan-with-lock/locked
'
test_expect_success '"add" worktree with orphan branch, lock, and reason' '
lock_reason="why not" &&
git worktree add --detach --lock --reason "$lock_reason" orphan-with-lock-reason main &&
test_when_finished "git worktree unlock orphan-with-lock-reason || :" &&
test -f .git/worktrees/orphan-with-lock-reason/locked &&
echo "$lock_reason" >expect &&
test_cmp expect .git/worktrees/orphan-with-lock-reason/locked
'
# Note: Quoted arguments containing spaces are not supported.
test_wt_add_orphan_hint () {
local context="$1" &&
local use_branch=$2 &&
shift 2 &&
local opts="$*" &&
test_expect_success "'worktree add' show orphan hint in bad/orphan HEAD w/ $context" '
test_when_finished "rm -rf repo" &&
git init repo &&
(cd repo && test_commit commit) &&
git -C repo switch --orphan noref &&
test_must_fail git -C repo worktree add $opts foobar/ 2>actual &&
! grep "error: unknown switch" actual &&
grep "hint: If you meant to create a worktree containing a new orphan branch" actual &&
if [ $use_branch -eq 1 ]
then
grep -E "^hint:\s+git worktree add --orphan -b \S+ \S+\s*$" actual
else
grep -E "^hint:\s+git worktree add --orphan \S+\s*$" actual
fi
'
}
test_wt_add_orphan_hint 'no opts' 0
test_wt_add_orphan_hint '-b' 1 -b foobar_branch
test_wt_add_orphan_hint '-B' 1 -B foobar_branch
test_expect_success "'worktree add' doesn't show orphan hint in bad/orphan HEAD w/ --quiet" '
test_when_finished "rm -rf repo" &&
git init repo &&
(cd repo && test_commit commit) &&
test_must_fail git -C repo worktree add --quiet foobar_branch foobar/ 2>actual &&
! grep "error: unknown switch" actual &&
! grep "hint: If you meant to create a worktree containing a new orphan branch" actual
'
test_expect_success 'local clone from linked checkout' ' test_expect_success 'local clone from linked checkout' '
git clone --local here here-clone && git clone --local here here-clone &&
( cd here-clone && git fsck ) ( cd here-clone && git fsck )
@ -446,6 +554,14 @@ setup_remote_repo () {
) )
} }
test_expect_success '"add" <path> <remote/branch> w/ no HEAD' '
test_when_finished rm -rf repo_upstream repo_local foo &&
setup_remote_repo repo_upstream repo_local &&
git -C repo_local config --bool core.bare true &&
git -C repo_local branch -D main &&
git -C repo_local worktree add ./foo repo_upstream/foo
'
test_expect_success '--no-track avoids setting up tracking' ' test_expect_success '--no-track avoids setting up tracking' '
test_when_finished rm -rf repo_upstream repo_local foo && test_when_finished rm -rf repo_upstream repo_local foo &&
setup_remote_repo repo_upstream repo_local && setup_remote_repo repo_upstream repo_local &&
@ -528,6 +644,35 @@ test_expect_success 'git worktree add --guess-remote sets up tracking' '
test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo
) )
' '
test_expect_success 'git worktree add --guess-remote sets up tracking (quiet)' '
test_when_finished rm -rf repo_a repo_b foo &&
setup_remote_repo repo_a repo_b &&
(
cd repo_b &&
git worktree add --quiet --guess-remote ../foo 2>actual &&
test_must_be_empty actual
) &&
(
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 (quiet)' '
test_when_finished rm -rf repo_a repo_b foo &&
setup_remote_repo repo_a repo_b &&
(
cd repo_b &&
git worktree add --quiet --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_expect_success 'git worktree add with worktree.guessRemote sets up tracking' ' test_expect_success 'git worktree add with worktree.guessRemote sets up tracking' '
test_when_finished rm -rf repo_a repo_b foo && test_when_finished rm -rf repo_a repo_b foo &&
@ -560,6 +705,348 @@ test_expect_success 'git worktree --no-guess-remote option overrides config' '
) )
' '
test_dwim_orphan () {
local info_text="No possible source branch, inferring '--orphan'" &&
local fetch_error_text="fatal: No local or remote refs exist despite at least one remote" &&
local orphan_hint="hint: If you meant to create a worktree containing a new orphan branch" &&
local invalid_ref_regex="^fatal: invalid reference:\s\+.*" &&
local bad_combo_regex="^fatal: '[a-z-]\+' and '[a-z-]\+' cannot be used together" &&
local git_ns="repo" &&
local dashc_args="-C $git_ns" &&
local use_cd=0 &&
local bad_head=0 &&
local empty_repo=1 &&
local local_ref=0 &&
local use_quiet=0 &&
local remote=0 &&
local remote_ref=0 &&
local use_detach=0 &&
local use_new_branch=0 &&
local outcome="$1" &&
local outcome_text &&
local success &&
shift &&
local args="" &&
local context="" &&
case "$outcome" in
"infer")
success=1 &&
outcome_text='"add" DWIM infer --orphan'
;;
"no_infer")
success=1 &&
outcome_text='"add" DWIM doesnt infer --orphan'
;;
"fetch_error")
success=0 &&
outcome_text='"add" error need fetch'
;;
"fatal_orphan_bad_combo")
success=0 &&
outcome_text='"add" error inferred "--orphan" gives illegal opts combo'
;;
"warn_bad_head")
success=0 &&
outcome_text='"add" error, warn on bad HEAD, hint use orphan'
;;
*)
echo "test_dwim_orphan(): invalid outcome: '$outcome'" >&2 &&
return 1
;;
esac &&
while [ $# -gt 0 ]
do
case "$1" in
# How and from where to create the worktree
"-C_repo")
use_cd=0 &&
git_ns="repo" &&
dashc_args="-C $git_ns" &&
context="$context, 'git -C repo'"
;;
"-C_wt")
use_cd=0 &&
git_ns="wt" &&
dashc_args="-C $git_ns" &&
context="$context, 'git -C wt'"
;;
"cd_repo")
use_cd=1 &&
git_ns="repo" &&
dashc_args="" &&
context="$context, 'cd repo && git'"
;;
"cd_wt")
use_cd=1 &&
git_ns="wt" &&
dashc_args="" &&
context="$context, 'cd wt && git'"
;;
# Bypass the "pull first" warning
"force")
args="$args --force" &&
context="$context, --force"
;;
# Try to use remote refs when DWIM
"guess_remote")
args="$args --guess-remote" &&
context="$context, --guess-remote"
;;
"no_guess_remote")
args="$args --no-guess-remote" &&
context="$context, --no-guess-remote"
;;
# Whether there is at least one local branch present
"local_ref")
empty_repo=0 &&
local_ref=1 &&
context="$context, >=1 local branches"
;;
"no_local_ref")
empty_repo=0 &&
context="$context, 0 local branches"
;;
# Whether the HEAD points at a valid ref (skip this opt when no refs)
"good_head")
# requires: local_ref
context="$context, valid HEAD"
;;
"bad_head")
bad_head=1 &&
context="$context, invalid (or orphan) HEAD"
;;
# Whether the code path is tested with the base add command, -b, or --detach
"no_-b")
use_new_branch=0 &&
context="$context, no --branch"
;;
"-b")
use_new_branch=1 &&
context="$context, --branch"
;;
"detach")
use_detach=1 &&
context="$context, --detach"
;;
# Whether to check that all output is suppressed (except errors)
# or that the output is as expected
"quiet")
use_quiet=1 &&
args="$args --quiet" &&
context="$context, --quiet"
;;
"no_quiet")
use_quiet=0 &&
context="$context, no --quiet (expect output)"
;;
# Whether there is at least one remote attached to the repo
"remote")
empty_repo=0 &&
remote=1 &&
context="$context, >=1 remotes"
;;
"no_remote")
empty_repo=0 &&
remote=0 &&
context="$context, 0 remotes"
;;
# Whether there is at least one valid remote ref
"remote_ref")
# requires: remote
empty_repo=0 &&
remote_ref=1 &&
context="$context, >=1 fetched remote branches"
;;
"no_remote_ref")
empty_repo=0 &&
remote_ref=0 &&
context="$context, 0 fetched remote branches"
;;
# Options or flags that become illegal when --orphan is inferred
"no_checkout")
args="$args --no-checkout" &&
context="$context, --no-checkout"
;;
"track")
args="$args --track" &&
context="$context, --track"
;;
# All other options are illegal
*)
echo "test_dwim_orphan(): invalid arg: '$1'" >&2 &&
return 1
;;
esac &&
shift
done &&
context="${context#', '}" &&
if [ $use_new_branch -eq 1 ]
then
args="$args -b foo"
elif [ $use_detach -eq 1 ]
then
args="$args --detach"
else
context="DWIM (no --branch), $context"
fi &&
if [ $empty_repo -eq 1 ]
then
context="empty repo, $context"
fi &&
args="$args ../foo" &&
context="${context%', '}" &&
test_expect_success "$outcome_text w/ $context" '
test_when_finished "rm -rf repo" &&
git init repo &&
if [ $local_ref -eq 1 ] && [ "$git_ns" = "repo" ]
then
(cd repo && test_commit commit) &&
if [ $bad_head -eq 1 ]
then
git -C repo symbolic-ref HEAD refs/heads/badbranch
fi
elif [ $local_ref -eq 1 ] && [ "$git_ns" = "wt" ]
then
test_when_finished "git -C repo worktree remove -f ../wt" &&
git -C repo worktree add --orphan -b main ../wt &&
(cd wt && test_commit commit) &&
if [ $bad_head -eq 1 ]
then
git -C wt symbolic-ref HEAD refs/heads/badbranch
fi
elif [ $local_ref -eq 0 ] && [ "$git_ns" = "wt" ]
then
test_when_finished "git -C repo worktree remove -f ../wt" &&
git -C repo worktree add --orphan -b orphanbranch ../wt
fi &&
if [ $remote -eq 1 ]
then
test_when_finished "rm -rf upstream" &&
git init upstream &&
(cd upstream && test_commit commit) &&
git -C upstream switch -c foo &&
git -C repo remote add upstream ../upstream
fi &&
if [ $remote_ref -eq 1 ]
then
git -C repo fetch
fi &&
if [ $success -eq 1 ]
then
test_when_finished git -C repo worktree remove ../foo
fi &&
(
if [ $use_cd -eq 1 ]
then
cd $git_ns
fi &&
if [ "$outcome" = "infer" ]
then
git $dashc_args worktree add $args 2>actual &&
if [ $use_quiet -eq 1 ]
then
test_must_be_empty actual
else
grep "$info_text" actual
fi
elif [ "$outcome" = "no_infer" ]
then
git $dashc_args worktree add $args 2>actual &&
if [ $use_quiet -eq 1 ]
then
test_must_be_empty actual
else
! grep "$info_text" actual
fi
elif [ "$outcome" = "fetch_error" ]
then
test_must_fail git $dashc_args worktree add $args 2>actual &&
grep "$fetch_error_text" actual
elif [ "$outcome" = "fatal_orphan_bad_combo" ]
then
test_must_fail git $dashc_args worktree add $args 2>actual &&
if [ $use_quiet -eq 1 ]
then
! grep "$info_text" actual
else
grep "$info_text" actual
fi &&
grep "$bad_combo_regex" actual
elif [ "$outcome" = "warn_bad_head" ]
then
test_must_fail git $dashc_args worktree add $args 2>actual &&
if [ $use_quiet -eq 1 ]
then
grep "$invalid_ref_regex" actual &&
! grep "$orphan_hint" actual
else
headpath=$(git $dashc_args rev-parse --sq --path-format=absolute --git-path HEAD) &&
headcontents=$(cat "$headpath") &&
grep "HEAD points to an invalid (or orphaned) reference" actual &&
grep "HEAD path:\s*.$headpath." actual &&
grep "HEAD contents:\s*.$headcontents." actual &&
grep "$orphan_hint" actual &&
! grep "$info_text" actual
fi &&
grep "$invalid_ref_regex" actual
else
# Unreachable
false
fi
) &&
if [ $success -ne 1 ]
then
test_path_is_missing foo
fi
'
}
for quiet_mode in "no_quiet" "quiet"
do
for changedir_type in "cd_repo" "cd_wt" "-C_repo" "-C_wt"
do
dwim_test_args="$quiet_mode $changedir_type"
test_dwim_orphan 'infer' $dwim_test_args no_-b
test_dwim_orphan 'no_infer' $dwim_test_args no_-b local_ref good_head
test_dwim_orphan 'infer' $dwim_test_args no_-b no_local_ref no_remote no_remote_ref no_guess_remote
test_dwim_orphan 'infer' $dwim_test_args no_-b no_local_ref remote no_remote_ref no_guess_remote
test_dwim_orphan 'fetch_error' $dwim_test_args no_-b no_local_ref remote no_remote_ref guess_remote
test_dwim_orphan 'infer' $dwim_test_args no_-b no_local_ref remote no_remote_ref guess_remote force
test_dwim_orphan 'no_infer' $dwim_test_args no_-b no_local_ref remote remote_ref guess_remote
test_dwim_orphan 'infer' $dwim_test_args -b
test_dwim_orphan 'no_infer' $dwim_test_args -b local_ref good_head
test_dwim_orphan 'infer' $dwim_test_args -b no_local_ref no_remote no_remote_ref no_guess_remote
test_dwim_orphan 'infer' $dwim_test_args -b no_local_ref remote no_remote_ref no_guess_remote
test_dwim_orphan 'infer' $dwim_test_args -b no_local_ref remote no_remote_ref guess_remote
test_dwim_orphan 'infer' $dwim_test_args -b no_local_ref remote remote_ref guess_remote
test_dwim_orphan 'warn_bad_head' $dwim_test_args no_-b local_ref bad_head
test_dwim_orphan 'warn_bad_head' $dwim_test_args -b local_ref bad_head
test_dwim_orphan 'warn_bad_head' $dwim_test_args detach local_ref bad_head
done
test_dwim_orphan 'fatal_orphan_bad_combo' $quiet_mode no_-b no_checkout
test_dwim_orphan 'fatal_orphan_bad_combo' $quiet_mode no_-b track
test_dwim_orphan 'fatal_orphan_bad_combo' $quiet_mode -b no_checkout
test_dwim_orphan 'fatal_orphan_bad_combo' $quiet_mode -b track
done
post_checkout_hook () { post_checkout_hook () {
test_when_finished "rm -rf .git/hooks" && test_when_finished "rm -rf .git/hooks" &&
mkdir .git/hooks && mkdir .git/hooks &&