While working with the worktree based git workflow, I realised that setting up a new git repository required switching between the traditional and worktree based workflows. Searching online I found a SO answer [1] which seemed to support this and which indicated that adding support for this should not be technically difficult. This patchset has three parts: * adding `-B` to the usage docs (noticed during dev and it seemed too small to justify a separate submission) * adding orphan branch functionality (as is present in `git-switch`) to `git-worktree-add` * adding an advise for using --orphan when `git worktree add` fails due to a bad ref. Changes from v3: * Fix mistake in SYNOPSIS and `--help`. Patch for this change can be found in [2], by courtesy of Ævar Arnfjörð Bjarmason. * Collapsed sequential if statements into if-else chain & simplified conditions as requested in [2]. * Simplified tests for mutually exclusive options and removed duplicate `-B/--detach` test case. Patch for this change can be found in [3], by courtesy of Ævar Arnfjörð Bjarmason. * Remove references to `git-switch`. Behavior is now explained fully in docs instead. Changes to the docs suggested in [4], by courtesy of Eric Sunshine. * Updated test case to use `test_path_is_missing` instead of `! test -d` [5]. * Updated signature for `make_worktree_orphan()` and changed `const char *orphan_branch` in `struct add_opts` to `int orphan` (boolean) to simplify the patch and maintain consistency with other flags [6]. * Added hint to common cases where `--orphan` is desired [7] to address concerns brought up in [8][9]. Slight change from `HEAD` to `branch`. * Added the new advice to the docs, advice.c/h, and using `advise_if_enabled()` as requested in [10]. * Added tests to verify `--lock` and `--reason` work properly with the newly added `--orphan` flag. * Added tests to verify that the orphan advise [7] is emitted only at the proper times. * Added tests to verify the changes [7] do not interfere with existing behavior. ex: creating a worktree from a remote branch when HEAD is an orphan. 1. https://stackoverflow.com/a/68717229/15064705/ 2. https://lore.kernel.org/git/221115.86edu3kfqz.gmgdl@xxxxxxxxxxxxxxxxxxx/ 3. https://lore.kernel.org/git/221116.86a64rkdcj.gmgdl@xxxxxxxxxxxxxxxxxxx/ 4. https://lore.kernel.org/git/CAPig+cQiyd9yGE_XpPZmzLQnNDMypnrEgkV7nqRZZn3j6E0APQ@xxxxxxxxxxxxxx/ 5. https://lore.kernel.org/git/221115.86iljfkjjo.gmgdl@xxxxxxxxxxxxxxxxxxx/ 6. https://lore.kernel.org/git/20221119025701.syuscuoqjuqhqsoz@phi/ 7. https://lore.kernel.org/git/20221119034728.m4kxh4tdpof7us7j@phi/ 8. https://lore.kernel.org/git/CAPig+cTTn764ObHJuw8epOtBdTUwocVRV=tLieCa4zf-PGCegw@xxxxxxxxxxxxxx/ 9. https://lore.kernel.org/git/221117.86sfihj3mw.gmgdl@xxxxxxxxxxxxxxxxxxx/ 10. https://lore.kernel.org/git/221123.86a64ia6bx.gmgdl@xxxxxxxxxxxxxxxxxxx/ Jacob Abel (3): worktree add: Include -B in usage docs worktree add: add --orphan flag worktree add: Add hint to use --orphan when bad ref Documentation/config/advice.txt | 4 ++ Documentation/git-worktree.txt | 17 ++++++- advice.c | 1 + advice.h | 1 + builtin/worktree.c | 81 ++++++++++++++++++++++++++--- t/t2400-worktree-add.sh | 90 +++++++++++++++++++++++++++++---- 6 files changed, 176 insertions(+), 18 deletions(-) Range-diff against v3: 1: f35d78cfb4 = 1: f35d78cfb4 worktree add: Include -B in usage docs 2: c040c87c6d ! 2: 8b1cdf1322 worktree add: add --orphan flag @@ Commit message The original reason this feature was implemented was to allow a user to initialise a new repository using solely the worktree oriented - workflow. Example usage included below. + workflow. - $ GIT_DIR=".git" git init --bare - $ git worktree add --orphan master master/ + Current Behavior: + + % git init --bare foo.git + Initialized empty Git repository in /path/to/foo.git/ + % git -C foo.git worktree add main/ + Preparing worktree (new branch 'main') + fatal: not a valid object name: 'HEAD' + % + + New Behavior: + + % git init --bare foo.git + Initialized empty Git repository in /path/to/foo.git/ + % git -C foo.git worktree add --orphan main main/ + Preparing worktree (new branch 'main') + % Signed-off-by: Jacob Abel <jacobabel@xxxxxxxxxx> + Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@xxxxxxxxx> ## Documentation/git-worktree.txt ## @@ Documentation/git-worktree.txt: SYNOPSIS - -------- [verse] 'git worktree add' [-f] [--detach] [--checkout] [--lock [--reason <string>]] -- [[-b | -B] <new-branch>] <path> [<commit-ish>] -+ [[-b | -B | --orphan] <new-branch>] <path> [<commit-ish>] + [[-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> @@ Documentation/git-worktree.txt: exist, a new branch based on `HEAD` is automatic +$ git worktree add --orphan <branch> <path> +------------ ++ -+Create a worktree containing an orphan branch named `<branch>` with a -+clean working directory. See `--orphan` in linkgit:git-switch[1] for -+more details. ++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:: @@ Documentation/git-worktree.txt: This can also be set up as the default behaviour remove. +--orphan <new-branch>:: -+ With `add`, create a new orphan branch named `<new-branch>` in the new -+ worktree. See `--orphan` in linkgit:git-switch[1] for details. ++ 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. @@ builtin/worktree.c #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 | --orphan] <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 \ @@ builtin/worktree.c: struct add_opts { int detach; int quiet; int checkout; -+ const char *orphan_branch; ++ int orphan; const char *keep_locked; }; @@ builtin/worktree.c: static int checkout_worktree(const struct add_opts *opts, return run_command(&cp); } -+static int make_worktree_orphan(const struct add_opts *opts, ++static int make_worktree_orphan(const char * ref, const struct add_opts *opts, + struct strvec *child_env) +{ + int ret; @@ builtin/worktree.c: static int checkout_worktree(const struct add_opts *opts, + struct child_process cp = CHILD_PROCESS_INIT; + cp.git_cmd = 1; + -+ validate_new_branchname(opts->orphan_branch, &symref, 0); ++ validate_new_branchname(ref, &symref, 0); + strvec_pushl(&cp.args, "symbolic-ref", "HEAD", symref.buf, NULL); + if (opts->quiet) + strvec_push(&cp.args, "--quiet"); @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam } commit = lookup_commit_reference_by_name(refname); - if (!commit) -+ if (!commit && !opts->orphan_branch) ++ if (!commit && !opts->orphan) { die(_("invalid reference: %s"), refname); ++ } name = worktree_basename(path, &len); + strbuf_add(&sb, name, path + len - name); @@ builtin/worktree.c: 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; @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam if (ret) goto done; -+ if (opts->orphan_branch && -+ (ret = make_worktree_orphan(opts, &child_env))) ++ if (opts->orphan && ++ (ret = make_worktree_orphan(refname, opts, &child_env))) + goto done; + if (opts->checkout && @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam * is_junk is cleared, but do return appropriate code when hook fails. */ - if (!ret && opts->checkout) { -+ if (!ret && opts->checkout && !opts->orphan_branch) { ++ 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); +@@ builtin/worktree.c: 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; @@ builtin/worktree.c: 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", &opts.orphan_branch, N_("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")), @@ builtin/worktree.c: 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 + !!new_branch + !!new_branch_force + !!opts.orphan_branch > 1) ++ 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_branch && opt_track) ++ if (opts.orphan && opt_track) + die(_("'%s' and '%s' cannot be used together"), "--orphan", "--track"); -+ if (opts.orphan_branch && !opts.checkout) ++ 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) @@ builtin/worktree.c: static int add(int ac, const char **av, const char *prefix) } - if (ac < 2 && !new_branch && !opts.detach) { -+ /* -+ * As the orphan cannot be created until the contents of branch -+ * are staged, opts.orphan_branch should be treated as both a boolean -+ * indicating that `--orphan` was selected and as the name of the new -+ * orphan branch from this point on. -+ */ -+ if (opts.orphan_branch) { -+ new_branch = opts.orphan_branch; -+ } -+ -+ if (ac < 2 && !new_branch && !opts.detach && !opts.orphan_branch) { ++ 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; @@ builtin/worktree.c: static int add(int ac, const char **av, const char *prefix) print_preparing_worktree_line(opts.detach, branch, new_branch, !!new_branch_force); - if (new_branch) { -+ if (new_branch && !opts.orphan_branch) { ++ if (opts.orphan) { ++ branch = new_branch; ++ } else if (!lookup_commit_reference_by_name(branch)) { ++ /* ++ * If `branch` does not reference a valid commit, a new ++ * worktree (and/or branch) cannot be created based off of it. ++ */ ++ 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"); ## t/t2400-worktree-add.sh ## -@@ t/t2400-worktree-add.sh: test_expect_success '"add" -B/--detach mutually exclusive' ' - test_must_fail git worktree add -B poodle --detach bamboo main +@@ t/t2400-worktree-add.sh: test_expect_success '"add" no auto-vivify with --detach and <branch> omitted' ' + test_must_fail git -C mish/mash symbolic-ref HEAD ' -+test_expect_success '"add" --orphan/-b mutually exclusive' ' -+ test_must_fail git worktree add --orphan poodle -b poodle bamboo -+' -+ -+test_expect_success '"add" --orphan/-B mutually exclusive' ' -+ test_must_fail git worktree add --orphan poodle -B poodle bamboo -+' -+ -+test_expect_success '"add" --orphan/--detach mutually exclusive' ' -+ test_must_fail git worktree add --orphan poodle --detach bamboo -+' -+ -+test_expect_success '"add" --orphan/--no-checkout mutually exclusive' ' -+ test_must_fail git worktree add --orphan poodle --no-checkout bamboo -+' -+ -+test_expect_success '"add" -B/--detach mutually exclusive' ' -+ test_must_fail git worktree add -B poodle --detach bamboo main -+' -+ +-test_expect_success '"add" -b/-B mutually exclusive' ' +- test_must_fail git worktree add -b poodle -B poodle bamboo main +-' +- +-test_expect_success '"add" -b/--detach mutually exclusive' ' +- test_must_fail git worktree add -b poodle --detach bamboo main +-' ++# Helper function to test mutually exclusive options. ++test_wt_add_excl() { ++ local opts="$@" && ++ test_expect_success "'worktree add' with '$opts' has mutually exclusive options" ' ++ test_must_fail git worktree add $opts ++ ' ++} + +-test_expect_success '"add" -B/--detach mutually exclusive' ' +- test_must_fail git worktree add -B poodle --detach bamboo main +-' ++test_wt_add_excl -b poodle -B poodle bamboo main ++test_wt_add_excl -b poodle --orphan poodle bamboo ++test_wt_add_excl -b poodle --detach 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 --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 && - test_must_fail git worktree add -B newmain bamboo main && @@ t/t2400-worktree-add.sh: test_expect_success 'add --quiet' ' test_must_be_empty actual ' @@ t/t2400-worktree-add.sh: test_expect_success 'add --quiet' ' + 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 ! -d orphandir2 ++ test_path_is_missing orphandir2 +' + +test_expect_success '"add --orphan" with empty repository' ' @@ t/t2400-worktree-add.sh: test_expect_success 'add --quiet' ' + 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 ) +@@ t/t2400-worktree-add.sh: 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 && -: ---------- > 3: 74cb091bb3 worktree add: Add hint to use --orphan when bad ref -- 2.37.4