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.
diverging::
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]
'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 lock' [--reason <string>] <worktree>
'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
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).
+
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::
@ -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
remove.
--orphan::
With `add`, make the new worktree and index empty, associating
the worktree with a new orphan/unborn branch named `<new-branch>`.
--porcelain::
With `list`, output in an easy-to-parse format for scripts.
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_UPDATE_SPARSE_PATH] = { "updateSparsePath", 1 },
[ADVICE_WAITING_FOR_EDITOR] = { "waitingForEditor", 1 },
[ADVICE_WORKTREE_ADD_ORPHAN] = { "worktreeAddOrphan", 1 },
};
static const char turn_off_instructions[] =

View file

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

View file

@ -1,5 +1,6 @@
#include "cache.h"
#include "abspath.h"
#include "advice.h"
#include "checkout.h"
#include "config.h"
#include "copy.h"
@ -14,6 +15,7 @@
#include "strvec.h"
#include "branch.h"
#include "refs.h"
#include "remote.h"
#include "repository.h"
#include "run-command.h"
#include "hook.h"
@ -26,7 +28,8 @@
#define BUILTIN_WORKTREE_ADD_USAGE \
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 \
N_("git worktree list [-v | --porcelain [-z]]")
#define BUILTIN_WORKTREE_LOCK_USAGE \
@ -42,6 +45,23 @@
#define BUILTIN_WORKTREE_UNLOCK_USAGE \
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[] = {
BUILTIN_WORKTREE_ADD_USAGE,
BUILTIN_WORKTREE_LIST_USAGE,
@ -99,6 +119,7 @@ struct add_opts {
int detach;
int quiet;
int checkout;
int orphan;
const char *keep_locked;
};
@ -372,6 +393,22 @@ static int checkout_worktree(const struct add_opts *opts,
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,
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);
}
commit = lookup_commit_reference_by_name(refname);
if (!commit)
if (!commit && !opts->orphan)
die(_("invalid reference: %s"), refname);
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);
cp.git_cmd = 1;
if (!is_branch)
if (!is_branch && commit) {
strvec_pushl(&cp.args, "update-ref", "HEAD",
oid_to_hex(&commit->object.oid), NULL);
else {
} else {
strvec_pushl(&cp.args, "symbolic-ref", "HEAD",
symref.buf, NULL);
if (opts->quiet)
@ -505,6 +542,10 @@ static int add_worktree(const char *path, const char *refname,
if (ret)
goto done;
if (opts->orphan &&
(ret = make_worktree_orphan(refname, opts, &child_env)))
goto done;
if (opts->checkout &&
(ret = checkout_worktree(opts, &child_env)))
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
* 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;
strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL);
@ -572,7 +613,7 @@ static void print_preparing_worktree_line(int detach,
else {
struct commit *commit = lookup_commit_reference_by_name(branch);
if (!commit)
die(_("invalid reference: %s"), branch);
BUG(_("unreachable: invalid reference: %s"), branch);
fprintf_ln(stderr, _("Preparing worktree (detached HEAD %s)"),
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)
{
int n;
@ -616,6 +774,7 @@ static int add(int ac, const char **av, const char *prefix)
const char *opt_track = NULL;
const char *lock_reason = NULL;
int keep_locked = 0;
int used_new_branch_options;
struct option options[] = {
OPT__FORCE(&opts.force,
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")),
OPT_STRING('B', NULL, &new_branch_force, N_("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(0, "checkout", &opts.checkout, N_("populate the new working tree")),
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);
if (!!opts.detach + !!new_branch + !!new_branch_force > 1)
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)
die(_("the option '%s' requires '%s'"), "--reason", "--lock");
if (lock_reason)
@ -656,6 +827,7 @@ static int add(int ac, const char **av, const char *prefix)
path = prefix_filename(prefix, av[0]);
branch = ac < 2 ? "HEAD" : av[1];
used_new_branch_options = new_branch || new_branch_force;
if (!strcmp(branch, "-"))
branch = "@{-1}";
@ -672,13 +844,28 @@ static int add(int ac, const char **av, const char *prefix)
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);
if (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 commit *commit;
const char *remote;
@ -691,11 +878,31 @@ static int add(int ac, const char **av, const char *prefix)
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)
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;
cp.git_cmd = 1;
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_expect_success '"add" -b/-B mutually exclusive' '
test_must_fail git worktree add -b poodle -B poodle bamboo main
'
# Helper function to test mutually exclusive options.
#
# 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_must_fail git worktree add -b poodle --detach bamboo main
'
test_expect_success '"add" -B/--detach mutually exclusive' '
test_must_fail git worktree add -B poodle --detach bamboo main
'
test_wt_add_excl -b poodle -B poodle 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_wt_add_excl --orphan --no-checkout bamboo
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' '
git rev-parse newmain >before &&
@ -326,10 +333,111 @@ test_expect_success 'add -B' '
'
test_expect_success 'add --quiet' '
test_when_finished "git worktree remove -f -f another-worktree" &&
git worktree add --quiet another-worktree main 2>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' '
git clone --local here here-clone &&
( 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_when_finished rm -rf repo_upstream repo_local foo &&
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_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_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 () {
test_when_finished "rm -rf .git/hooks" &&
mkdir .git/hooks &&