Subject: Re: New orphan worktree?

From: Ævar Arnfjörð Bjarmason <[email-masked]>
Date: Thu, 18 Feb 2021 02:26:27 +0100

On Wed, Jan 06 2021, Eric Sunshine wrote:

> On Wed, Jan 6, 2021 at 2:41 PM Elijah Newren <[email-masked]> wrote:
>> On Wed, Jan 6, 2021 at 8:21 AM Stefan Monnier <[email-masked]> wrote:
>> > I wish I could just do something like:
>> >
>> >     git worktree add --orphan foo newbranch
>>
>> Out of curiosity, why are you frequently creating orphan branches?
>> Such an option would drop the number of commands you need to run from
>> four down to two, but I'm surprised this would come up enough to
>> matter enough to do much more than create a personal git alias for it.
>
> I'm also curious about a use-case in which orphaned branches are used
> so frequently.
>
> Nevertheless, I'm not at all opposed to seeing an --orphan option
> added to `git worktree add`. In fact, --orphan has been on the
> radar[1] from the very beginning. Although the original implementation
> of `git worktree add` mimicked a small set of options from
> git-checkout, the plan all along was to mimic additional options from
> git-checkout as demand for them arose, and several such options have
> already been added.
>
>> Yeah, checkout --orphan is broken.  You should use switch --orphan,
>> which doesn't require the extra 'git rm -rf .' step.  That bit is
>> definitely cumbersome.
>
> Yep, when/if --orphan is added to `git worktree add`, it should mimic
> the behavior of --orphan in git-switch rather than git-checkout.

How would a mode for "worktree add --orphan" that mimics checkout rather
than switch even look like? The "checkout --orphan" special-case is
because we retain the index, so you need to "git rm -rf .".

But with worktrees we always get a new index, so AFAICT the only way to
make it work like "checkout" would be to have it be the only mode that
copies over the current worktree's index.

In any case I implemented a rough version of this today, and it uses the
"switch" semantics. I only discovered this ML thread afterwards.

It's surely full of bugs, and needs test work (see all the BUG(...)),
but if someone's interested in taking it further all it should need is
some more tests & dealing with the edge cases of incompatible options
etc. It's Signed-off-by: Ævar Arnfjörð Bjarmason <[email-masked]>.

I'd tried crafting as simple worktree manually, and discovered that I
could get worktree_add() to create it rather easily by jumping past some
of the "commit exists?" checks.

diff --git a/builtin/worktree.c b/builtin/worktree.c
index 1cd5c2016e3..eeecb6da380 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -31,6 +31,7 @@ struct add_opts {
 	int quiet;
 	int checkout;
 	int keep_locked;
+	int orphan;
 };
 
 static int show_only;
@@ -266,8 +267,12 @@ 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 (opts->orphan) {
+		if (commit)
+			die(_("valid reference: %s"), refname);
+	} else if  (!commit) {
 		die(_("invalid reference: %s"), refname);
+	}
 
 	name = worktree_basename(path, &len);
 	strbuf_add(&sb, name, path + len - name);
@@ -340,7 +345,7 @@ 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 (!opts->orphan && !is_branch)
 		strvec_pushl(&cp.args, "update-ref", "HEAD",
 			     oid_to_hex(&commit->object.oid), NULL);
 	else {
@@ -414,18 +419,28 @@ static int add_worktree(const char *path, const char *refname,
 static void print_preparing_worktree_line(int detach,
 					  const char *branch,
 					  const char *new_branch,
-					  int force_new_branch)
+					  int force_new_branch,
+					  int orphan)
 {
 	if (force_new_branch) {
 		struct commit *commit = lookup_commit_reference_by_name(new_branch);
-		if (!commit)
-			printf_ln(_("Preparing worktree (new branch '%s')"), new_branch);
-		else
+		if (!commit) {
+			if (orphan)
+				printf_ln(_("Preparing worktree (new orphan branch '%s')"), new_branch);
+			else
+				printf_ln(_("Preparing worktree (new branch '%s')"), new_branch);
+		} else {
+			if (orphan)
+				BUG("TODO");
 			printf_ln(_("Preparing worktree (resetting branch '%s'; was at %s)"),
 				  new_branch,
 				  find_unique_abbrev(&commit->object.oid, DEFAULT_ABBREV));
+		}
 	} else if (new_branch) {
-		printf_ln(_("Preparing worktree (new branch '%s')"), new_branch);
+		if (orphan)
+			printf_ln(_("Preparing worktree (new orphan branch '%s')"), new_branch);
+		else
+			printf_ln(_("Preparing worktree (new branch '%s')"), new_branch);
 	} else {
 		struct strbuf s = STRBUF_INIT;
 		if (!detach && !strbuf_check_branch_ref(&s, branch) &&
@@ -486,6 +501,7 @@ static int add(int ac, const char **av, const char *prefix)
 		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", &opts.keep_locked, N_("keep the new working tree locked")),
+		OPT_BOOL(0, "orphan", &opts.orphan, N_("new unparented branch")),
 		OPT__QUIET(&opts.quiet, N_("suppress progress reporting")),
 		OPT_PASSTHRU(0, "track", &opt_track, NULL,
 			     N_("set up tracking mode (see git-branch(1))"),
@@ -505,6 +521,8 @@ static int add(int ac, const char **av, const char *prefix)
 
 	path = prefix_filename(prefix, av[0]);
 	branch = ac < 2 ? "HEAD" : av[1];
+	if (opts.orphan)
+		branch = "";
 
 	if (!strcmp(branch, "-"))
 		branch = "@{-1}";
@@ -542,9 +560,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);
+		print_preparing_worktree_line(opts.detach, branch, new_branch, !!new_branch_force, opts.orphan);
 
-	if (new_branch) {
+	if (opts.orphan && new_branch) {
+		branch = new_branch;
+	} else if (new_branch) {
 		struct child_process cp = CHILD_PROCESS_INIT;
 		cp.git_cmd = 1;
 		strvec_push(&cp.args, "branch");
@@ -560,6 +580,8 @@ static int add(int ac, const char **av, const char *prefix)
 			return -1;
 		branch = new_branch;
 	} else if (opt_track) {
+		if (opts.orphan)
+			BUG("TODO");
 		die(_("--[no-]track can only be used if a new branch is created"));
 	}
 
diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh
index 96dfca15542..e52aa6a11a2 100755
--- a/t/t2400-worktree-add.sh
+++ b/t/t2400-worktree-add.sh
@@ -66,6 +66,19 @@ test_expect_success '"add" worktree' '
 	)
 '
 
+test_expect_success '"add" worktree orphan branch' '
+	git worktree add --orphan -b orphan here-orphan &&
+	echo refs/heads/orphan >expect &&
+	git -C here-orphan symbolic-ref HEAD >actual &&
+	test_cmp expect actual &&
+	test_must_fail git -C here-orphan rev-parse --verify HEAD &&
+	test_must_fail git -C here-orphan log &&
+
+	echo here-orphan/.git >expected &&
+	find here-orphan -type f >actual &&
+	test_cmp expected actual
+'
+
 test_expect_success '"add" worktree with lock' '
 	git rev-parse HEAD >expect &&
 	git worktree add --detach --lock here-with-lock main &&