{"thread":{"id":"59708","subject":"[PATCH v10 5/8] worktree add: add --orphan flag","startedAt":"2023-05-07T12:07:07Z","lastAt":"2023-05-07T12:07:07Z","messageCount":1,"participants":["Jacob Abel"],"isPatch":true,"patchVersion":10,"patchTotal":8},"messages":[{"id":"476722","messageId":"20230507120530.14669-6-jacobabel@nullpo.dev","threadId":"59708","inReplyTo":null,"subject":"[PATCH v10 5/8] worktree add: add --orphan flag","fromName":"Jacob Abel","fromEmail":"jacobabel@nullpo.dev","sentAt":"2023-05-07T12:06:43Z","receivedAt":"2023-05-07T12:07:07Z","isPatch":true,"sender":{"key":"jacobabel@nullpo.dev","avatar":"https://avatars.githubusercontent.com/u/9424043?v=4"},"body":"Add support for creating an orphan branch when adding a new worktree.\nThe functionality of this flag is equivalent to git switch's --orphan\noption.\n\nCurrent Behavior:\n% git -C foo.git --no-pager branch -l\n+ main\n% git -C foo.git worktree add main/\nPreparing worktree (new branch 'main')\nHEAD is now at 6c93a75 a commit\n%\n\n% git init bar.git\nInitialized empty Git repository in /path/to/bar.git/\n% git -C bar.git --no-pager branch -l\n\n% git -C bar.git worktree add main/\nPreparing worktree (new branch 'main')\nfatal: not a valid object name: 'HEAD'\n%\n\nNew Behavior:\n\n% git -C foo.git --no-pager branch -l\n+ main\n% git -C foo.git worktree add main/\nPreparing worktree (new branch 'main')\nHEAD is now at 6c93a75 a commit\n%\n\n% git init --bare bar.git\nInitialized empty Git repository in /path/to/bar.git/\n% git -C bar.git --no-pager branch -l\n\n% git -C bar.git worktree add main/\nPreparing worktree (new branch 'main')\nfatal: invalid reference: HEAD\n% git -C bar.git worktree add --orphan -b main/\nPreparing worktree (new branch 'main')\n% git -C bar.git worktree add --orphan -b newbranch worktreedir/\nPreparing worktree (new branch 'newbranch')\n%\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nSigned-off-by: Jacob Abel <jacobabel@nullpo.dev>\n---\n Documentation/git-worktree.txt |  6 ++-\n builtin/worktree.c             | 67 +++++++++++++++++++++++++++------\n t/t2400-worktree-add.sh        | 68 ++++++++++++++++++++++++++++++++++\n 3 files changed, 129 insertions(+), 12 deletions(-)\n\ndiff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt\nindex b9c12779f1..485d865eb2 100644\n--- a/Documentation/git-worktree.txt\n+++ b/Documentation/git-worktree.txt\n@@ -10,7 +10,7 @@ SYNOPSIS\n --------\n [verse]\n 'git worktree add' [-f] [--detach] [--checkout] [--lock [--reason <string>]]\n-\t\t   [(-b | -B) <new-branch>] <path> [<commit-ish>]\n+\t\t   [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]\n 'git worktree list' [-v | --porcelain [-z]]\n 'git worktree lock' [--reason <string>] <worktree>\n 'git worktree move' <worktree> <new-path>\n@@ -222,6 +222,10 @@ This can also be set up as the default behaviour by using the\n \tWith `prune`, do not remove anything; just report what it would\n \tremove.\n\n+--orphan::\n+\tWith `add`, make the new worktree and index empty, associating\n+\tthe worktree with a new orphan/unborn branch named `<new-branch>`.\n+\n --porcelain::\n \tWith `list`, output in an easy-to-parse format for scripts.\n \tThis format will remain stable across Git versions and regardless of user\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex d1b4b53f2c..48de7fc3b0 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -22,7 +22,8 @@\n\n #define BUILTIN_WORKTREE_ADD_USAGE \\\n \tN_(\"git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]]\\n\" \\\n-\t   \"                 [(-b | -B) <new-branch>] <path> [<commit-ish>]\")\n+\t   \"                 [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]\")\n+\n #define BUILTIN_WORKTREE_LIST_USAGE \\\n \tN_(\"git worktree list [-v | --porcelain [-z]]\")\n #define BUILTIN_WORKTREE_LOCK_USAGE \\\n@@ -95,6 +96,7 @@ struct add_opts {\n \tint detach;\n \tint quiet;\n \tint checkout;\n+\tint orphan;\n \tconst char *keep_locked;\n };\n\n@@ -368,6 +370,22 @@ static int checkout_worktree(const struct add_opts *opts,\n \treturn run_command(&cp);\n }\n\n+static int make_worktree_orphan(const char * ref, const struct add_opts *opts,\n+\t\t\t\tstruct strvec *child_env)\n+{\n+\tstruct strbuf symref = STRBUF_INIT;\n+\tstruct child_process cp = CHILD_PROCESS_INIT;\n+\n+\tvalidate_new_branchname(ref, &symref, 0);\n+\tstrvec_pushl(&cp.args, \"symbolic-ref\", \"HEAD\", symref.buf, NULL);\n+\tif (opts->quiet)\n+\t\tstrvec_push(&cp.args, \"--quiet\");\n+\tstrvec_pushv(&cp.env, child_env->v);\n+\tstrbuf_release(&symref);\n+\tcp.git_cmd = 1;\n+\treturn run_command(&cp);\n+}\n+\n static int add_worktree(const char *path, const char *refname,\n \t\t\tconst struct add_opts *opts)\n {\n@@ -397,7 +415,7 @@ static int add_worktree(const char *path, const char *refname,\n \t\t\tdie_if_checked_out(symref.buf, 0);\n \t}\n \tcommit = lookup_commit_reference_by_name(refname);\n-\tif (!commit)\n+\tif (!commit && !opts->orphan)\n \t\tdie(_(\"invalid reference: %s\"), refname);\n\n \tname = worktree_basename(path, &len);\n@@ -486,10 +504,10 @@ static int add_worktree(const char *path, const char *refname,\n \tstrvec_pushf(&child_env, \"%s=%s\", GIT_WORK_TREE_ENVIRONMENT, path);\n \tcp.git_cmd = 1;\n\n-\tif (!is_branch)\n+\tif (!is_branch && commit) {\n \t\tstrvec_pushl(&cp.args, \"update-ref\", \"HEAD\",\n \t\t\t     oid_to_hex(&commit->object.oid), NULL);\n-\telse {\n+\t} else {\n \t\tstrvec_pushl(&cp.args, \"symbolic-ref\", \"HEAD\",\n \t\t\t     symref.buf, NULL);\n \t\tif (opts->quiet)\n@@ -501,6 +519,10 @@ static int add_worktree(const char *path, const char *refname,\n \tif (ret)\n \t\tgoto done;\n\n+\tif (opts->orphan &&\n+\t    (ret = make_worktree_orphan(refname, opts, &child_env)))\n+\t\tgoto done;\n+\n \tif (opts->checkout &&\n \t    (ret = checkout_worktree(opts, &child_env)))\n \t\tgoto done;\n@@ -520,7 +542,7 @@ static int add_worktree(const char *path, const char *refname,\n \t * Hook failure does not warrant worktree deletion, so run hook after\n \t * is_junk is cleared, but do return appropriate code when hook fails.\n \t */\n-\tif (!ret && opts->checkout) {\n+\tif (!ret && opts->checkout && !opts->orphan) {\n \t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n\n \t\tstrvec_pushl(&opt.env, \"GIT_DIR\", \"GIT_WORK_TREE\", NULL);\n@@ -568,7 +590,7 @@ static void print_preparing_worktree_line(int detach,\n \t\telse {\n \t\t\tstruct commit *commit = lookup_commit_reference_by_name(branch);\n \t\t\tif (!commit)\n-\t\t\t\tdie(_(\"invalid reference: %s\"), branch);\n+\t\t\t\tBUG(_(\"unreachable: invalid reference: %s\"), branch);\n \t\t\tfprintf_ln(stderr, _(\"Preparing worktree (detached HEAD %s)\"),\n \t\t\t\t  repo_find_unique_abbrev(the_repository, &commit->object.oid, DEFAULT_ABBREV));\n \t\t}\n@@ -620,6 +642,7 @@ static int add(int ac, const char **av, const char *prefix)\n \t\t\t   N_(\"create a new branch\")),\n \t\tOPT_STRING('B', NULL, &new_branch_force, N_(\"branch\"),\n \t\t\t   N_(\"create or reset a branch\")),\n+\t\tOPT_BOOL(0, \"orphan\", &opts.orphan, N_(\"create unborn/orphaned branch\")),\n \t\tOPT_BOOL('d', \"detach\", &opts.detach, N_(\"detach HEAD at named commit\")),\n \t\tOPT_BOOL(0, \"checkout\", &opts.checkout, N_(\"populate the new working tree\")),\n \t\tOPT_BOOL(0, \"lock\", &keep_locked, N_(\"keep the new working tree locked\")),\n@@ -640,6 +663,17 @@ static int add(int ac, const char **av, const char *prefix)\n \tac = parse_options(ac, av, prefix, options, git_worktree_add_usage, 0);\n \tif (!!opts.detach + !!new_branch + !!new_branch_force > 1)\n \t\tdie(_(\"options '%s', '%s', and '%s' cannot be used together\"), \"-b\", \"-B\", \"--detach\");\n+\tif (opts.detach && opts.orphan)\n+\t\tdie(_(\"options '%s', and '%s' cannot be used together\"),\n+\t\t    \"--orphan\", \"--detach\");\n+\tif (opts.orphan && opt_track)\n+\t\tdie(_(\"'%s' and '%s' cannot be used together\"), \"--orphan\", \"--track\");\n+\tif (opts.orphan && !opts.checkout)\n+\t\tdie(_(\"'%s' and '%s' cannot be used together\"), \"--orphan\",\n+\t\t    \"--no-checkout\");\n+\tif (opts.orphan && ac == 2)\n+\t\tdie(_(\"'%s' and '%s' cannot be used together\"), \"--orphan\",\n+\t\t    _(\"<commit-ish>\"));\n \tif (lock_reason && !keep_locked)\n \t\tdie(_(\"the option '%s' requires '%s'\"), \"--reason\", \"--lock\");\n \tif (lock_reason)\n@@ -668,13 +702,17 @@ static int add(int ac, const char **av, const char *prefix)\n \t\tstrbuf_release(&symref);\n \t}\n\n-\tif (ac < 2 && !new_branch && !opts.detach) {\n+\tif (opts.orphan && !new_branch) {\n+\t\tint n;\n+\t\tconst char *s = worktree_basename(path, &n);\n+\t\tnew_branch = xstrndup(s, n);\n+\t} else if (new_branch || opts.detach || opts.orphan) {\n+\t\t// No-op\n+\t} else if (ac < 2) {\n \t\tconst char *s = dwim_branch(path, &new_branch);\n \t\tif (s)\n \t\t\tbranch = s;\n-\t}\n-\n-\tif (ac == 2 && !new_branch && !opts.detach) {\n+\t} else if (ac == 2) {\n \t\tstruct object_id oid;\n \t\tstruct commit *commit;\n \t\tconst char *remote;\n@@ -688,10 +726,17 @@ static int add(int ac, const char **av, const char *prefix)\n \t\t\t}\n \t\t}\n \t}\n+\n+\tif (!opts.orphan && !lookup_commit_reference_by_name(branch)) {\n+\t\tdie(_(\"invalid reference: %s\"), branch);\n+\t}\n+\n \tif (!opts.quiet)\n \t\tprint_preparing_worktree_line(opts.detach, branch, new_branch, !!new_branch_force);\n\n-\tif (new_branch) {\n+\tif (opts.orphan) {\n+\t\tbranch = new_branch;\n+\t} else if (new_branch) {\n \t\tstruct child_process cp = CHILD_PROCESS_INIT;\n \t\tcp.git_cmd = 1;\n \t\tstrvec_push(&cp.args, \"branch\");\ndiff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh\nindex 841f15f59e..fba90582b6 100755\n--- a/t/t2400-worktree-add.sh\n+++ b/t/t2400-worktree-add.sh\n@@ -312,6 +312,10 @@ test_wt_add_excl () {\n test_wt_add_excl -b poodle -B poodle bamboo main\n test_wt_add_excl -b poodle --detach bamboo main\n test_wt_add_excl -B poodle --detach bamboo main\n+test_wt_add_excl --orphan --detach bamboo\n+test_wt_add_excl --orphan --no-checkout bamboo\n+test_wt_add_excl --orphan bamboo main\n+test_wt_add_excl --orphan -b bamboo wtdir/ main\n\n test_expect_success '\"add -B\" fails if the branch is checked out' '\n \tgit rev-parse newmain >before &&\n@@ -341,6 +345,62 @@ test_expect_success 'add --quiet -b' '\n \ttest_must_be_empty actual\n '\n\n+test_expect_success '\"add --orphan\"' '\n+\ttest_when_finished \"git worktree remove -f -f orphandir\" &&\n+\tgit worktree add --orphan -b neworphan orphandir &&\n+\techo refs/heads/neworphan >expected &&\n+\tgit -C orphandir symbolic-ref HEAD >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success '\"add --orphan (no -b)\"' '\n+\ttest_when_finished \"git worktree remove -f -f neworphan\" &&\n+\tgit worktree add --orphan neworphan &&\n+\techo refs/heads/neworphan >expected &&\n+\tgit -C neworphan symbolic-ref HEAD >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success '\"add --orphan --quiet\"' '\n+\ttest_when_finished \"git worktree remove -f -f orphandir\" &&\n+\tgit worktree add --quiet --orphan -b neworphan orphandir 2>log.actual &&\n+\ttest_must_be_empty log.actual &&\n+\techo refs/heads/neworphan >expected &&\n+\tgit -C orphandir symbolic-ref HEAD >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success '\"add --orphan\" fails if the branch already exists' '\n+\ttest_when_finished \"git branch -D existingbranch\" &&\n+\tgit worktree add -b existingbranch orphandir main &&\n+\tgit worktree remove orphandir &&\n+\ttest_must_fail git worktree add --orphan -b existingbranch orphandir\n+'\n+\n+test_expect_success '\"add --orphan\" with empty repository' '\n+\ttest_when_finished \"rm -rf empty_repo\" &&\n+\techo refs/heads/newbranch >expected &&\n+\tGIT_DIR=\"empty_repo\" git init --bare &&\n+\tgit -C empty_repo worktree add --orphan -b newbranch worktreedir &&\n+\tgit -C empty_repo/worktreedir symbolic-ref HEAD >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success '\"add\" worktree with orphan branch and lock' '\n+\tgit worktree add --lock --orphan -b orphanbr orphan-with-lock &&\n+\ttest_when_finished \"git worktree unlock orphan-with-lock || :\" &&\n+\ttest -f .git/worktrees/orphan-with-lock/locked\n+'\n+\n+test_expect_success '\"add\" worktree with orphan branch, lock, and reason' '\n+\tlock_reason=\"why not\" &&\n+\tgit worktree add --detach --lock --reason \"$lock_reason\" orphan-with-lock-reason main &&\n+\ttest_when_finished \"git worktree unlock orphan-with-lock-reason || :\" &&\n+\ttest -f .git/worktrees/orphan-with-lock-reason/locked &&\n+\techo \"$lock_reason\" >expect &&\n+\ttest_cmp expect .git/worktrees/orphan-with-lock-reason/locked\n+'\n+\n test_expect_success 'local clone from linked checkout' '\n \tgit clone --local here here-clone &&\n \t( cd here-clone && git fsck )\n@@ -457,6 +517,14 @@ setup_remote_repo () {\n \t)\n }\n\n+test_expect_success '\"add\" <path> <remote/branch> w/ no HEAD' '\n+\ttest_when_finished rm -rf repo_upstream repo_local foo &&\n+\tsetup_remote_repo repo_upstream repo_local &&\n+\tgit -C repo_local config --bool core.bare true &&\n+\tgit -C repo_local branch -D main &&\n+\tgit -C repo_local worktree add ./foo repo_upstream/foo\n+'\n+\n test_expect_success '--no-track avoids setting up tracking' '\n \ttest_when_finished rm -rf repo_upstream repo_local foo &&\n \tsetup_remote_repo repo_upstream repo_local &&\n--\n2.39.3\n\n\n"}]}