mirror of
https://github.com/git/git
synced 2024-07-17 11:07:55 +00:00
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:
commit
4dd0469328
|
@ -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.
|
||||
--
|
||||
|
|
|
@ -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
|
||||
|
|
1
advice.c
1
advice.c
|
@ -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[] =
|
||||
|
|
1
advice.h
1
advice.h
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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 &&
|
||||
|
|
Loading…
Reference in a new issue