The intention of `git init --separate-work-dir=<path>` is to move the .git/ directory to a location outside of the main worktree. When used within a linked worktree, however, rather than moving the .git/ directory as intended, it instead incorrectly moves the worktree's .git/worktrees/<id> directory to <path>, thus disconnecting the linked worktree from its parent repository and breaking the worktree in the process since its local .git file no longer points at a location at which it can find the object database. Fix this broken behavior. An intentional side-effect of this change is that it also closes a loophole not caught by ccf236a23a (init: disallow --separate-git-dir with bare repository, 2020-08-09) in which the check to prevent --separate-git-dir being used in conjunction with a bare repository was unable to detect the invalid combination when invoked from within a linked worktree. Therefore, add a test to verify that this loophole is closed, as well. Reported-by: Henré Botha <henrebotha@xxxxxxxxx> Signed-off-by: Eric Sunshine <sunshine@xxxxxxxxxxxxxx> --- builtin/init-db.c | 24 ++++++++++++++++++++++++ t/t0001-init.sh | 21 +++++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/builtin/init-db.c b/builtin/init-db.c index 7b915d88ab..cd3e760541 100644 --- a/builtin/init-db.c +++ b/builtin/init-db.c @@ -642,6 +642,30 @@ int cmd_init_db(int argc, const char **argv, const char *prefix) if (!git_dir) git_dir = DEFAULT_GIT_DIR_ENVIRONMENT; + /* + * When --separate-git-dir is used inside a linked worktree, take + * care to ensure that the common .git/ directory is relocated, not + * the worktree-specific .git/worktrees/<id>/ directory. + */ + if (real_git_dir) { + int err; + const char *p; + struct strbuf sb = STRBUF_INIT; + + p = read_gitfile_gently(git_dir, &err); + if (p && get_common_dir(&sb, p)) { + struct strbuf mainwt = STRBUF_INIT; + + strbuf_addbuf(&mainwt, &sb); + strbuf_strip_suffix(&mainwt, "/.git"); + if (chdir(mainwt.buf) < 0) + die_errno(_("cannot chdir to %s"), mainwt.buf); + strbuf_release(&mainwt); + git_dir = strbuf_detach(&sb, NULL); + } + strbuf_release(&sb); + } + if (is_bare_repository_cfg < 0) is_bare_repository_cfg = guess_repository_type(git_dir); diff --git a/t/t0001-init.sh b/t/t0001-init.sh index e489eb4ddb..2f7c3dcd0f 100755 --- a/t/t0001-init.sh +++ b/t/t0001-init.sh @@ -329,6 +329,15 @@ test_expect_success 'implicit bare & --separate-git-dir incompatible' ' test_i18ngrep "incompatible" err ' +test_expect_success 'bare & --separate-git-dir incompatible within worktree' ' + test_when_finished "rm -rf bare.git linkwt seprepo" && + test_commit gumby && + git clone --bare . bare.git && + git -C bare.git worktree add --detach ../linkwt && + test_must_fail git -C linkwt init --separate-git-dir seprepo 2>err && + test_i18ngrep "incompatible" err +' + test_lazy_prereq GETCWD_IGNORES_PERMS ' base=GETCWD_TEST_BASE_DIR && mkdir -p $base/dir && @@ -405,15 +414,23 @@ test_expect_success SYMLINKS 're-init to move gitdir symlink' ' test_path_is_dir realgitdir/refs ' -test_expect_success 're-init to move gitdir with linked worktrees' ' +sep_git_dir_worktree () { test_when_finished "rm -rf mainwt linkwt seprepo" && git init mainwt && test_commit -C mainwt gumby && git -C mainwt worktree add --detach ../linkwt && - git -C mainwt init --separate-git-dir ../seprepo && + git -C "$1" init --separate-git-dir ../seprepo && git -C mainwt rev-parse --git-common-dir >expect && git -C linkwt rev-parse --git-common-dir >actual && test_cmp expect actual +} + +test_expect_success 're-init to move gitdir with linked worktrees' ' + sep_git_dir_worktree mainwt +' + +test_expect_success 're-init to move gitdir within linked worktree' ' + sep_git_dir_worktree linkwt ' test_expect_success MINGW '.git hidden' ' -- 2.28.0.531.g41c3d8a546