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
6 changed files with 735 additions and 21 deletions

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 @@ done:
* 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");