worktree add: extend DWIM to infer --orphan
Extend DWIM to try to infer `--orphan` when in an empty repository. i.e. a repository with an invalid/unborn HEAD, no local branches, and if `--guess-remote` is used then no remote branches. This behavior is equivalent to `git switch -c` or `git checkout -b` in an empty repository. Also warn the user (overriden with `-f`/`--force`) when they likely intend to checkout a remote branch to the worktree but have not yet fetched from the remote. i.e. when using `--guess-remote` and there is a remote but no local or remote refs. Current Behavior: % git --no-pager branch --list --remotes % git remote origin % git workree add ../main hint: If you meant to create a worktree containing a new orphan branch [...] hint: Disable this message with "git config advice.worktreeAddOrphan false" fatal: invalid reference: HEAD % git workree add --guess-remote ../main hint: If you meant to create a worktree containing a new orphan branch [...] hint: Disable this message with "git config advice.worktreeAddOrphan false" fatal: invalid reference: HEAD % git fetch --quiet % git --no-pager branch --list --remotes origin/HEAD -> origin/main origin/main % git workree add --guess-remote ../main Preparing worktree (new branch 'main') branch 'main' set up to track 'origin/main'. HEAD is now atmaintdadc8e6dac
commit message % New Behavior: % git --no-pager branch --list --remotes % git remote origin % git workree add ../main No possible source branch, inferring '--orphan' Preparing worktree (new branch 'main') % git worktree remove ../main % git workree add --guess-remote ../main fatal: No local or remote refs exist despite at least one remote present, stopping; use 'add -f' to overide or fetch a remote first % git workree add --guess-remote -f ../main No possible source branch, inferring '--orphan' Preparing worktree (new branch 'main') % git worktree remove ../main % git fetch --quiet % git --no-pager branch --list --remotes origin/HEAD -> origin/main origin/main % git workree add --guess-remote ../main Preparing worktree (new branch 'main') branch 'main' set up to track 'origin/main'. HEAD is now atdadc8e6dac
commit message % Signed-off-by: Jacob Abel <jacobabel@nullpo.dev> Signed-off-by: Junio C Hamano <gitster@pobox.com>
parent
35f0383ca6
commit
128e5496b3
|
@ -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::
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
#include "strvec.h"
|
||||
#include "branch.h"
|
||||
#include "refs.h"
|
||||
#include "remote.h"
|
||||
#include "run-command.h"
|
||||
#include "hook.h"
|
||||
#include "sigchain.h"
|
||||
|
@ -40,6 +41,9 @@
|
|||
#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" \
|
||||
|
@ -613,6 +617,107 @@ 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.
|
||||
*
|
||||
* 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)) {
|
||||
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) {
|
||||
if (!opts->quiet)
|
||||
fprintf_ln(stderr, WORKTREE_ADD_DWIM_ORPHAN_INFER_TEXT);
|
||||
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"));
|
||||
} else if (!opts->quiet) {
|
||||
fprintf_ln(stderr, WORKTREE_ADD_DWIM_ORPHAN_INFER_TEXT);
|
||||
}
|
||||
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;
|
||||
|
@ -723,12 +828,19 @@ static int add(int ac, const char **av, const char *prefix)
|
|||
int n;
|
||||
const char *s = worktree_basename(path, &n);
|
||||
new_branch = xstrndup(s, n);
|
||||
} else if (new_branch || opts.detach || opts.orphan) {
|
||||
} else if (opts.orphan || opts.detach) {
|
||||
// No-op
|
||||
} 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;
|
||||
|
||||
// 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;
|
||||
|
|
|
@ -705,6 +705,332 @@ 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_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'
|
||||
;;
|
||||
*)
|
||||
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 or -b
|
||||
"no_-b")
|
||||
use_new_branch=0 &&
|
||||
context="$context, no --branch"
|
||||
;;
|
||||
"-b")
|
||||
use_new_branch=1 &&
|
||||
context="$context, --branch"
|
||||
;;
|
||||
|
||||
# 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"
|
||||
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
|
||||
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 New Issue