diff mbox series

[v8,3/4] worktree add: add --orphan flag

Message ID 20230109173227.29264-4-jacobabel@nullpo.dev (mailing list archive)
State New, archived
Headers show
Series worktree: Support `--orphan` when creating new worktrees | expand

Commit Message

Jacob Abel Jan. 9, 2023, 5:33 p.m. UTC
Adds support for creating an orphan branch when adding a new worktree.
This functionality is equivalent to git switch's --orphan flag.

The original reason this feature was implemented was to allow a user
to initialise a new repository using solely the worktree oriented
workflow.

Current Behavior:
% git -C foo.git --no-pager branch -l
+ main
% git -C foo.git worktree add main/
Preparing worktree (new branch 'main')
HEAD is now at 6c93a75 a commit
%

% git init bar.git
Initialized empty Git repository in /path/to/bar.git/
% git -C bar.git --no-pager branch -l

% git -C bar.git worktree add main/
Preparing worktree (new branch 'main')
fatal: not a valid object name: 'HEAD'
%

New Behavior:

% git -C foo.git --no-pager branch -l
+ main
% git -C foo.git worktree add main/
Preparing worktree (new branch 'main')
HEAD is now at 6c93a75 a commit
%

% git init --bare bar.git
Initialized empty Git repository in /path/to/bar.git/
% git -C bar.git --no-pager branch -l

% git -C bar.git worktree add main/
Preparing worktree (new branch 'main')
fatal: invalid reference: HEAD
% git -C bar.git worktree add --orphan main main/
Preparing worktree (new branch 'main')
%

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Jacob Abel <jacobabel@nullpo.dev>
---
 Documentation/git-worktree.txt | 15 +++++++++
 builtin/worktree.c             | 59 ++++++++++++++++++++++++++++++----
 t/t2400-worktree-add.sh        | 53 ++++++++++++++++++++++++++++++
 3 files changed, 120 insertions(+), 7 deletions(-)

--
2.38.2

Comments

Phillip Wood Jan. 13, 2023, 10:20 a.m. UTC | #1
Hi Jacob

On 09/01/2023 17:33, Jacob Abel wrote:
> Adds support for creating an orphan branch when adding a new worktree.
> This functionality is equivalent to git switch's --orphan flag.
> 
> The original reason this feature was implemented was to allow a user
> to initialise a new repository using solely the worktree oriented
> workflow.
> 
> Current Behavior:
> % git -C foo.git --no-pager branch -l
> + main
> % git -C foo.git worktree add main/
> Preparing worktree (new branch 'main')
> HEAD is now at 6c93a75 a commit
> %
> 
> % git init bar.git
> Initialized empty Git repository in /path/to/bar.git/
> % git -C bar.git --no-pager branch -l
> 
> % git -C bar.git worktree add main/
> Preparing worktree (new branch 'main')
> fatal: not a valid object name: 'HEAD'
> %
> 
> New Behavior:
> 
> % git -C foo.git --no-pager branch -l
> + main
> % git -C foo.git worktree add main/
> Preparing worktree (new branch 'main')
> HEAD is now at 6c93a75 a commit
> %
> 
> % git init --bare bar.git
> Initialized empty Git repository in /path/to/bar.git/
> % git -C bar.git --no-pager branch -l
> 
> % git -C bar.git worktree add main/
> Preparing worktree (new branch 'main')
> fatal: invalid reference: HEAD
> % git -C bar.git worktree add --orphan main main/

It's perhaps a bit late to bring this up but I've only just realized 
that it is unfortunate that --orphan takes a branch name rather than 
working in conjunction with -b/-B. It means that in the common case 
where the branch name is the same as the worktree the user has to repeat 
it on the command line as shown above. It also means there is no way to 
force an orphan branch (that's admittedly quite niche). If instead 
--orphan did not take an argument we could have

	git worktree add --orphan main
	git worktree add --orphan -b topic main
	git worktree add --orphan -B topic main

Best Wishes

Phillip

> Preparing worktree (new branch 'main')
> %
> 
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> Signed-off-by: Jacob Abel <jacobabel@nullpo.dev>
> ---
>   Documentation/git-worktree.txt | 15 +++++++++
>   builtin/worktree.c             | 59 ++++++++++++++++++++++++++++++----
>   t/t2400-worktree-add.sh        | 53 ++++++++++++++++++++++++++++++
>   3 files changed, 120 insertions(+), 7 deletions(-)
> 
> diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
> index b9c12779f1..d78460c29c 100644
> --- a/Documentation/git-worktree.txt
> +++ b/Documentation/git-worktree.txt
> @@ -11,6 +11,8 @@ SYNOPSIS
>   [verse]
>   'git worktree add' [-f] [--detach] [--checkout] [--lock [--reason <string>]]
>   		   [(-b | -B) <new-branch>] <path> [<commit-ish>]
> +'git worktree add' [-f] [--lock [--reason <string>]]
> +		   --orphan <new-branch> <path>
>   'git worktree list' [-v | --porcelain [-z]]
>   'git worktree lock' [--reason <string>] <worktree>
>   'git worktree move' <worktree> <new-path>
> @@ -95,6 +97,15 @@ 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).
> ++
> +------------
> +$ git worktree add --orphan <branch> <path>
> +------------
> ++
> +Create a worktree containing no files, with an empty index, and associated
> +with a new orphan branch named `<branch>`. The first commit made on this new
> +branch will have no parents and will be the root of a new history disconnected
> +from any other branches.
> 
>   list::
> 
> @@ -222,6 +233,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 <new-branch>::
> +	With `add`, make the new worktree and index empty, associating
> +	the worktree with a new orphan 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
> diff --git a/builtin/worktree.c b/builtin/worktree.c
> index ddb33f48a0..ac82c5feda 100644
> --- a/builtin/worktree.c
> +++ b/builtin/worktree.c
> @@ -17,7 +17,10 @@
> 
>   #define BUILTIN_WORKTREE_ADD_USAGE \
>   	N_("git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]]\n" \
> -	   "                 [(-b | -B) <new-branch>] <path> [<commit-ish>]")
> +	   "                 [(-b | -B) <new-branch>] <path> [<commit-ish>]"), \
> +	N_("git worktree add [-f] [--lock [--reason <string>]]\n" \
> +	   "                 --orphan <new-branch> <path>")
> +
>   #define BUILTIN_WORKTREE_LIST_USAGE \
>   	N_("git worktree list [-v | --porcelain [-z]]")
>   #define BUILTIN_WORKTREE_LOCK_USAGE \
> @@ -90,6 +93,7 @@ struct add_opts {
>   	int detach;
>   	int quiet;
>   	int checkout;
> +	int orphan;
>   	const char *keep_locked;
>   };
> 
> @@ -364,6 +368,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)
>   {
> @@ -393,7 +413,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);
> @@ -482,10 +502,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)
> @@ -497,6 +517,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;
> @@ -516,7 +540,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);
> @@ -605,6 +629,7 @@ static int add(int ac, const char **av, const char *prefix)
>   	char *path;
>   	const char *branch;
>   	const char *new_branch = NULL;
> +	const char *orphan_branch = NULL;
>   	const char *opt_track = NULL;
>   	const char *lock_reason = NULL;
>   	int keep_locked = 0;
> @@ -616,6 +641,8 @@ 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_STRING(0, "orphan", &orphan_branch, N_("branch"),
> +			   N_("new unparented 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")),
> @@ -633,8 +660,20 @@ static int add(int ac, const char **av, const char *prefix)
>   	memset(&opts, 0, sizeof(opts));
>   	opts.checkout = 1;
>   	ac = parse_options(ac, av, prefix, options, git_worktree_add_usage, 0);
> +	opts.orphan = !!orphan_branch;
>   	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 + !!new_branch + !!new_branch_force > 1)
> +		die(_("options '%s', '%s', '%s', and '%s' cannot be used together"),
> +		    "-b", "-B", "--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)
> @@ -663,7 +702,9 @@ 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 = orphan_branch;
> +	} else if (ac < 2 && !new_branch && !opts.detach) {
>   		const char *s = dwim_branch(path, &new_branch);
>   		if (s)
>   			branch = s;
> @@ -686,7 +727,11 @@ static int add(int ac, const char **av, const char *prefix)
>   	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 (!lookup_commit_reference_by_name(branch)) {
> +		die(_("invalid reference: %s"), branch);
> +	} else if (new_branch) {
>   		struct child_process cp = CHILD_PROCESS_INIT;
>   		cp.git_cmd = 1;
>   		strvec_push(&cp.args, "branch");
> diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh
> index dd729a00d8..45f896dcd0 100755
> --- a/t/t2400-worktree-add.sh
> +++ b/t/t2400-worktree-add.sh
> @@ -312,6 +312,11 @@ test_wt_add_excl () {
>   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 -B poodle --orphan poodle bamboo
> +test_wt_add_excl -b poodle --orphan poodle bamboo
> +test_wt_add_excl --orphan poodle --detach bamboo
> +test_wt_add_excl --orphan poodle --no-checkout bamboo
> +test_wt_add_excl --orphan poodle bamboo main
> 
>   test_expect_success '"add -B" fails if the branch is checked out' '
>   	git rev-parse newmain >before &&
> @@ -333,6 +338,46 @@ test_expect_success 'add --quiet' '
>   	test_must_be_empty actual
>   '
> 
> +test_expect_success '"add --orphan"' '
> +	test_when_finished "git worktree remove -f -f orphandir" &&
> +	git worktree add --orphan neworphan orphandir &&
> +	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" &&
> +	test_when_finished "git worktree remove -f -f orphandir" &&
> +	git worktree add -b existingbranch orphandir main &&
> +	test_must_fail git worktree add --orphan existingbranch orphandir2 &&
> +	test_path_is_missing orphandir2
> +'
> +
> +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 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 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
> +'
> +
>   test_expect_success 'local clone from linked checkout' '
>   	git clone --local here here-clone &&
>   	( cd here-clone && git fsck )
> @@ -449,6 +494,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 &&
> --
> 2.38.2
> 
>
Junio C Hamano Jan. 13, 2023, 5:32 p.m. UTC | #2
Phillip Wood <phillip.wood123@gmail.com> writes:

>> % git -C bar.git worktree add --orphan main main/
>
> It's perhaps a bit late to bring this up but I've only just realized
> that it is unfortunate that --orphan takes a branch name rather than
> working in conjunction with -b/-B. It means that in the common case
> where the branch name is the same as the worktree the user has to
> repeat it on the command line as shown above. It also means there is
> no way to force an orphan branch (that's admittedly quite niche). If
> instead --orphan did not take an argument we could have
>
> 	git worktree add --orphan main
> 	git worktree add --orphan -b topic main
> 	git worktree add --orphan -B topic main

Good point.  I am not sure if it is too late, though.

Thanks.
Jacob Abel Jan. 14, 2023, 10:47 p.m. UTC | #3
On 23/01/13 10:20AM, Phillip Wood wrote:
> Hi Jacob
>
> On 09/01/2023 17:33, Jacob Abel wrote:
> > [...]
>
> It's perhaps a bit late to bring this up but I've only just realized
> that it is unfortunate that --orphan takes a branch name rather than
> working in conjunction with -b/-B. It means that in the common case
> where the branch name is the same as the worktree the user has to repeat
> it on the command line as shown above. It also means there is no way to
> force an orphan branch (that's admittedly quite niche). If instead
> --orphan did not take an argument we could have
>
> 	git worktree add --orphan main
> 	git worktree add --orphan -b topic main
> 	git worktree add --orphan -B topic main
>
> Best Wishes
>
> Phillip
>
> > [...]

I think this is a good idea and something similar was brought up previously
however I originally wanted to handle this and a common --orphan DWYM in a later
patch.

> 	git worktree add --orphan main

I am OK implementing this option and have been workshopping it prior to
responding. I think I have it worked out now as an additional patch which can be
be applied on top of the v8 patchset.

I'll reply to this message with the one-off patch to get feedback. Since this is
essentially a discrete change on top of v8, I can either keep it as a separate
patch or reroll depending on how much needs to be changed (and what would be
easier for everyone).

> 	git worktree add --orphan -b topic main
> 	git worktree add --orphan -B topic main

I am hesitant to add these as they break away from the syntax used in
`git switch` and `git checkout`.

Also apologies for the tangent but while researching this path, I noticed that
--orphan behaves unexpectedly on both `git switch` and `git checkout` when mixed
with `-c` and `-b` respectively.

    % git switch --orphan -c foobar
    fatal: invalid reference: foobar

    % git switch -c --orphan foobar
    fatal: invalid reference: foobar

    % git checkout -b --orphan foobar
    fatal: 'foobar' is not a commit and a branch '--orphan' cannot be created from it

    % git checkout --orphan -b foobar
    fatal: 'foobar' is not a commit and a branch '-b' cannot be created from it

I tried this on my system install as well as from a fresh fetch of next FWIW.

[Info: fresh build from next]
git version 2.39.0.287.g8cbeef4abd
cpu: x86_64
built from commit: 8cbeef4abda4907dd68ea144d9dcb85f0b49c3e6
sizeof-long: 8
sizeof-size_t: 8
shell-path: /bin/sh

[Info: system install]
git version 2.38.2
cpu: x86_64
no commit associated with this build
sizeof-long: 8
sizeof-size_t: 8
shell-path: /bin/sh

If this bug is something that needs to be addressed, I can dig a bit deeper and
put together a patch for it in the next few days.

VR,
Abel
Junio C Hamano Jan. 15, 2023, 3:09 a.m. UTC | #4
Jacob Abel <jacobabel@nullpo.dev> writes:

>> 	git worktree add --orphan -b topic main
>> 	git worktree add --orphan -B topic main
>
> I am hesitant to add these as they break away from the syntax used in
> `git switch` and `git checkout`.

Not that I care too deeply, but doesn't it introduce end-user
confusion if we try to be compatible with "git checkout --orphan
<branch>", while allowing this to be compatible with the default
choice of the branch name done by "git worktree add"?  "--orphan" in
"git checkout" behaves similar to "-b|-B" in that it always wants a
name, but "git worktree add" wants to make it optional.

By the way "--orphan" in checkout|switch wants to take a name for
itself, e.g.

	git checkout --orphan $name [$commit]
	git checkout -b $name [$commit]
	git checkout -B $name [$commit]

so it is impossible to force their "--orphan" to rename an existing
branch, which is probalby a design mistake we may want to fix.

In any case, as I said, I do not care too deeply which way you guys
decide to go, because I think the whole "orphan" UI is a design
mistake that instills a broken mental model to its users [*].

But let's wait a bit more to see which among

(1) git worktree add [[--orphan] -b $branch] $path
    This allows --orphan to act as a modifier to existing -b,

(2) git worktree add [(--orphan|-b) $branch] $path
    This allows --orphan to be another mode of -b, or

(3) git worktree add [--orphan [$branch]|(-b $branch)] $path
    This allows --orphan to default to $(basename $path)

people prefer.


[Footnote]

* I am not saying that it is wrong or useless to keep an unrelated
  history, especially one that records trees that have no relevance
  to the main history like created with "switch --orphan", in the
  same repository.  Allowing "git switch --orphan" to create such a
  separate history in the same repository blurs the distinction.  It
  would help newbies to form the right mental model if they start a
  separate repository that the separate history originates in, and
  pull from it to bootstrap the unrelated history in the local
  repository.
Randall S. Becker Jan. 15, 2023, 3:41 a.m. UTC | #5
On January 14, 2023 10:09 PM, Junio C Hamano wrote:
>Jacob Abel <jacobabel@nullpo.dev> writes:
>
>>> 	git worktree add --orphan -b topic main
>>> 	git worktree add --orphan -B topic main
>>
>> I am hesitant to add these as they break away from the syntax used in
>> `git switch` and `git checkout`.
>
>Not that I care too deeply, but doesn't it introduce end-user confusion if we try to
>be compatible with "git checkout --orphan <branch>", while allowing this to be
>compatible with the default choice of the branch name done by "git worktree
>add"?  "--orphan" in "git checkout" behaves similar to "-b|-B" in that it always
>wants a name, but "git worktree add" wants to make it optional.
>
>By the way "--orphan" in checkout|switch wants to take a name for itself, e.g.
>
>	git checkout --orphan $name [$commit]
>	git checkout -b $name [$commit]
>	git checkout -B $name [$commit]
>
>so it is impossible to force their "--orphan" to rename an existing branch, which is
>probalby a design mistake we may want to fix.
>
>In any case, as I said, I do not care too deeply which way you guys decide to go,
>because I think the whole "orphan" UI is a design mistake that instills a broken
>mental model to its users [*].
>
>But let's wait a bit more to see which among
>
>(1) git worktree add [[--orphan] -b $branch] $path
>    This allows --orphan to act as a modifier to existing -b,
>
>(2) git worktree add [(--orphan|-b) $branch] $path
>    This allows --orphan to be another mode of -b, or
>
>(3) git worktree add [--orphan [$branch]|(-b $branch)] $path
>    This allows --orphan to default to $(basename $path)
>
>people prefer.
>
>
>[Footnote]
>
>* I am not saying that it is wrong or useless to keep an unrelated
>  history, especially one that records trees that have no relevance
>  to the main history like created with "switch --orphan", in the
>  same repository.  Allowing "git switch --orphan" to create such a
>  separate history in the same repository blurs the distinction.  It
>  would help newbies to form the right mental model if they start a
>  separate repository that the separate history originates in, and
>  pull from it to bootstrap the unrelated history in the local
>  repository.

I am wondering whether --detached is a more semantically consistent option. While --orphan has meaning in checkout (not one I ever liked), detached makes more sense as a description of what is intended here - as in not connected.

--Randall
Junio C Hamano Jan. 15, 2023, 3:49 a.m. UTC | #6
<rsbecker@nexbridge.com> writes:

> I am wondering whether --detached is a more semantically consistent option. While --orphan has meaning in checkout (not one I ever liked), detached makes more sense as a description of what is intended here - as in not connected.

An orphan is not even detached, if I understand correctly.

The state is what is called "being on an unborn branch", where your
HEAD does not even point at any commit.  HEAD only knows a name of a
branch that is not yet created but will be when you make a commit.

While "(HEAD) being detached" means that you are on an existing
commit---it is just that future history you extend by making a
commit from that state will not be on any branch.

So if we wanted to fix the misnomer, s/orphan/unborn/ would be how I
would go about it.
Phillip Wood Jan. 16, 2023, 10:47 a.m. UTC | #7
Hi Jacob

On 14/01/2023 22:47, Jacob Abel wrote:
> On 23/01/13 10:20AM, Phillip Wood wrote:
>> Hi Jacob
>>
>> On 09/01/2023 17:33, Jacob Abel wrote:
>>> [...]
>>
>> It's perhaps a bit late to bring this up but I've only just realized
>> that it is unfortunate that --orphan takes a branch name rather than
>> working in conjunction with -b/-B. It means that in the common case
>> where the branch name is the same as the worktree the user has to repeat
>> it on the command line as shown above. It also means there is no way to
>> force an orphan branch (that's admittedly quite niche). If instead
>> --orphan did not take an argument we could have
>>
>> 	git worktree add --orphan main
>> 	git worktree add --orphan -b topic main
>> 	git worktree add --orphan -B topic main
>>
>> Best Wishes
>>
>> Phillip
>>
>>> [...]
> 
> I think this is a good idea and something similar was brought up previously
> however I originally wanted to handle this and a common --orphan DWYM in a later
> patch.

I think adding it in a later patch makes the implementation more 
complicated than it needs to be as you'll still have to support --orphan 
taking a name for backwards compatibility that means you need to handle

	git worktree add --orphan=main main
	git worktree add --orphan topic main
	git worktree add --orphan --lock main
	git worktree add --orphan -b topic main
	git worktree add --orphan -B topic main

Rather than just the last three. Now you can probably get that to work 
by changing --orphan to take an optional argument but I think it would 
be simpler to have --orphan as a flag from the start.

>> 	git worktree add --orphan main
> 
> I am OK implementing this option and have been workshopping it prior to
> responding. I think I have it worked out now as an additional patch which can be
> be applied on top of the v8 patchset.
> 
> I'll reply to this message with the one-off patch to get feedback. Since this is
> essentially a discrete change on top of v8, I can either keep it as a separate
> patch or reroll depending on how much needs to be changed (and what would be
> easier for everyone).
> 
>> 	git worktree add --orphan -b topic main
>> 	git worktree add --orphan -B topic main
> 
> I am hesitant to add these as they break away from the syntax used in
> `git switch` and `git checkout`.

When I wrote my original email I wrongly though that --orphan did not 
take an argument for "git checkout". While I think it is a mistake for 
checkout and switch to have --orphan take an argument they do at least 
always need a branch name with that option. "git worktree" add already 
has the branch name in the form of the worktree directory in the common 
case.

> Also apologies for the tangent but while researching this path, I noticed that
> --orphan behaves unexpectedly on both `git switch` and `git checkout` when mixed
> with `-c` and `-b` respectively.
> 
>      % git switch --orphan -c foobar
>      fatal: invalid reference: foobar
> 
>      % git switch -c --orphan foobar
>      fatal: invalid reference: foobar
> >      % git checkout -b --orphan foobar
>      fatal: 'foobar' is not a commit and a branch '--orphan' cannot be created from it
>
>      % git checkout --orphan -b foobar
>      fatal: 'foobar' is not a commit and a branch '-b' cannot be created from it

The messages for checkout look better than the switch ones to me as they 
show the branch name which makes it clearer that we're treating what 
looks like an option as an argument. What in particular is unexpected 
here - --orphan and -b take an argument so they'll hoover up the next 
thing on the commandline whatever it is.

Best Wishes

Phillip

> I tried this on my system install as well as from a fresh fetch of next FWIW.
> 
> [Info: fresh build from next]
> git version 2.39.0.287.g8cbeef4abd
> cpu: x86_64
> built from commit: 8cbeef4abda4907dd68ea144d9dcb85f0b49c3e6
> sizeof-long: 8
> sizeof-size_t: 8
> shell-path: /bin/sh
> 
> [Info: system install]
> git version 2.38.2
> cpu: x86_64
> no commit associated with this build
> sizeof-long: 8
> sizeof-size_t: 8
> shell-path: /bin/sh
> 
> If this bug is something that needs to be addressed, I can dig a bit deeper and
> put together a patch for it in the next few days.
> 
> VR,
> Abel
>
Jacob Abel Jan. 18, 2023, 10:18 p.m. UTC | #8
On 23/01/14 07:09PM, Junio C Hamano wrote:
> Jacob Abel <jacobabel@nullpo.dev> writes:
>
> >> 	git worktree add --orphan -b topic main
> >> 	git worktree add --orphan -B topic main
> >
> > I am hesitant to add these as they break away from the syntax used in
> > `git switch` and `git checkout`.
>
> Not that I care too deeply, but doesn't it introduce end-user
> confusion if we try to be compatible with "git checkout --orphan
> <branch>", while allowing this to be compatible with the default
> choice of the branch name done by "git worktree add"?  "--orphan" in
> "git checkout" behaves similar to "-b|-B" in that it always wants a
> name, but "git worktree add" wants to make it optional.

Yes. I think it's a fairly minor degree of confusion but I agree that it adds
potentially unneeded confusion.

>
> By the way "--orphan" in checkout|switch wants to take a name for
> itself, e.g.
>
> 	git checkout --orphan $name [$commit]
> 	git checkout -b $name [$commit]
> 	git checkout -B $name [$commit]
>
> so it is impossible to force their "--orphan" to rename an existing
> branch, which is probalby a design mistake we may want to fix.

Can you elaborate on what you mean by "rename an existing branch" here?

Do you mean like `git checkout --orphan $branchname` being able to convert an
existing branch into an orphan/unborn branch?

Also a small point but in an earlier thread [1], we made the decision to model
functionality on `git switch --orphan $branch` instead of
`git checkout --orphan $branch [$commit]`.

>
> In any case, as I said, I do not care too deeply which way you guys
> decide to go, because I think the whole "orphan" UI is a design
> mistake that instills a broken mental model to its users [*].

Understood.

>
> But let's wait a bit more to see which among
>
> (1) git worktree add [[--orphan] -b $branch] $path
>     This allows --orphan to act as a modifier to existing -b,
>
> (2) git worktree add [(--orphan|-b) $branch] $path
>     This allows --orphan to be another mode of -b, or
>
> (3) git worktree add [--orphan [$branch]|(-b $branch)] $path
>     This allows --orphan to default to $(basename $path)
>
> people prefer.
>

I'd personally argue that option 2 (the current behavior) is probably the
cleanest path forward as option 3 requires a bit of awkward code [2] and
`--orphan` is such an esoteric option that the user may only use it once or
twice in the life of a given repository, if that.

And eventually I'd like `git worktree add $path` to "just work" on a new/empty
repository. However as things stand, there wasn't an easy way to do this without
leading to potentially confusing behavior. It can be done, I just haven't taken
the time to figure it out yet.

Once `git worktree add $path` "just works" (when creating the first branch in a
repo), I highly doubt anyone would use `--orphan` often enough to justify the
use of shorthand options 1 or 3.

>
> [Footnote]
>
> * I am not saying that it is wrong or useless to keep an unrelated
>   history, especially one that records trees that have no relevance
>   to the main history like created with "switch --orphan", in the
>   same repository.  Allowing "git switch --orphan" to create such a
>   separate history in the same repository blurs the distinction.  It
>   would help newbies to form the right mental model if they start a
>   separate repository that the separate history originates in, and
>   pull from it to bootstrap the unrelated history in the local
>   repository.

Definitely agreed that `--orphan` is esoteric and probably should be avoided by
most users where possible.

1. https://lore.kernel.org/git/CAPig+cSVzewXpk+eDSC-W-+Q8X_7ikZXXeSQbmpHBcdLCU5svw@mail.gmail.com/
2. https://lore.kernel.org/git/20230114224956.24801-1-jacobabel@nullpo.dev/
Jacob Abel Jan. 18, 2023, 10:40 p.m. UTC | #9
On 23/01/16 10:47AM, Phillip Wood wrote:
> Hi Jacob
>
> On 14/01/2023 22:47, Jacob Abel wrote:
> > On 23/01/13 10:20AM, Phillip Wood wrote:
> >> Hi Jacob
> >>
> > [...]
> >
> > I'll reply to this message with the one-off patch to get feedback. Since this is
> > essentially a discrete change on top of v8, I can either keep it as a separate
> > patch or reroll depending on how much needs to be changed (and what would be
> > easier for everyone).
> >
> >> 	git worktree add --orphan -b topic main
> >> 	git worktree add --orphan -B topic main
> >
> > I am hesitant to add these as they break away from the syntax used in
> > `git switch` and `git checkout`.
>
> When I wrote my original email I wrongly though that --orphan did not
> take an argument for "git checkout". While I think it is a mistake for
> checkout and switch to have --orphan take an argument they do at least
> always need a branch name with that option. "git worktree" add already
> has the branch name in the form of the worktree directory in the common
> case.

Understood.

I'm not entirely opposed to making this change to OPT_BOOL but I have to wonder
how often `--orphan` will actually be used by a given user and whether the
slightly shorter invocation will be used regularly.

With the base `git worktree add $path`, the shorthand/DWYM makes sense as it's
used regularly but I don't see users working with `--orphan` outside of trying
to create the first branch in a repository.

And I'd like that operation of creating the first branch in a repo to eventually
"just work" with the base command, i.e. `git worktree add main/`. The reason I
hadn't yet added that is because I've yet to figure out how to get it to work
without accidentally introducing potentially confusing situations and I didn't
want to hold up introducing the core functionality itself.

Once that main use-case "just works", I don't see users utilising `--orphan`
except in very rare circumstances. Doubly so since the average user likely
shouldn't be using `--orphan` in most cases.

Hence the question of whether this change would be worth it vs the existing
`--orphan $branchname $path` which is (for better or worse) consistent with `-b`
and `-B`.

>
> > Also apologies for the tangent but while researching this path, I noticed that
> > --orphan behaves unexpectedly on both `git switch` and `git checkout` when mixed
> > with `-c` and `-b` respectively.
> >
> >      % git switch --orphan -c foobar
> >      fatal: invalid reference: foobar
> >
> >      % git switch -c --orphan foobar
> >      fatal: invalid reference: foobar
> >      % git checkout -b --orphan foobar
> >      fatal: 'foobar' is not a commit and a branch '--orphan' cannot be created from it
> >
> >      % git checkout --orphan -b foobar
> >      fatal: 'foobar' is not a commit and a branch '-b' cannot be created from it
>
> The messages for checkout look better than the switch ones to me as they
> show the branch name which makes it clearer that we're treating what
> looks like an option as an argument. What in particular is unexpected
> here - --orphan and -b take an argument so they'll hoover up the next
> thing on the commandline whatever it is.
>
> Best Wishes
>
> Phillip
>
> > [...]

Agreed. I wasn't sure if this would be something worth addressing in a patch but
at the very least I can work on putting together a small patch for `git switch`
since it doesn't seem to be hoovering the flags like `git checkout` does.
Jacob Abel Jan. 18, 2023, 10:46 p.m. UTC | #10
On 23/01/14 07:49PM, Junio C Hamano wrote:
> <rsbecker@nexbridge.com> writes:
>
> > [...]
>
> An orphan is not even detached, if I understand correctly.
>
> The state is what is called "being on an unborn branch", where your
> HEAD does not even point at any commit.  HEAD only knows a name of a
> branch that is not yet created but will be when you make a commit.
>
> While "(HEAD) being detached" means that you are on an existing
> commit---it is just that future history you extend by making a
> commit from that state will not be on any branch.
>
> So if we wanted to fix the misnomer, s/orphan/unborn/ would be how I
> would go about it.
>

I would support making this change (s/orphan/unborn/) as it's definitely less
confusing. Especially given that orphan already has a completely different,
overloaded meaning when referring to orphaned objects & commits (in the context
of garbage collection).
Ævar Arnfjörð Bjarmason Jan. 19, 2023, 3:32 p.m. UTC | #11
On Wed, Jan 18 2023, Jacob Abel wrote:

> On 23/01/14 07:09PM, Junio C Hamano wrote:
>> Jacob Abel <jacobabel@nullpo.dev> writes:
>>
>> >> 	git worktree add --orphan -b topic main
>> >> 	git worktree add --orphan -B topic main
>> >
>> > I am hesitant to add these as they break away from the syntax used in
>> > `git switch` and `git checkout`.
>>
>> Not that I care too deeply, but doesn't it introduce end-user
>> confusion if we try to be compatible with "git checkout --orphan
>> <branch>", while allowing this to be compatible with the default
>> choice of the branch name done by "git worktree add"?  "--orphan" in
>> "git checkout" behaves similar to "-b|-B" in that it always wants a
>> name, but "git worktree add" wants to make it optional.
>
> Yes. I think it's a fairly minor degree of confusion but I agree that it adds
> potentially unneeded confusion.

I think this topic is ready to advance as-is without Phillip's upthread
suggestion (<e5aadd5d-9b85-4dc9-e9f7-117892b4b283@dunelm.org.uk>) to
allow us to combine --orphan and -b and -B.

I also think that UX suggestion is sensible, but if we do that we
shouldn't just apply that to "git worktree", but also change the the
corresponding "git switch" UX, on which this new "git worktree --orphan"
is modeled.

I don't think it's worth it to make the UX between the two inconsistent
in this regard, so if "switch" doesn't learn to do this we'd be better
off with not making "--orphan" a flag.

But if we are going to make it a flag let's have both support the same
sort of invocation.

Therefore I think this series is ready as-is without this proposed UX
change. We should first support the same sort of invocations that
"swich" already supports.

If we then want to change the UX later we should change it for both, not
leave the two inconsistent.
Phillip Wood Jan. 19, 2023, 4:18 p.m. UTC | #12
On 18/01/2023 22:40, Jacob Abel wrote:
> On 23/01/16 10:47AM, Phillip Wood wrote:
>> Hi Jacob
>>
>> On 14/01/2023 22:47, Jacob Abel wrote:
>>> On 23/01/13 10:20AM, Phillip Wood wrote:
>>>> Hi Jacob
>>>>
>>> [...]
>>>
>>> I'll reply to this message with the one-off patch to get feedback. Since this is
>>> essentially a discrete change on top of v8, I can either keep it as a separate
>>> patch or reroll depending on how much needs to be changed (and what would be
>>> easier for everyone).
>>>
>>>> 	git worktree add --orphan -b topic main
>>>> 	git worktree add --orphan -B topic main
>>>
>>> I am hesitant to add these as they break away from the syntax used in
>>> `git switch` and `git checkout`.
>>
>> When I wrote my original email I wrongly though that --orphan did not
>> take an argument for "git checkout". While I think it is a mistake for
>> checkout and switch to have --orphan take an argument they do at least
>> always need a branch name with that option. "git worktree" add already
>> has the branch name in the form of the worktree directory in the common
>> case.
> 
> Understood.
> 
> I'm not entirely opposed to making this change to OPT_BOOL but I have to wonder
> how often `--orphan` will actually be used by a given user and whether the
> slightly shorter invocation will be used regularly.
> 
> With the base `git worktree add $path`, the shorthand/DWYM makes sense as it's
> used regularly but I don't see users working with `--orphan` outside of trying
> to create the first branch in a repository.

Your example use in the commit message shows the user using the same 
name for the branch and worktree. If that really is the likely use than 
I think we should make --orphan OPT_BOOL. If it is not the likely use 
perhaps you could update the commit message to show how you think it 
will be used.

> And I'd like that operation of creating the first branch in a repo to eventually
> "just work" with the base command, i.e. `git worktree add main/`. The reason I
> hadn't yet added that is because I've yet to figure out how to get it to work
> without accidentally introducing potentially confusing situations and I didn't
> want to hold up introducing the core functionality itself.
> 
> Once that main use-case "just works", I don't see users utilising `--orphan`
> except in very rare circumstances. Doubly so since the average user likely
> shouldn't be using `--orphan` in most cases.

This brings us back to the question that we discussed earlier of whether 
we need --orphan at all. If we can get the main use-case working without 
it we'd perhaps be better doing that rather than adding an option no one 
ends up using.

Best Wishes

Phillip

> Hence the question of whether this change would be worth it vs the existing
> `--orphan $branchname $path` which is (for better or worse) consistent with `-b`
> and `-B`.
> 
>>
>>> Also apologies for the tangent but while researching this path, I noticed that
>>> --orphan behaves unexpectedly on both `git switch` and `git checkout` when mixed
>>> with `-c` and `-b` respectively.
>>>
>>>       % git switch --orphan -c foobar
>>>       fatal: invalid reference: foobar
>>>
>>>       % git switch -c --orphan foobar
>>>       fatal: invalid reference: foobar
>>>       % git checkout -b --orphan foobar
>>>       fatal: 'foobar' is not a commit and a branch '--orphan' cannot be created from it
>>>
>>>       % git checkout --orphan -b foobar
>>>       fatal: 'foobar' is not a commit and a branch '-b' cannot be created from it
>>
>> The messages for checkout look better than the switch ones to me as they
>> show the branch name which makes it clearer that we're treating what
>> looks like an option as an argument. What in particular is unexpected
>> here - --orphan and -b take an argument so they'll hoover up the next
>> thing on the commandline whatever it is.
>>
>> Best Wishes
>>
>> Phillip
>>
>>> [...]
> 
> Agreed. I wasn't sure if this would be something worth addressing in a patch but
> at the very least I can work on putting together a small patch for `git switch`
> since it doesn't seem to be hoovering the flags like `git checkout` does.
>
Junio C Hamano Jan. 19, 2023, 4:32 p.m. UTC | #13
Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> I also think that UX suggestion is sensible, but if we do that we
> shouldn't just apply that to "git worktree", but also change the the
> corresponding "git switch" UX, on which this new "git worktree --orphan"
> is modeled.

But the thing is that "worktree --orphan" that wants to implicitly
infer the name of the branch out of the basename of the worktree
cannot be sensibly modeled after "switch" or "checkout" that do not
have such a dwimmery.

In any case, fixing UI mistakes after the fact is always painful
once it is in the released version. When we know there is a new UI
mistake in an unreleased version, it is sensible to grab the rare
opportunity that we can avoid such a costly fixes later.

So, I am not quite convinced by what you said, at least not yet.

Thanks.
Jacob Abel Jan. 19, 2023, 10:20 p.m. UTC | #14
On 23/01/19 04:18PM, Phillip Wood wrote:
> On 18/01/2023 22:40, Jacob Abel wrote:
> > [...]
> > Understood.
> >
> > I'm not entirely opposed to making this change to OPT_BOOL but I have to wonder
> > how often `--orphan` will actually be used by a given user and whether the
> > slightly shorter invocation will be used regularly.
> >
> > With the base `git worktree add $path`, the shorthand/DWYM makes sense as it's
> > used regularly but I don't see users working with `--orphan` outside of trying
> > to create the first branch in a repository.
>
> Your example use in the commit message shows the user using the same
> name for the branch and worktree. If that really is the likely use than
> I think we should make --orphan OPT_BOOL. If it is not the likely use
> perhaps you could update the commit message to show how you think it
> will be used.

The example in the commit message is mostly a trivial example to show the
change. I think whether someone uses the same name for the branch and worktree
depends on how they use the feature. At least personally, generally I name my
worktree based on either the relevant project/"hat" or using an "a/, b/, c/" or
"alpha/, beta/, gamma/" style.

So in my personal use, it'd look something like:

    git worktree add --orphan main alpha/

---

Or to occassionally break out a scratchpad that I may want to keep around for a
while but not actually integrate into the feature branch in it's current form.
This isn't really for code but rather so I can scratch out and document my logic
prior to doing a formal write up at the end of a given feature. Of course since
it doesn't make it into the main tree, when everything is written up, I can just
toss the branch and let gc take care of it.

This ends up looking like so:

    git worktree add --orphan scratch-1234-foobar-feature scratchpad/

And since `worktree add` only needs to be done once, I only do this the first
time I set up my dev environment on a machine. After that I can just use
`git switch --orphan` to create new scratchpad branches and `git switch` to
swap between existing scratchpads.

---

The first example I see as being the main use case (which could hopefully be
DWYMed eventually) and the latter example is a quirk of my admittedly niche
personal usecase for worktrees.

> > And I'd like that operation of creating the first branch in a repo to eventually
> > "just work" with the base command, i.e. `git worktree add main/`. The reason I
> > hadn't yet added that is because I've yet to figure out how to get it to work
> > without accidentally introducing potentially confusing situations and I didn't
> > want to hold up introducing the core functionality itself.
> >
> > Once that main use-case "just works", I don't see users utilising `--orphan`
> > except in very rare circumstances. Doubly so since the average user likely
> > shouldn't be using `--orphan` in most cases.
>
> This brings us back to the question that we discussed earlier of whether
> we need --orphan at all. If we can get the main use-case working without
> it we'd perhaps be better doing that rather than adding an option no one
> ends up using.

At least personally, I'd rather expose the option for users who may potentially
want it. I think it'd be useful regardless but in the same way `--orphan` is
currently useful in `git switch`, which is for very specific niche cases and
really only for power users rather than as a common tool everyday users are
expected to know.

And for the main use case, my concerns were:

1. If we DWYM and create a branch when there are no existing branches, what
about the case where a user sets up the repo but forgot to fetch. i.e. if a user
does:

    # Or instead of init --template, use some language's project init tool to
    # setup git hooks.
    % git init --bare --template <tmpldir> .git
    % git remote add origin <remote>
    % git worktree add main/

Should we just warn the user that they haven't fetched their remote yet while we
prepare the worktree?

2. Suppose we also check whether the remote has been fetched and the remote has
a branch matching the current branch name.

Should we fail (as it currently does on main) with an advise to try the command
`git worktree add main main` instead? Or should that command also "just work"

3. If we want to do the above, should it do this for all commands trying to
create a worktree until at least one real branch (with commits) exists in the
repo or should we only do this when the branch name matches the one defined in
`init.defaultBranch`?

> Best Wishes
>
> Phillip
>
> > [...]
diff mbox series

Patch

diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index b9c12779f1..d78460c29c 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -11,6 +11,8 @@  SYNOPSIS
 [verse]
 'git worktree add' [-f] [--detach] [--checkout] [--lock [--reason <string>]]
 		   [(-b | -B) <new-branch>] <path> [<commit-ish>]
+'git worktree add' [-f] [--lock [--reason <string>]]
+		   --orphan <new-branch> <path>
 'git worktree list' [-v | --porcelain [-z]]
 'git worktree lock' [--reason <string>] <worktree>
 'git worktree move' <worktree> <new-path>
@@ -95,6 +97,15 @@  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).
++
+------------
+$ git worktree add --orphan <branch> <path>
+------------
++
+Create a worktree containing no files, with an empty index, and associated
+with a new orphan branch named `<branch>`. The first commit made on this new
+branch will have no parents and will be the root of a new history disconnected
+from any other branches.

 list::

@@ -222,6 +233,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 <new-branch>::
+	With `add`, make the new worktree and index empty, associating
+	the worktree with a new orphan 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
diff --git a/builtin/worktree.c b/builtin/worktree.c
index ddb33f48a0..ac82c5feda 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -17,7 +17,10 @@ 

 #define BUILTIN_WORKTREE_ADD_USAGE \
 	N_("git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]]\n" \
-	   "                 [(-b | -B) <new-branch>] <path> [<commit-ish>]")
+	   "                 [(-b | -B) <new-branch>] <path> [<commit-ish>]"), \
+	N_("git worktree add [-f] [--lock [--reason <string>]]\n" \
+	   "                 --orphan <new-branch> <path>")
+
 #define BUILTIN_WORKTREE_LIST_USAGE \
 	N_("git worktree list [-v | --porcelain [-z]]")
 #define BUILTIN_WORKTREE_LOCK_USAGE \
@@ -90,6 +93,7 @@  struct add_opts {
 	int detach;
 	int quiet;
 	int checkout;
+	int orphan;
 	const char *keep_locked;
 };

@@ -364,6 +368,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)
 {
@@ -393,7 +413,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);
@@ -482,10 +502,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)
@@ -497,6 +517,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;
@@ -516,7 +540,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);
@@ -605,6 +629,7 @@  static int add(int ac, const char **av, const char *prefix)
 	char *path;
 	const char *branch;
 	const char *new_branch = NULL;
+	const char *orphan_branch = NULL;
 	const char *opt_track = NULL;
 	const char *lock_reason = NULL;
 	int keep_locked = 0;
@@ -616,6 +641,8 @@  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_STRING(0, "orphan", &orphan_branch, N_("branch"),
+			   N_("new unparented 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")),
@@ -633,8 +660,20 @@  static int add(int ac, const char **av, const char *prefix)
 	memset(&opts, 0, sizeof(opts));
 	opts.checkout = 1;
 	ac = parse_options(ac, av, prefix, options, git_worktree_add_usage, 0);
+	opts.orphan = !!orphan_branch;
 	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 + !!new_branch + !!new_branch_force > 1)
+		die(_("options '%s', '%s', '%s', and '%s' cannot be used together"),
+		    "-b", "-B", "--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)
@@ -663,7 +702,9 @@  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 = orphan_branch;
+	} else if (ac < 2 && !new_branch && !opts.detach) {
 		const char *s = dwim_branch(path, &new_branch);
 		if (s)
 			branch = s;
@@ -686,7 +727,11 @@  static int add(int ac, const char **av, const char *prefix)
 	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 (!lookup_commit_reference_by_name(branch)) {
+		die(_("invalid reference: %s"), branch);
+	} else if (new_branch) {
 		struct child_process cp = CHILD_PROCESS_INIT;
 		cp.git_cmd = 1;
 		strvec_push(&cp.args, "branch");
diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh
index dd729a00d8..45f896dcd0 100755
--- a/t/t2400-worktree-add.sh
+++ b/t/t2400-worktree-add.sh
@@ -312,6 +312,11 @@  test_wt_add_excl () {
 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 -B poodle --orphan poodle bamboo
+test_wt_add_excl -b poodle --orphan poodle bamboo
+test_wt_add_excl --orphan poodle --detach bamboo
+test_wt_add_excl --orphan poodle --no-checkout bamboo
+test_wt_add_excl --orphan poodle bamboo main

 test_expect_success '"add -B" fails if the branch is checked out' '
 	git rev-parse newmain >before &&
@@ -333,6 +338,46 @@  test_expect_success 'add --quiet' '
 	test_must_be_empty actual
 '

+test_expect_success '"add --orphan"' '
+	test_when_finished "git worktree remove -f -f orphandir" &&
+	git worktree add --orphan neworphan orphandir &&
+	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" &&
+	test_when_finished "git worktree remove -f -f orphandir" &&
+	git worktree add -b existingbranch orphandir main &&
+	test_must_fail git worktree add --orphan existingbranch orphandir2 &&
+	test_path_is_missing orphandir2
+'
+
+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 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 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
+'
+
 test_expect_success 'local clone from linked checkout' '
 	git clone --local here here-clone &&
 	( cd here-clone && git fsck )
@@ -449,6 +494,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 &&