worktree add: add --orphan flag

Add support for creating an orphan branch when adding a new worktree. The functionality of this flag is equivalent to git switch's --orphan option. 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 -b main/ Preparing worktree (new branch 'main') % git -C bar.git worktree add --orphan -b newbranch worktreedir/ Preparing worktree (new branch 'newbranch') % Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com> Signed-off-by: Jacob Abel <jacobabel@nullpo.dev> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jacob Abel committed May 17, 2023 at 21:48 UTC 7ab891898524abbeb2c52f3f55fcf5e2457f930b
3 files changed +129 -12
Documentation/git-worktree.txt
+5 -1
@@ -10,7 +10,7 @@ SYNOPSIS
10 --------
11 [verse]
12 'git worktree add' [-f] [--detach] [--checkout] [--lock [--reason <string>]]
13 - [(-b | -B) <new-branch>] <path> [<commit-ish>]
13 + [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]
14 'git worktree list' [-v | --porcelain [-z]]
15 'git worktree lock' [--reason <string>] <worktree>
16 'git worktree move' <worktree> <new-path>
@@ -222,6 +222,10 @@ This can also be set up as the default behaviour by using the
222 With `prune`, do not remove anything; just report what it would
223 remove.
224
225 +--orphan::
226 + With `add`, make the new worktree and index empty, associating
227 + the worktree with a new orphan/unborn branch named `<new-branch>`.
228 +
229 --porcelain::
230 With `list`, output in an easy-to-parse format for scripts.
231 This format will remain stable across Git versions and regardless of user
builtin/worktree.c
+56 -11
@@ -22,7 +22,8 @@
22
23 #define BUILTIN_WORKTREE_ADD_USAGE \
24 N_("git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]]\n" \
25 - " [(-b | -B) <new-branch>] <path> [<commit-ish>]")
25 + " [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]")
26 +
27 #define BUILTIN_WORKTREE_LIST_USAGE \
28 N_("git worktree list [-v | --porcelain [-z]]")
29 #define BUILTIN_WORKTREE_LOCK_USAGE \
@@ -95,6 +96,7 @@ struct add_opts {
96 int detach;
97 int quiet;
98 int checkout;
99 + int orphan;
100 const char *keep_locked;
101 };
102
@@ -368,6 +370,22 @@ static int checkout_worktree(const struct add_opts *opts,
370 return run_command(&cp);
371 }
372
373 +static int make_worktree_orphan(const char * ref, const struct add_opts *opts,
374 + struct strvec *child_env)
375 +{
376 + struct strbuf symref = STRBUF_INIT;
377 + struct child_process cp = CHILD_PROCESS_INIT;
378 +
379 + validate_new_branchname(ref, &symref, 0);
380 + strvec_pushl(&cp.args, "symbolic-ref", "HEAD", symref.buf, NULL);
381 + if (opts->quiet)
382 + strvec_push(&cp.args, "--quiet");
383 + strvec_pushv(&cp.env, child_env->v);
384 + strbuf_release(&symref);
385 + cp.git_cmd = 1;
386 + return run_command(&cp);
387 +}
388 +
389 static int add_worktree(const char *path, const char *refname,
390 const struct add_opts *opts)
391 {
@@ -397,7 +415,7 @@ static int add_worktree(const char *path, const char *refname,
415 die_if_checked_out(symref.buf, 0);
416 }
417 commit = lookup_commit_reference_by_name(refname);
400 - if (!commit)
418 + if (!commit && !opts->orphan)
419 die(_("invalid reference: %s"), refname);
420
421 name = worktree_basename(path, &len);
@@ -486,10 +504,10 @@ static int add_worktree(const char *path, const char *refname,
504 strvec_pushf(&child_env, "%s=%s", GIT_WORK_TREE_ENVIRONMENT, path);
505 cp.git_cmd = 1;
506
489 - if (!is_branch)
507 + if (!is_branch && commit) {
508 strvec_pushl(&cp.args, "update-ref", "HEAD",
509 oid_to_hex(&commit->object.oid), NULL);
492 - else {
510 + } else {
511 strvec_pushl(&cp.args, "symbolic-ref", "HEAD",
512 symref.buf, NULL);
513 if (opts->quiet)
@@ -501,6 +519,10 @@ static int add_worktree(const char *path, const char *refname,
519 if (ret)
520 goto done;
521
522 + if (opts->orphan &&
523 + (ret = make_worktree_orphan(refname, opts, &child_env)))
524 + goto done;
525 +
526 if (opts->checkout &&
527 (ret = checkout_worktree(opts, &child_env)))
528 goto done;
@@ -520,7 +542,7 @@ done:
542 * Hook failure does not warrant worktree deletion, so run hook after
543 * is_junk is cleared, but do return appropriate code when hook fails.
544 */
523 - if (!ret && opts->checkout) {
545 + if (!ret && opts->checkout && !opts->orphan) {
546 struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
547
548 strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL);
@@ -568,7 +590,7 @@ static void print_preparing_worktree_line(int detach,
590 else {
591 struct commit *commit = lookup_commit_reference_by_name(branch);
592 if (!commit)
571 - die(_("invalid reference: %s"), branch);
593 + BUG(_("unreachable: invalid reference: %s"), branch);
594 fprintf_ln(stderr, _("Preparing worktree (detached HEAD %s)"),
595 repo_find_unique_abbrev(the_repository, &commit->object.oid, DEFAULT_ABBREV));
596 }
@@ -620,6 +642,7 @@ static int add(int ac, const char **av, const char *prefix)
642 N_("create a new branch")),
643 OPT_STRING('B', NULL, &new_branch_force, N_("branch"),
644 N_("create or reset a branch")),
645 + OPT_BOOL(0, "orphan", &opts.orphan, N_("create unborn/orphaned branch")),
646 OPT_BOOL('d', "detach", &opts.detach, N_("detach HEAD at named commit")),
647 OPT_BOOL(0, "checkout", &opts.checkout, N_("populate the new working tree")),
648 OPT_BOOL(0, "lock", &keep_locked, N_("keep the new working tree locked")),
@@ -640,6 +663,17 @@ static int add(int ac, const char **av, const char *prefix)
663 ac = parse_options(ac, av, prefix, options, git_worktree_add_usage, 0);
664 if (!!opts.detach + !!new_branch + !!new_branch_force > 1)
665 die(_("options '%s', '%s', and '%s' cannot be used together"), "-b", "-B", "--detach");
666 + if (opts.detach && opts.orphan)
667 + die(_("options '%s', and '%s' cannot be used together"),
668 + "--orphan", "--detach");
669 + if (opts.orphan && opt_track)
670 + die(_("'%s' and '%s' cannot be used together"), "--orphan", "--track");
671 + if (opts.orphan && !opts.checkout)
672 + die(_("'%s' and '%s' cannot be used together"), "--orphan",
673 + "--no-checkout");
674 + if (opts.orphan && ac == 2)
675 + die(_("'%s' and '%s' cannot be used together"), "--orphan",
676 + _("<commit-ish>"));
677 if (lock_reason && !keep_locked)
678 die(_("the option '%s' requires '%s'"), "--reason", "--lock");
679 if (lock_reason)
@@ -668,13 +702,17 @@ static int add(int ac, const char **av, const char *prefix)
702 strbuf_release(&symref);
703 }
704
671 - if (ac < 2 && !new_branch && !opts.detach) {
705 + if (opts.orphan && !new_branch) {
706 + int n;
707 + const char *s = worktree_basename(path, &n);
708 + new_branch = xstrndup(s, n);
709 + } else if (new_branch || opts.detach || opts.orphan) {
710 + // No-op
711 + } else if (ac < 2) {
712 const char *s = dwim_branch(path, &new_branch);
713 if (s)
714 branch = s;
675 - }
676 -
677 - if (ac == 2 && !new_branch && !opts.detach) {
715 + } else if (ac == 2) {
716 struct object_id oid;
717 struct commit *commit;
718 const char *remote;
@@ -688,10 +726,17 @@ static int add(int ac, const char **av, const char *prefix)
726 }
727 }
728 }
729 +
730 + if (!opts.orphan && !lookup_commit_reference_by_name(branch)) {
731 + die(_("invalid reference: %s"), branch);
732 + }
733 +
734 if (!opts.quiet)
735 print_preparing_worktree_line(opts.detach, branch, new_branch, !!new_branch_force);
736
694 - if (new_branch) {
737 + if (opts.orphan) {
738 + branch = new_branch;
739 + } else if (new_branch) {
740 struct child_process cp = CHILD_PROCESS_INIT;
741 cp.git_cmd = 1;
742 strvec_push(&cp.args, "branch");
t/t2400-worktree-add.sh
+68
@@ -312,6 +312,10 @@ test_wt_add_excl () {
312 test_wt_add_excl -b poodle -B poodle bamboo main
313 test_wt_add_excl -b poodle --detach bamboo main
314 test_wt_add_excl -B poodle --detach bamboo main
315 +test_wt_add_excl --orphan --detach bamboo
316 +test_wt_add_excl --orphan --no-checkout bamboo
317 +test_wt_add_excl --orphan bamboo main
318 +test_wt_add_excl --orphan -b bamboo wtdir/ main
319
320 test_expect_success '"add -B" fails if the branch is checked out' '
321 git rev-parse newmain >before &&
@@ -341,6 +345,62 @@ test_expect_success 'add --quiet -b' '
345 test_must_be_empty actual
346 '
347
348 +test_expect_success '"add --orphan"' '
349 + test_when_finished "git worktree remove -f -f orphandir" &&
350 + git worktree add --orphan -b neworphan orphandir &&
351 + echo refs/heads/neworphan >expected &&
352 + git -C orphandir symbolic-ref HEAD >actual &&
353 + test_cmp expected actual
354 +'
355 +
356 +test_expect_success '"add --orphan (no -b)"' '
357 + test_when_finished "git worktree remove -f -f neworphan" &&
358 + git worktree add --orphan neworphan &&
359 + echo refs/heads/neworphan >expected &&
360 + git -C neworphan symbolic-ref HEAD >actual &&
361 + test_cmp expected actual
362 +'
363 +
364 +test_expect_success '"add --orphan --quiet"' '
365 + test_when_finished "git worktree remove -f -f orphandir" &&
366 + git worktree add --quiet --orphan -b neworphan orphandir 2>log.actual &&
367 + test_must_be_empty log.actual &&
368 + echo refs/heads/neworphan >expected &&
369 + git -C orphandir symbolic-ref HEAD >actual &&
370 + test_cmp expected actual
371 +'
372 +
373 +test_expect_success '"add --orphan" fails if the branch already exists' '
374 + test_when_finished "git branch -D existingbranch" &&
375 + git worktree add -b existingbranch orphandir main &&
376 + git worktree remove orphandir &&
377 + test_must_fail git worktree add --orphan -b existingbranch orphandir
378 +'
379 +
380 +test_expect_success '"add --orphan" with empty repository' '
381 + test_when_finished "rm -rf empty_repo" &&
382 + echo refs/heads/newbranch >expected &&
383 + GIT_DIR="empty_repo" git init --bare &&
384 + git -C empty_repo worktree add --orphan -b newbranch worktreedir &&
385 + git -C empty_repo/worktreedir symbolic-ref HEAD >actual &&
386 + test_cmp expected actual
387 +'
388 +
389 +test_expect_success '"add" worktree with orphan branch and lock' '
390 + git worktree add --lock --orphan -b orphanbr orphan-with-lock &&
391 + test_when_finished "git worktree unlock orphan-with-lock || :" &&
392 + test -f .git/worktrees/orphan-with-lock/locked
393 +'
394 +
395 +test_expect_success '"add" worktree with orphan branch, lock, and reason' '
396 + lock_reason="why not" &&
397 + git worktree add --detach --lock --reason "$lock_reason" orphan-with-lock-reason main &&
398 + test_when_finished "git worktree unlock orphan-with-lock-reason || :" &&
399 + test -f .git/worktrees/orphan-with-lock-reason/locked &&
400 + echo "$lock_reason" >expect &&
401 + test_cmp expect .git/worktrees/orphan-with-lock-reason/locked
402 +'
403 +
404 test_expect_success 'local clone from linked checkout' '
405 git clone --local here here-clone &&
406 ( cd here-clone && git fsck )
@@ -457,6 +517,14 @@ setup_remote_repo () {
517 )
518 }
519
520 +test_expect_success '"add" <path> <remote/branch> w/ no HEAD' '
521 + test_when_finished rm -rf repo_upstream repo_local foo &&
522 + setup_remote_repo repo_upstream repo_local &&
523 + git -C repo_local config --bool core.bare true &&
524 + git -C repo_local branch -D main &&
525 + git -C repo_local worktree add ./foo repo_upstream/foo
526 +'
527 +
528 test_expect_success '--no-track avoids setting up tracking' '
529 test_when_finished rm -rf repo_upstream repo_local foo &&
530 setup_remote_repo repo_upstream repo_local &&