{"thread":{"id":"66180","subject":"[PATCH] worktree repair: detect relative path in .git file correctly","startedAt":"2026-08-15T13:11:22Z","lastAt":"2026-08-28T17:41:37Z","messageCount":12,"participants":["Yoichi NAKAYAMA via GitGitGadget","Junio C Hamano","Yoichi Nakayama"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"550660","messageId":"pull.2205.git.1786799480344.gitgitgadget@gmail.com","threadId":"66180","inReplyTo":null,"subject":"[PATCH] worktree repair: detect relative path in .git file correctly","fromName":"Yoichi NAKAYAMA via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-15T13:11:19Z","receivedAt":"2026-08-15T13:11:22Z","isPatch":true,"body":"From: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>\n\nSince read_gitfile_gently() always returns an absolute path, the\nconversion from a relative path to an absolute path was not\nfunctioning and dead code existed.\n\nSigned-off-by: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>\n---\n    worktree repair: detect relative path in .git file correctly\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2205%2Fyoichi%2Fworktree-repair-relative-path-handling-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2205/yoichi/worktree-repair-relative-path-handling-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/2205\n\n builtin/init-db.c          |  2 +-\n setup.c                    | 13 ++++++---\n setup.h                    |  4 +--\n t/t2406-worktree-repair.sh | 54 +++++++++++++++++++++++++++++---------\n worktree.c                 | 28 ++++++--------------\n 5 files changed, 62 insertions(+), 39 deletions(-)\n\ndiff --git a/builtin/init-db.c b/builtin/init-db.c\nindex e96b1283b7..2369fcea1b 100644\n--- a/builtin/init-db.c\n+++ b/builtin/init-db.c\n@@ -210,7 +210,7 @@ int cmd_init_db(int argc,\n \t\tconst char *p;\n \t\tstruct strbuf sb = STRBUF_INIT;\n \n-\t\tp = read_gitfile_gently(git_dir, &err);\n+\t\tp = read_gitfile_gently(git_dir, NULL, &err);\n \t\tif (p && get_common_dir(&sb, p)) {\n \t\t\tstruct strbuf mainwt = STRBUF_INIT;\n \ndiff --git a/setup.c b/setup.c\nindex 95909e9603..febb3248a5 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -458,7 +458,7 @@ int is_nonbare_repository_dir(struct strbuf *path)\n \tassert(orig_path_len != 0);\n \tstrbuf_complete(path, '/');\n \tstrbuf_addstr(path, \".git\");\n-\tif (read_gitfile_gently(path->buf, &gitfile_error) || is_git_directory(path->buf))\n+\tif (read_gitfile_gently(path->buf, NULL, &gitfile_error) || is_git_directory(path->buf))\n \t\tret = 1;\n \tif (gitfile_error == READ_GITFILE_ERR_OPEN_FAILED ||\n \t    gitfile_error == READ_GITFILE_ERR_READ_FAILED)\n@@ -956,12 +956,15 @@ void read_gitfile_error_die(int error_code, const char *path)\n  * return path to git directory if found. The return value comes from\n  * a shared buffer.\n  *\n+ * On success, if absolute is not NULL, it will be set to whether the\n+ * path in .git file is an absolute path.\n+ *\n  * On failure, if return_error_code is not NULL, return_error_code\n  * will be set to an error code and NULL will be returned. If\n  * return_error_code is NULL the function will die instead (for most\n  * cases).\n  */\n-const char *read_gitfile_gently(const char *path, int *return_error_code)\n+const char *read_gitfile_gently(const char *path, bool *absolute, int *return_error_code)\n {\n \tconst int max_file_size = 1 << 20;  /* 1MB */\n \tint error_code = 0;\n@@ -1016,6 +1019,8 @@ const char *read_gitfile_gently(const char *path, int *return_error_code)\n \t}\n \tbuf[len] = '\\0';\n \tdir = buf + 8;\n+\tif (absolute)\n+\t\t*absolute = is_absolute_path(dir);\n \n \tif (!is_absolute_path(dir) && (slash = strrchr(path, '/'))) {\n \t\tsize_t pathlen = slash+1 - path;\n@@ -1615,7 +1620,7 @@ static enum discovery_result repo_discovery_find_dir(struct strbuf *dir,\n \t\tif (offset > min_offset)\n \t\t\tstrbuf_addch(dir, '/');\n \t\tstrbuf_addstr(dir, DEFAULT_GIT_DIR_ENVIRONMENT);\n-\t\tgitdirenv = read_gitfile_gently(dir->buf, &error_code);\n+\t\tgitdirenv = read_gitfile_gently(dir->buf, NULL, &error_code);\n \t\tif (!gitdirenv) {\n \t\t\tswitch (error_code) {\n \t\t\tcase READ_GITFILE_ERR_MISSING:\n@@ -2185,7 +2190,7 @@ const char *resolve_gitdir_gently(const char *suspect, int *return_error_code)\n {\n \tif (is_git_directory(suspect))\n \t\treturn suspect;\n-\treturn read_gitfile_gently(suspect, return_error_code);\n+\treturn read_gitfile_gently(suspect, NULL, return_error_code);\n }\n \n /* if any standard file descriptor is missing open it to /dev/null */\ndiff --git a/setup.h b/setup.h\nindex 654f10e059..018893b1d7 100644\n--- a/setup.h\n+++ b/setup.h\n@@ -39,8 +39,8 @@ int is_nonbare_repository_dir(struct strbuf *path);\n #define READ_GITFILE_ERR_MISSING 9\n #define READ_GITFILE_ERR_IS_A_DIR 10\n void read_gitfile_error_die(int error_code, const char *path);\n-const char *read_gitfile_gently(const char *path, int *return_error_code);\n-#define read_gitfile(path) read_gitfile_gently((path), NULL)\n+const char *read_gitfile_gently(const char *path, bool *absolute, int *return_error_code);\n+#define read_gitfile(path) read_gitfile_gently((path), NULL, NULL)\n const char *resolve_gitdir_gently(const char *suspect, int *return_error_code);\n #define resolve_gitdir(path) resolve_gitdir_gently((path), NULL)\n \ndiff --git a/t/t2406-worktree-repair.sh b/t/t2406-worktree-repair.sh\nindex f5f19b3169..5f241c9878 100755\n--- a/t/t2406-worktree-repair.sh\n+++ b/t/t2406-worktree-repair.sh\n@@ -228,30 +228,60 @@ test_expect_success 'repair worktree with relative path with missing gitfile' '\n \ttest_cmp expect wt/.git\n '\n \n-test_expect_success 'repair absolute worktree to use relative paths' '\n-\ttest_when_finished \"rm -rf main side sidemoved\" &&\n+test_expect_success 'repair absolute to relative in side worktree' '\n+\ttest_when_finished \"rm -rf main side\" &&\n \ttest_create_repo main &&\n \ttest_commit -C main init &&\n \tgit -C main worktree add --detach ../side &&\n-\techo \"../../../../sidemoved/.git\" >expect-gitdir &&\n+\techo \"../../../../side/.git\" >expect-gitdir &&\n \techo \"gitdir: ../main/.git/worktrees/side\" >expect-gitfile &&\n-\tmv side sidemoved &&\n-\tgit -C main worktree repair --relative-paths ../sidemoved &&\n+\tgit -C side worktree repair --relative-paths 2>main/err &&\n+\ttest_grep \"gitdir absolute/relative path mismatch\" main/err &&\n \ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n-\ttest_cmp expect-gitfile sidemoved/.git\n+\ttest_cmp expect-gitfile side/.git\n '\n \n-test_expect_success 'repair relative worktree to use absolute paths' '\n-\ttest_when_finished \"rm -rf main side sidemoved\" &&\n+test_expect_success 'repair relative to absolute in side worktree' '\n+\ttest_when_finished \"rm -rf main side\" &&\n \ttest_create_repo main &&\n \ttest_commit -C main init &&\n \tgit -C main worktree add --relative-paths --detach ../side &&\n-\techo \"$(pwd)/sidemoved/.git\" >expect-gitdir &&\n+\techo \"$(pwd)/side/.git\" >expect-gitdir &&\n \techo \"gitdir: $(pwd)/main/.git/worktrees/side\" >expect-gitfile &&\n-\tmv side sidemoved &&\n-\tgit -C main worktree repair ../sidemoved &&\n+\tgit -C side worktree repair 2>main/err &&\n+\ttest_grep \"gitdir absolute/relative path mismatch\" main/err &&\n \ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n-\ttest_cmp expect-gitfile sidemoved/.git\n+\ttest_cmp expect-gitfile side/.git\n+'\n+\n+test_expect_success 'repair absolute to relative in main worktree' '\n+\ttest_when_finished \"rm -rf main side\" &&\n+\ttest_create_repo main &&\n+\tgit -C main config worktree.useRelativePaths false &&\n+\ttest_commit -C main init &&\n+\tgit -C main worktree add --detach ../side &&\n+\techo \"../../../../side/.git\" >expect-gitdir &&\n+\techo \"gitdir: ../main/.git/worktrees/side\" >expect-gitfile &&\n+\tgit -C main config worktree.useRelativePaths true &&\n+\tgit -C main worktree repair 2>main/err &&\n+\ttest_grep \".git file absolute/relative path mismatch\" main/err &&\n+\ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n+\ttest_cmp expect-gitfile side/.git\n+'\n+\n+test_expect_success 'repair relative to absolute in main worktree' '\n+\ttest_when_finished \"rm -rf main side\" &&\n+\ttest_create_repo main &&\n+\tgit -C main config worktree.useRelativePaths true &&\n+\ttest_commit -C main init &&\n+\tgit -C main worktree add --detach ../side &&\n+\techo \"$(pwd)/side/.git\" >expect-gitdir &&\n+\techo \"gitdir: $(pwd)/main/.git/worktrees/side\" >expect-gitfile &&\n+\tgit -C main config worktree.useRelativePaths false &&\n+\tgit -C main worktree repair 2>main/err &&\n+\ttest_grep \".git file absolute/relative path mismatch\" main/err &&\n+\ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n+\ttest_cmp expect-gitfile side/.git\n '\n \n test_done\ndiff --git a/worktree.c b/worktree.c\nindex cbf95328a3..86b599ed27 100644\n--- a/worktree.c\n+++ b/worktree.c\n@@ -409,7 +409,7 @@ int validate_worktree(const struct worktree *wt, struct strbuf *errmsg,\n \t\tgoto done;\n \t}\n \n-\tpath = xstrdup_or_null(read_gitfile_gently(wt_path.buf, &err));\n+\tpath = xstrdup_or_null(read_gitfile_gently(wt_path.buf, NULL, &err));\n \tif (!path) {\n \t\tstrbuf_addf_gently(errmsg, _(\"'%s' is not a .git file, error code %d\"),\n \t\t\t\t   wt_path.buf, err);\n@@ -650,6 +650,7 @@ static void repair_gitfile(struct worktree *wt,\n \tstruct strbuf repo = STRBUF_INIT;\n \tstruct strbuf backlink = STRBUF_INIT;\n \tchar *dotgit_contents = NULL;\n+\tbool absolute;\n \tconst char *repair = NULL;\n \tchar *path = NULL;\n \tint err;\n@@ -667,16 +668,10 @@ static void repair_gitfile(struct worktree *wt,\n \tstrbuf_realpath(&repo, path, 1);\n \tstrbuf_addf(&dotgit, \"%s/.git\", wt->path);\n \tstrbuf_addf(&gitdir, \"%s/gitdir\", repo.buf);\n-\tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));\n+\tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &absolute, &err));\n \n-\tif (dotgit_contents) {\n-\t\tif (is_absolute_path(dotgit_contents)) {\n-\t\t\tstrbuf_addstr(&backlink, dotgit_contents);\n-\t\t} else {\n-\t\t\tstrbuf_addf(&backlink, \"%s/%s\", wt->path, dotgit_contents);\n-\t\t\tstrbuf_realpath_forgiving(&backlink, backlink.buf, 0);\n-\t\t}\n-\t}\n+\tif (dotgit_contents)\n+\t\tstrbuf_addstr(&backlink, dotgit_contents);\n \n \tif (err == READ_GITFILE_ERR_NOT_A_FILE ||\n \t\terr == READ_GITFILE_ERR_IS_A_DIR)\n@@ -685,7 +680,7 @@ static void repair_gitfile(struct worktree *wt,\n \t\trepair = _(\".git file broken\");\n \telse if (fspathcmp(backlink.buf, repo.buf))\n \t\trepair = _(\".git file incorrect\");\n-\telse if (use_relative_paths == is_absolute_path(dotgit_contents))\n+\telse if (use_relative_paths == absolute)\n \t\trepair = _(\".git file absolute/relative path mismatch\");\n \n \tif (repair) {\n@@ -855,16 +850,9 @@ void repair_worktree_at_path(struct repository *repo,\n \n \tinfer_backlink(repo, dotgit.buf, &inferred_backlink);\n \tstrbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0);\n-\tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));\n+\tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, NULL, &err));\n \tif (dotgit_contents) {\n-\t\tif (is_absolute_path(dotgit_contents)) {\n-\t\t\tstrbuf_addstr(&backlink, dotgit_contents);\n-\t\t} else {\n-\t\t\tstrbuf_addbuf(&backlink, &dotgit);\n-\t\t\tstrbuf_strip_suffix(&backlink, \".git\");\n-\t\t\tstrbuf_addstr(&backlink, dotgit_contents);\n-\t\t\tstrbuf_realpath_forgiving(&backlink, backlink.buf, 0);\n-\t\t}\n+\t\tstrbuf_addstr(&backlink, dotgit_contents);\n \t} else if (err == READ_GITFILE_ERR_NOT_A_FILE ||\n \t\t\terr == READ_GITFILE_ERR_IS_A_DIR) {\n \t\tfn(1, dotgit.buf, _(\"unable to locate repository; .git is not a file\"), cb_data);\n\nbase-commit: 11c6700f10234578d10523faf35656ca491425c9\n-- \ngitgitgadget\n"},{"id":"550712","messageId":"xmqqwlto4q9a.fsf@gitster.g","threadId":"66180","inReplyTo":"pull.2205.git.1786799480344.gitgitgadget@gmail.com","subject":"Re: [PATCH] worktree repair: detect relative path in .git file correctly","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-17T17:21:21Z","receivedAt":"2026-08-17T17:21:24Z","isPatch":true,"body":"\"Yoichi NAKAYAMA via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>\n>\n> Since read_gitfile_gently() always returns an absolute path, the\n> conversion from a relative path to an absolute path was not\n> functioning and dead code existed.\n\nThis is ugly.  What problem is this really fixing?  What \"conversion\nfrom a relative path to an absolute path\" does the above refer to?\nWhat \"dead code\"?  Where in what file and what function?  Why does\nthe caller even care if it is absolute or relative?  Shouldn't they\nwork equally well as long as they point at the right location?\n\nThe proposed log message hides so many details to evaluate the claim\nthat this is a good change, and raises many unanswered questions.\n\nYes, read_gitfile_gently() always turns the gitfile it reads into an\nabsolute form.  Is there a caller A that wants the underlying\nrelative form, and if so why?  Is it to compare with some other path\nthat is relative?  How did the code B obtained the other path to be\ncompared that is relative?  If that code B used the helper that is\ndifferent from read_gitfile_gently() to obtain the other path that\nis relative, perhaps the caller A can be changed to call it instead\nof calling read_gitfile_gently() and the fix can be done without\nchurning so many existing call sites?\n\nStepping back a bit, why does \"repair\" even care if it is relative?\nIs it considered a semi-error when a gitfile records its target as a\nrelative path?  If so, I wonder if a cleaner way may be to add a new\nREAD_GITFILE_ERR_RELATIVE_PATH constant that is treated as non-fatal\nerror by the read_gitfile_error_die() function?  If that approach\nworks, that may be the cleanest, as I suspect that \"was it recorded\nas an absolute path?\" will not stay to be the only special case in\nniche applications like \"repair\", but we need to audit callers of\nthe _gently() function and make sure they do not barf with the new\nreturn code.\n\nIf not, perhaps introduce a separate function that returns the path\nit read without any conversion, i.e.,\n\n    char *read_raw_gitfile(const char *path);\n\nthat \"repair\" thing can use, and have it do the relateve-to-absolute\nconveraion itself, perhaps?  That function would be created by moving\nmost of the code from read_gitfile_gently() and read_gitfile_gently()\nwould become a very thin wrapper around that function.  Wouldn't that\nbe the least invasive and cleanest solution, if it works?\n\nThanks.\n"},{"id":"550726","messageId":"CAF5D8-sLL+OAqrQG4ZWkxxNbsmtGPv_Y6GgEt6ULb3EqLHcNvA@mail.gmail.com","threadId":"66180","inReplyTo":"xmqqwlto4q9a.fsf@gitster.g","subject":"Re: [PATCH] worktree repair: detect relative path in .git file correctly","fromName":"Yoichi Nakayama","fromEmail":"yoichi.nakayama@gmail.com","sentAt":"2026-08-17T21:27:01Z","receivedAt":"2026-08-17T21:27:14Z","isPatch":true,"body":"On Tue, Aug 18, 2026 at 2:21 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> \"Yoichi NAKAYAMA via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n> > From: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>\n> >\n> > Since read_gitfile_gently() always returns an absolute path, the\n> > conversion from a relative path to an absolute path was not\n> > functioning and dead code existed.\n>\n> This is ugly.  What problem is this really fixing?  What \"conversion\n> from a relative path to an absolute path\" does the above refer to?\n> What \"dead code\"?  Where in what file and what function?  Why does\n> the caller even care if it is absolute or relative?  Shouldn't they\n> work equally well as long as they point at the right location?\n>\n> The proposed log message hides so many details to evaluate the claim\n> that this is a good change, and raises many unanswered questions.\n\nI'm sorry, the commit message lacked an explanation.\nLet me explain the details of the issue I want to resolve.\n\nWhen we create a worktree using default settings or with\n`worktree.useRelativePaths=false`,\nthe cross references between the worktree and the repository\n(specifically `worktree/id/gitdir`\nin the main repository and the `.git` file in the worktree) are\nrecorded using absolute paths.\n\n% mkdir repo\n% cd repo\nrepo % git init\nInitialized empty Git repository in /private/tmp/repo/.git/\nrepo % git commit --allow-empty -m init\n[master (root-commit) bb4f6a1] init\nrepo % git config worktree.useRelativePaths\nrepo % git worktree add ../foo --detach\nPreparing worktree (detached HEAD bb4f6a1)\nHEAD is now at bb4f6a1 init\nrepo % cat .git/worktrees/foo/gitdir\n/private/tmp/foo/.git\nrepo % cat ../foo/.git\ngitdir: /private/tmp/repo/.git/worktrees/foo\n\nIn this situation, if we change the setting to\n`worktree.useRelativePaths=true` and run\n `git worktree repair` within the main worktree, the cross references\nare converted to\nrelative paths (this is an expected behavior).\n\nrepo % git config worktree.useRelativePaths true\nrepo % git worktree repair\nrepair: .git file absolute/relative path mismatch: /private/tmp/foo\nrepo % cat .git/worktrees/foo/gitdir\n../../../../foo/.git\nrepo % cat ../foo/.git\ngitdir: ../repo/.git/worktrees/foo\n\nOn the other hand, given a state where cross references are recorded\nusing relative paths,\none would expect (by symmetry) that changing\n`worktree.useRelativePath` from `true` to `false`\nand running `git worktree repair` would convert the cross references\nto absolute paths. However,\nno \"absolute/relative path mismatch\" is detected, and the cross\nreferences remain as relative paths.\nThis is the problem I wanted to fix.\n\nrepo % cat .git/worktrees/foo/gitdir\n../../../../foo/.git\nrepo % cat ../foo/.git\ngitdir: ../repo/.git/worktrees/foo\nrepo % git config worktree.useRelativePaths false\nrepo % git worktree repair\nrepo % cat .git/worktrees/foo/gitdir\n../../../../foo/.git\nrepo % cat ../foo/.git\ngitdir: ../repo/.git/worktrees/foo\n\nThe issue has been present since the initial implementation:\n717af916cd (worktree: link worktrees with relative paths, 2024-10-07)\nAlthough `dotgit_contents` (retrieved via `read_gitfile_gently()`) is\nalways an absolute path,\nthe implementations of `repair_gitfile()` and\n`repair_worktree_at_path()` treat it as if the\nactual contents of the `.git` file had been returned.\n\nI have confirmed that the above issue can be reproduced even in the\nv2.48.0 tag, which was\nthe first release to include that change.\n\n> Yes, read_gitfile_gently() always turns the gitfile it reads into an\n> absolute form.  Is there a caller A that wants the underlying\n> relative form, and if so why?  Is it to compare with some other path\n> that is relative?  How did the code B obtained the other path to be\n> compared that is relative?  If that code B used the helper that is\n> different from read_gitfile_gently() to obtain the other path that\n> is relative, perhaps the caller A can be changed to call it instead\n> of calling read_gitfile_gently() and the fix can be done without\n> churning so many existing call sites?\n>\n> Stepping back a bit, why does \"repair\" even care if it is relative?\n> Is it considered a semi-error when a gitfile records its target as a\n> relative path?  If so, I wonder if a cleaner way may be to add a new\n> READ_GITFILE_ERR_RELATIVE_PATH constant that is treated as non-fatal\n> error by the read_gitfile_error_die() function?  If that approach\n> works, that may be the cleanest, as I suspect that \"was it recorded\n> as an absolute path?\" will not stay to be the only special case in\n> niche applications like \"repair\", but we need to audit callers of\n> the _gently() function and make sure they do not barf with the new\n> return code.\n>\n> If not, perhaps introduce a separate function that returns the path\n> it read without any conversion, i.e.,\n>\n>     char *read_raw_gitfile(const char *path);\n>\n> that \"repair\" thing can use, and have it do the relateve-to-absolute\n> converaion itself, perhaps?  That function would be created by moving\n> most of the code from read_gitfile_gently() and read_gitfile_gently()\n> would become a very thin wrapper around that function.  Wouldn't that\n> be the least invasive and cleanest solution, if it works?\n\nYou're right; changing the signature of `read_gitfile_gently` for a niche use\ncase like `worktree repair` isn't a good idea. I'll revise the\napproach to introduce\nsomething like the `read_raw_gitfile()` you suggested.\n\nThanks,\n-- \nYoichi NAKAYAMA\n"},{"id":"550916","messageId":"pull.2205.v2.git.1787240760069.gitgitgadget@gmail.com","threadId":"66180","inReplyTo":"pull.2205.git.1786799480344.gitgitgadget@gmail.com","subject":"[PATCH v2] worktree repair: detect relative path in .git file correctly","fromName":"Yoichi NAKAYAMA via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-20T15:46:00Z","receivedAt":"2026-08-20T15:46:04Z","isPatch":true,"body":"From: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>\n\nGiven a state where the cross references between the worktree and the\nrepository (specifically worktree/id/gitdir in the main repository and\nthe .git file in the worktree) are recorded using absolute paths,\nsetting 'worktree.useRelativePaths=true' and running 'git worktree\nrepair' within the main worktree converts them to relative paths.\n\nOn the other hand, given a state where the cross references are\nrecorded using relative paths, one would expect (by symmetry) that\nsetting 'worktree.useRelativePath=false' and running 'git worktree\nrepair' would convert them to absolute paths. However, they remain as\nrelative paths.\n\nThis is because we wrongly use read_gitfile_gently() which always\nreturns an absolute path. To fix this, introduce read_gitfile_raw()\nthat is almost same as read_gitfile_gently(), but it skips existence\ncheck of the referenced repository and returns the unmodified path\nread from .git file.\n\nSigned-off-by: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>\n---\n    worktree repair: detect relative path in .git file correctly\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2205%2Fyoichi%2Fworktree-repair-relative-path-handling-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2205/yoichi/worktree-repair-relative-path-handling-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/2205\n\nRange-diff vs v1:\n\n 1:  c97b948565 ! 1:  5bcf19ef50 worktree repair: detect relative path in .git file correctly\n     @@ Metadata\n       ## Commit message ##\n          worktree repair: detect relative path in .git file correctly\n      \n     -    Since read_gitfile_gently() always returns an absolute path, the\n     -    conversion from a relative path to an absolute path was not\n     -    functioning and dead code existed.\n     +    Given a state where the cross references between the worktree and the\n     +    repository (specifically worktree/id/gitdir in the main repository and\n     +    the .git file in the worktree) are recorded using absolute paths,\n     +    setting 'worktree.useRelativePaths=true' and running 'git worktree\n     +    repair' within the main worktree converts them to relative paths.\n      \n     -    Signed-off-by: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>\n     +    On the other hand, given a state where the cross references are\n     +    recorded using relative paths, one would expect (by symmetry) that\n     +    setting 'worktree.useRelativePath=false' and running 'git worktree\n     +    repair' would convert them to absolute paths. However, they remain as\n     +    relative paths.\n      \n     - ## builtin/init-db.c ##\n     -@@ builtin/init-db.c: int cmd_init_db(int argc,\n     - \t\tconst char *p;\n     - \t\tstruct strbuf sb = STRBUF_INIT;\n     - \n     --\t\tp = read_gitfile_gently(git_dir, &err);\n     -+\t\tp = read_gitfile_gently(git_dir, NULL, &err);\n     - \t\tif (p && get_common_dir(&sb, p)) {\n     - \t\t\tstruct strbuf mainwt = STRBUF_INIT;\n     - \n     +    This is because we wrongly use read_gitfile_gently() which always\n     +    returns an absolute path. To fix this, introduce read_gitfile_raw()\n     +    that is almost same as read_gitfile_gently(), but it skips existence\n     +    check of the referenced repository and returns the unmodified path\n     +    read from .git file.\n     +\n     +    Signed-off-by: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>\n      \n       ## setup.c ##\n     -@@ setup.c: int is_nonbare_repository_dir(struct strbuf *path)\n     - \tassert(orig_path_len != 0);\n     - \tstrbuf_complete(path, '/');\n     - \tstrbuf_addstr(path, \".git\");\n     --\tif (read_gitfile_gently(path->buf, &gitfile_error) || is_git_directory(path->buf))\n     -+\tif (read_gitfile_gently(path->buf, NULL, &gitfile_error) || is_git_directory(path->buf))\n     - \t\tret = 1;\n     - \tif (gitfile_error == READ_GITFILE_ERR_OPEN_FAILED ||\n     - \t    gitfile_error == READ_GITFILE_ERR_READ_FAILED)\n      @@ setup.c: void read_gitfile_error_die(int error_code, const char *path)\n     -  * return path to git directory if found. The return value comes from\n     -  * a shared buffer.\n     -  *\n     -+ * On success, if absolute is not NULL, it will be set to whether the\n     -+ * path in .git file is an absolute path.\n     -+ *\n     -  * On failure, if return_error_code is not NULL, return_error_code\n     -  * will be set to an error code and NULL will be returned. If\n     -  * return_error_code is NULL the function will die instead (for most\n     -  * cases).\n        */\n     --const char *read_gitfile_gently(const char *path, int *return_error_code)\n     -+const char *read_gitfile_gently(const char *path, bool *absolute, int *return_error_code)\n     + const char *read_gitfile_gently(const char *path, int *return_error_code)\n       {\n     - \tconst int max_file_size = 1 << 20;  /* 1MB */\n     +-\tconst int max_file_size = 1 << 20;  /* 1MB */\n       \tint error_code = 0;\n     + \tchar *buf = NULL;\n     +-\tchar *dir = NULL;\n     + \tconst char *slash;\n     ++\tstatic struct strbuf realpath = STRBUF_INIT;\n     ++\n     ++\tbuf = xstrdup_or_null(read_gitfile_raw(path, &error_code));\n     ++\tif (error_code)\n     ++\t\tgoto cleanup_return;\n     ++\n     ++\tif (!is_absolute_path(buf) && (slash = strrchr(path, '/'))) {\n     ++\t\tsize_t pathlen = slash+1 - path;\n     ++\t\tchar *dir = xstrfmt(\"%.*s%.*s\", (int)pathlen, path,\n     ++\t\t\t\t    (int)strlen(buf), buf);\n     ++\t\tfree(buf);\n     ++\t\tbuf = dir;\n     ++\t}\n     ++\tif (!is_git_directory(buf)) {\n     ++\t\terror_code = READ_GITFILE_ERR_NOT_A_REPO;\n     ++\t\tgoto cleanup_return;\n     ++\t}\n     ++\n     ++\tstrbuf_realpath(&realpath, buf, 1);\n     ++\n     ++cleanup_return:\n     ++\tif (return_error_code)\n     ++\t\t*return_error_code = error_code;\n     ++\telse if (error_code)\n     ++\t\tread_gitfile_error_die(error_code, path);\n     ++\n     ++\tfree(buf);\n     ++\treturn error_code ? NULL : realpath.buf;\n     ++}\n     ++\n     ++const char *read_gitfile_raw(const char *path, int *return_error_code)\n     ++{\n     ++\tconst int max_file_size = 1 << 20;  /* 1MB */\n     ++\tint error_code = 0;\n     ++\tchar *buf = NULL;\n     + \tstruct stat st;\n     + \tint fd;\n     + \tssize_t len;\n     +-\tstatic struct strbuf realpath = STRBUF_INIT;\n     ++\tstatic struct strbuf contents = STRBUF_INIT;\n     + \n     + \tif (stat(path, &st)) {\n     + \t\tif (errno == ENOENT || errno == ENOTDIR)\n      @@ setup.c: const char *read_gitfile_gently(const char *path, int *return_error_code)\n     + \t\terror_code = READ_GITFILE_ERR_NO_PATH;\n     + \t\tgoto cleanup_return;\n       \t}\n     - \tbuf[len] = '\\0';\n     - \tdir = buf + 8;\n     -+\tif (absolute)\n     -+\t\t*absolute = is_absolute_path(dir);\n     +-\tbuf[len] = '\\0';\n     +-\tdir = buf + 8;\n     +-\n     +-\tif (!is_absolute_path(dir) && (slash = strrchr(path, '/'))) {\n     +-\t\tsize_t pathlen = slash+1 - path;\n     +-\t\tdir = xstrfmt(\"%.*s%.*s\", (int)pathlen, path,\n     +-\t\t\t      (int)(len - 8), buf + 8);\n     +-\t\tfree(buf);\n     +-\t\tbuf = dir;\n     +-\t}\n     +-\tif (!is_git_directory(dir)) {\n     +-\t\terror_code = READ_GITFILE_ERR_NOT_A_REPO;\n     +-\t\tgoto cleanup_return;\n     +-\t}\n     +-\n     +-\tstrbuf_realpath(&realpath, dir, 1);\n     +-\tpath = realpath.buf;\n     ++\tstrbuf_reset(&contents);\n     ++\tstrbuf_add(&contents, buf+8, len-8);\n       \n     - \tif (!is_absolute_path(dir) && (slash = strrchr(path, '/'))) {\n     - \t\tsize_t pathlen = slash+1 - path;\n     -@@ setup.c: static enum discovery_result repo_discovery_find_dir(struct strbuf *dir,\n     - \t\tif (offset > min_offset)\n     - \t\t\tstrbuf_addch(dir, '/');\n     - \t\tstrbuf_addstr(dir, DEFAULT_GIT_DIR_ENVIRONMENT);\n     --\t\tgitdirenv = read_gitfile_gently(dir->buf, &error_code);\n     -+\t\tgitdirenv = read_gitfile_gently(dir->buf, NULL, &error_code);\n     - \t\tif (!gitdirenv) {\n     - \t\t\tswitch (error_code) {\n     - \t\t\tcase READ_GITFILE_ERR_MISSING:\n     -@@ setup.c: const char *resolve_gitdir_gently(const char *suspect, int *return_error_code)\n     - {\n     - \tif (is_git_directory(suspect))\n     - \t\treturn suspect;\n     --\treturn read_gitfile_gently(suspect, return_error_code);\n     -+\treturn read_gitfile_gently(suspect, NULL, return_error_code);\n     + cleanup_return:\n     +-\tif (return_error_code)\n     +-\t\t*return_error_code = error_code;\n     +-\telse if (error_code)\n     +-\t\tread_gitfile_error_die(error_code, path);\n     +-\n     ++\t*return_error_code = error_code;\n     + \tfree(buf);\n     +-\treturn error_code ? NULL : path;\n     ++\treturn error_code ? NULL : contents.buf;\n       }\n       \n     - /* if any standard file descriptor is missing open it to /dev/null */\n     + static void apply_gitdir_and_environment(struct repository *repo, const char *path)\n      \n       ## setup.h ##\n      @@ setup.h: int is_nonbare_repository_dir(struct strbuf *path);\n     - #define READ_GITFILE_ERR_MISSING 9\n       #define READ_GITFILE_ERR_IS_A_DIR 10\n       void read_gitfile_error_die(int error_code, const char *path);\n     --const char *read_gitfile_gently(const char *path, int *return_error_code);\n     --#define read_gitfile(path) read_gitfile_gently((path), NULL)\n     -+const char *read_gitfile_gently(const char *path, bool *absolute, int *return_error_code);\n     -+#define read_gitfile(path) read_gitfile_gently((path), NULL, NULL)\n     + const char *read_gitfile_gently(const char *path, int *return_error_code);\n     ++const char *read_gitfile_raw(const char *path, int *return_error_code);\n     + #define read_gitfile(path) read_gitfile_gently((path), NULL)\n       const char *resolve_gitdir_gently(const char *suspect, int *return_error_code);\n       #define resolve_gitdir(path) resolve_gitdir_gently((path), NULL)\n     - \n      \n       ## t/t2406-worktree-repair.sh ##\n      @@ t/t2406-worktree-repair.sh: test_expect_success 'repair worktree with relative path with missing gitfile' '\n     @@ t/t2406-worktree-repair.sh: test_expect_success 'repair worktree with relative p\n       \n      -test_expect_success 'repair absolute worktree to use relative paths' '\n      -\ttest_when_finished \"rm -rf main side sidemoved\" &&\n     -+test_expect_success 'repair absolute to relative in side worktree' '\n     ++test_expect_success 'repair absolute to relative from side worktree' '\n      +\ttest_when_finished \"rm -rf main side\" &&\n       \ttest_create_repo main &&\n       \ttest_commit -C main init &&\n     @@ t/t2406-worktree-repair.sh: test_expect_success 'repair worktree with relative p\n       \techo \"gitdir: ../main/.git/worktrees/side\" >expect-gitfile &&\n      -\tmv side sidemoved &&\n      -\tgit -C main worktree repair --relative-paths ../sidemoved &&\n     -+\tgit -C side worktree repair --relative-paths 2>main/err &&\n     ++\tgit -C main worktree repair --relative-paths ../side 2>main/err &&\n      +\ttest_grep \"gitdir absolute/relative path mismatch\" main/err &&\n       \ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n      -\ttest_cmp expect-gitfile sidemoved/.git\n     @@ t/t2406-worktree-repair.sh: test_expect_success 'repair worktree with relative p\n       \n      -test_expect_success 'repair relative worktree to use absolute paths' '\n      -\ttest_when_finished \"rm -rf main side sidemoved\" &&\n     -+test_expect_success 'repair relative to absolute in side worktree' '\n     ++test_expect_success 'repair relative to absolute from side worktree' '\n      +\ttest_when_finished \"rm -rf main side\" &&\n       \ttest_create_repo main &&\n       \ttest_commit -C main init &&\n     @@ t/t2406-worktree-repair.sh: test_expect_success 'repair worktree with relative p\n       \techo \"gitdir: $(pwd)/main/.git/worktrees/side\" >expect-gitfile &&\n      -\tmv side sidemoved &&\n      -\tgit -C main worktree repair ../sidemoved &&\n     -+\tgit -C side worktree repair 2>main/err &&\n     ++\tgit -C main worktree repair ../side 2>main/err &&\n      +\ttest_grep \"gitdir absolute/relative path mismatch\" main/err &&\n       \ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n      -\ttest_cmp expect-gitfile sidemoved/.git\n      +\ttest_cmp expect-gitfile side/.git\n      +'\n      +\n     -+test_expect_success 'repair absolute to relative in main worktree' '\n     ++test_expect_success 'repair absolute to relative from main worktree' '\n      +\ttest_when_finished \"rm -rf main side\" &&\n      +\ttest_create_repo main &&\n      +\tgit -C main config worktree.useRelativePaths false &&\n     @@ t/t2406-worktree-repair.sh: test_expect_success 'repair worktree with relative p\n      +\ttest_cmp expect-gitfile side/.git\n      +'\n      +\n     -+test_expect_success 'repair relative to absolute in main worktree' '\n     ++test_expect_success 'repair relative to absolute from main worktree' '\n      +\ttest_when_finished \"rm -rf main side\" &&\n      +\ttest_create_repo main &&\n      +\tgit -C main config worktree.useRelativePaths true &&\n     @@ t/t2406-worktree-repair.sh: test_expect_success 'repair worktree with relative p\n       test_done\n      \n       ## worktree.c ##\n     -@@ worktree.c: int validate_worktree(const struct worktree *wt, struct strbuf *errmsg,\n     - \t\tgoto done;\n     - \t}\n     - \n     --\tpath = xstrdup_or_null(read_gitfile_gently(wt_path.buf, &err));\n     -+\tpath = xstrdup_or_null(read_gitfile_gently(wt_path.buf, NULL, &err));\n     - \tif (!path) {\n     - \t\tstrbuf_addf_gently(errmsg, _(\"'%s' is not a .git file, error code %d\"),\n     - \t\t\t\t   wt_path.buf, err);\n     -@@ worktree.c: static void repair_gitfile(struct worktree *wt,\n     - \tstruct strbuf repo = STRBUF_INIT;\n     - \tstruct strbuf backlink = STRBUF_INIT;\n     - \tchar *dotgit_contents = NULL;\n     -+\tbool absolute;\n     - \tconst char *repair = NULL;\n     - \tchar *path = NULL;\n     - \tint err;\n      @@ worktree.c: static void repair_gitfile(struct worktree *wt,\n       \tstrbuf_realpath(&repo, path, 1);\n       \tstrbuf_addf(&dotgit, \"%s/.git\", wt->path);\n       \tstrbuf_addf(&gitdir, \"%s/gitdir\", repo.buf);\n      -\tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));\n     -+\tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &absolute, &err));\n     - \n     --\tif (dotgit_contents) {\n     --\t\tif (is_absolute_path(dotgit_contents)) {\n     --\t\t\tstrbuf_addstr(&backlink, dotgit_contents);\n     --\t\t} else {\n     --\t\t\tstrbuf_addf(&backlink, \"%s/%s\", wt->path, dotgit_contents);\n     --\t\t\tstrbuf_realpath_forgiving(&backlink, backlink.buf, 0);\n     --\t\t}\n     --\t}\n     -+\tif (dotgit_contents)\n     -+\t\tstrbuf_addstr(&backlink, dotgit_contents);\n     ++\tdotgit_contents = xstrdup_or_null(read_gitfile_raw(dotgit.buf, &err));\n       \n     + \tif (dotgit_contents) {\n     + \t\tif (is_absolute_path(dotgit_contents)) {\n     +@@ worktree.c: static void repair_gitfile(struct worktree *wt,\n       \tif (err == READ_GITFILE_ERR_NOT_A_FILE ||\n       \t\terr == READ_GITFILE_ERR_IS_A_DIR)\n     -@@ worktree.c: static void repair_gitfile(struct worktree *wt,\n     + \t\tfn(1, wt->path, _(\".git is not a file\"), cb_data);\n     +-\telse if (err)\n     ++\telse if (err || !is_git_directory(backlink.buf))\n       \t\trepair = _(\".git file broken\");\n       \telse if (fspathcmp(backlink.buf, repo.buf))\n       \t\trepair = _(\".git file incorrect\");\n     --\telse if (use_relative_paths == is_absolute_path(dotgit_contents))\n     -+\telse if (use_relative_paths == absolute)\n     - \t\trepair = _(\".git file absolute/relative path mismatch\");\n     - \n     - \tif (repair) {\n      @@ worktree.c: void repair_worktree_at_path(struct repository *repo,\n     - \n     - \tinfer_backlink(repo, dotgit.buf, &inferred_backlink);\n       \tstrbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0);\n     --\tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));\n     -+\tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, NULL, &err));\n     + \tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));\n       \tif (dotgit_contents) {\n      -\t\tif (is_absolute_path(dotgit_contents)) {\n      -\t\t\tstrbuf_addstr(&backlink, dotgit_contents);\n\n\n setup.c                    | 66 +++++++++++++++++++++++---------------\n setup.h                    |  1 +\n t/t2406-worktree-repair.sh | 54 ++++++++++++++++++++++++-------\n worktree.c                 | 13 ++------\n 4 files changed, 86 insertions(+), 48 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex 95909e9603..73111e08af 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -963,15 +963,48 @@ void read_gitfile_error_die(int error_code, const char *path)\n  */\n const char *read_gitfile_gently(const char *path, int *return_error_code)\n {\n-\tconst int max_file_size = 1 << 20;  /* 1MB */\n \tint error_code = 0;\n \tchar *buf = NULL;\n-\tchar *dir = NULL;\n \tconst char *slash;\n+\tstatic struct strbuf realpath = STRBUF_INIT;\n+\n+\tbuf = xstrdup_or_null(read_gitfile_raw(path, &error_code));\n+\tif (error_code)\n+\t\tgoto cleanup_return;\n+\n+\tif (!is_absolute_path(buf) && (slash = strrchr(path, '/'))) {\n+\t\tsize_t pathlen = slash+1 - path;\n+\t\tchar *dir = xstrfmt(\"%.*s%.*s\", (int)pathlen, path,\n+\t\t\t\t    (int)strlen(buf), buf);\n+\t\tfree(buf);\n+\t\tbuf = dir;\n+\t}\n+\tif (!is_git_directory(buf)) {\n+\t\terror_code = READ_GITFILE_ERR_NOT_A_REPO;\n+\t\tgoto cleanup_return;\n+\t}\n+\n+\tstrbuf_realpath(&realpath, buf, 1);\n+\n+cleanup_return:\n+\tif (return_error_code)\n+\t\t*return_error_code = error_code;\n+\telse if (error_code)\n+\t\tread_gitfile_error_die(error_code, path);\n+\n+\tfree(buf);\n+\treturn error_code ? NULL : realpath.buf;\n+}\n+\n+const char *read_gitfile_raw(const char *path, int *return_error_code)\n+{\n+\tconst int max_file_size = 1 << 20;  /* 1MB */\n+\tint error_code = 0;\n+\tchar *buf = NULL;\n \tstruct stat st;\n \tint fd;\n \tssize_t len;\n-\tstatic struct strbuf realpath = STRBUF_INIT;\n+\tstatic struct strbuf contents = STRBUF_INIT;\n \n \tif (stat(path, &st)) {\n \t\tif (errno == ENOENT || errno == ENOTDIR)\n@@ -1014,32 +1047,13 @@ const char *read_gitfile_gently(const char *path, int *return_error_code)\n \t\terror_code = READ_GITFILE_ERR_NO_PATH;\n \t\tgoto cleanup_return;\n \t}\n-\tbuf[len] = '\\0';\n-\tdir = buf + 8;\n-\n-\tif (!is_absolute_path(dir) && (slash = strrchr(path, '/'))) {\n-\t\tsize_t pathlen = slash+1 - path;\n-\t\tdir = xstrfmt(\"%.*s%.*s\", (int)pathlen, path,\n-\t\t\t      (int)(len - 8), buf + 8);\n-\t\tfree(buf);\n-\t\tbuf = dir;\n-\t}\n-\tif (!is_git_directory(dir)) {\n-\t\terror_code = READ_GITFILE_ERR_NOT_A_REPO;\n-\t\tgoto cleanup_return;\n-\t}\n-\n-\tstrbuf_realpath(&realpath, dir, 1);\n-\tpath = realpath.buf;\n+\tstrbuf_reset(&contents);\n+\tstrbuf_add(&contents, buf+8, len-8);\n \n cleanup_return:\n-\tif (return_error_code)\n-\t\t*return_error_code = error_code;\n-\telse if (error_code)\n-\t\tread_gitfile_error_die(error_code, path);\n-\n+\t*return_error_code = error_code;\n \tfree(buf);\n-\treturn error_code ? NULL : path;\n+\treturn error_code ? NULL : contents.buf;\n }\n \n static void apply_gitdir_and_environment(struct repository *repo, const char *path)\ndiff --git a/setup.h b/setup.h\nindex 654f10e059..442acd8954 100644\n--- a/setup.h\n+++ b/setup.h\n@@ -40,6 +40,7 @@ int is_nonbare_repository_dir(struct strbuf *path);\n #define READ_GITFILE_ERR_IS_A_DIR 10\n void read_gitfile_error_die(int error_code, const char *path);\n const char *read_gitfile_gently(const char *path, int *return_error_code);\n+const char *read_gitfile_raw(const char *path, int *return_error_code);\n #define read_gitfile(path) read_gitfile_gently((path), NULL)\n const char *resolve_gitdir_gently(const char *suspect, int *return_error_code);\n #define resolve_gitdir(path) resolve_gitdir_gently((path), NULL)\ndiff --git a/t/t2406-worktree-repair.sh b/t/t2406-worktree-repair.sh\nindex f5f19b3169..d4e53d492b 100755\n--- a/t/t2406-worktree-repair.sh\n+++ b/t/t2406-worktree-repair.sh\n@@ -228,30 +228,60 @@ test_expect_success 'repair worktree with relative path with missing gitfile' '\n \ttest_cmp expect wt/.git\n '\n \n-test_expect_success 'repair absolute worktree to use relative paths' '\n-\ttest_when_finished \"rm -rf main side sidemoved\" &&\n+test_expect_success 'repair absolute to relative from side worktree' '\n+\ttest_when_finished \"rm -rf main side\" &&\n \ttest_create_repo main &&\n \ttest_commit -C main init &&\n \tgit -C main worktree add --detach ../side &&\n-\techo \"../../../../sidemoved/.git\" >expect-gitdir &&\n+\techo \"../../../../side/.git\" >expect-gitdir &&\n \techo \"gitdir: ../main/.git/worktrees/side\" >expect-gitfile &&\n-\tmv side sidemoved &&\n-\tgit -C main worktree repair --relative-paths ../sidemoved &&\n+\tgit -C main worktree repair --relative-paths ../side 2>main/err &&\n+\ttest_grep \"gitdir absolute/relative path mismatch\" main/err &&\n \ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n-\ttest_cmp expect-gitfile sidemoved/.git\n+\ttest_cmp expect-gitfile side/.git\n '\n \n-test_expect_success 'repair relative worktree to use absolute paths' '\n-\ttest_when_finished \"rm -rf main side sidemoved\" &&\n+test_expect_success 'repair relative to absolute from side worktree' '\n+\ttest_when_finished \"rm -rf main side\" &&\n \ttest_create_repo main &&\n \ttest_commit -C main init &&\n \tgit -C main worktree add --relative-paths --detach ../side &&\n-\techo \"$(pwd)/sidemoved/.git\" >expect-gitdir &&\n+\techo \"$(pwd)/side/.git\" >expect-gitdir &&\n \techo \"gitdir: $(pwd)/main/.git/worktrees/side\" >expect-gitfile &&\n-\tmv side sidemoved &&\n-\tgit -C main worktree repair ../sidemoved &&\n+\tgit -C main worktree repair ../side 2>main/err &&\n+\ttest_grep \"gitdir absolute/relative path mismatch\" main/err &&\n \ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n-\ttest_cmp expect-gitfile sidemoved/.git\n+\ttest_cmp expect-gitfile side/.git\n+'\n+\n+test_expect_success 'repair absolute to relative from main worktree' '\n+\ttest_when_finished \"rm -rf main side\" &&\n+\ttest_create_repo main &&\n+\tgit -C main config worktree.useRelativePaths false &&\n+\ttest_commit -C main init &&\n+\tgit -C main worktree add --detach ../side &&\n+\techo \"../../../../side/.git\" >expect-gitdir &&\n+\techo \"gitdir: ../main/.git/worktrees/side\" >expect-gitfile &&\n+\tgit -C main config worktree.useRelativePaths true &&\n+\tgit -C main worktree repair 2>main/err &&\n+\ttest_grep \".git file absolute/relative path mismatch\" main/err &&\n+\ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n+\ttest_cmp expect-gitfile side/.git\n+'\n+\n+test_expect_success 'repair relative to absolute from main worktree' '\n+\ttest_when_finished \"rm -rf main side\" &&\n+\ttest_create_repo main &&\n+\tgit -C main config worktree.useRelativePaths true &&\n+\ttest_commit -C main init &&\n+\tgit -C main worktree add --detach ../side &&\n+\techo \"$(pwd)/side/.git\" >expect-gitdir &&\n+\techo \"gitdir: $(pwd)/main/.git/worktrees/side\" >expect-gitfile &&\n+\tgit -C main config worktree.useRelativePaths false &&\n+\tgit -C main worktree repair 2>main/err &&\n+\ttest_grep \".git file absolute/relative path mismatch\" main/err &&\n+\ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n+\ttest_cmp expect-gitfile side/.git\n '\n \n test_done\ndiff --git a/worktree.c b/worktree.c\nindex cbf95328a3..48e77636cf 100644\n--- a/worktree.c\n+++ b/worktree.c\n@@ -667,7 +667,7 @@ static void repair_gitfile(struct worktree *wt,\n \tstrbuf_realpath(&repo, path, 1);\n \tstrbuf_addf(&dotgit, \"%s/.git\", wt->path);\n \tstrbuf_addf(&gitdir, \"%s/gitdir\", repo.buf);\n-\tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));\n+\tdotgit_contents = xstrdup_or_null(read_gitfile_raw(dotgit.buf, &err));\n \n \tif (dotgit_contents) {\n \t\tif (is_absolute_path(dotgit_contents)) {\n@@ -681,7 +681,7 @@ static void repair_gitfile(struct worktree *wt,\n \tif (err == READ_GITFILE_ERR_NOT_A_FILE ||\n \t\terr == READ_GITFILE_ERR_IS_A_DIR)\n \t\tfn(1, wt->path, _(\".git is not a file\"), cb_data);\n-\telse if (err)\n+\telse if (err || !is_git_directory(backlink.buf))\n \t\trepair = _(\".git file broken\");\n \telse if (fspathcmp(backlink.buf, repo.buf))\n \t\trepair = _(\".git file incorrect\");\n@@ -857,14 +857,7 @@ void repair_worktree_at_path(struct repository *repo,\n \tstrbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0);\n \tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));\n \tif (dotgit_contents) {\n-\t\tif (is_absolute_path(dotgit_contents)) {\n-\t\t\tstrbuf_addstr(&backlink, dotgit_contents);\n-\t\t} else {\n-\t\t\tstrbuf_addbuf(&backlink, &dotgit);\n-\t\t\tstrbuf_strip_suffix(&backlink, \".git\");\n-\t\t\tstrbuf_addstr(&backlink, dotgit_contents);\n-\t\t\tstrbuf_realpath_forgiving(&backlink, backlink.buf, 0);\n-\t\t}\n+\t\tstrbuf_addstr(&backlink, dotgit_contents);\n \t} else if (err == READ_GITFILE_ERR_NOT_A_FILE ||\n \t\t\terr == READ_GITFILE_ERR_IS_A_DIR) {\n \t\tfn(1, dotgit.buf, _(\"unable to locate repository; .git is not a file\"), cb_data);\n\nbase-commit: dea0ea3582e6980ddbc1173cc8e3e9f9db91cde0\n-- \ngitgitgadget\n"},{"id":"550972","messageId":"xmqq1pbsteb5.fsf@gitster.g","threadId":"66180","inReplyTo":"pull.2205.v2.git.1787240760069.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] worktree repair: detect relative path in .git file correctly","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-21T02:09:18Z","receivedAt":"2026-08-21T02:09:21Z","isPatch":true,"body":"\"Yoichi NAKAYAMA via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> This is because we wrongly use read_gitfile_gently() which always\n> returns an absolute path. To fix this, introduce read_gitfile_raw()\n> that is almost same as read_gitfile_gently(), but it skips existence\n> check of the referenced repository and returns the unmodified path\n> read from .git file.\n\nThis is more or less what I expected to see, but two function-scope\nstatic variables are worse than one.  At least let us not\nproliferate the bad pattern that makes the functions non-reentrant.\n\nThe attached patch updates read_gitfile_raw() in your patch to take\na caller-prepared strbuf to store the value read from the '.git'\nfile, returning the error code as an integer.  Ideally in the far\nfuture, we would probably want to convert read_gitfile_gently() to\nfollow a similar function signature, but let us leave it as\n#leftoverbits, as it has many more existing callers and all of them\nwould need adjusting.  On the other hand, it is easier to get the API\nin read_gitfile_raw() right while it still has only two callers.\n\n\n setup.c | 9 +++------\n setup.h | 2 +-\n 2 files changed, 4 insertions(+), 7 deletions(-)\n\ndiff --git c/setup.c w/setup.c\nindex af7601ff67..052c7d669b 100644\n--- c/setup.c\n+++ w/setup.c\n@@ -996,7 +996,7 @@ const char *read_gitfile_gently(const char *path, int *return_error_code)\n \treturn error_code ? NULL : realpath.buf;\n }\n \n-const char *read_gitfile_raw(const char *path, int *return_error_code)\n+int read_gitfile_raw(struct strbuf *contents, const char *path)\n {\n \tconst int max_file_size = 1 << 20;  /* 1MB */\n \tint error_code = 0;\n@@ -1004,7 +1004,6 @@ const char *read_gitfile_raw(const char *path, int *return_error_code)\n \tstruct stat st;\n \tint fd;\n \tssize_t len;\n-\tstatic struct strbuf contents = STRBUF_INIT;\n \n \tif (stat(path, &st)) {\n \t\tif (errno == ENOENT || errno == ENOTDIR)\n@@ -1047,13 +1046,11 @@ const char *read_gitfile_raw(const char *path, int *return_error_code)\n \t\terror_code = READ_GITFILE_ERR_NO_PATH;\n \t\tgoto cleanup_return;\n \t}\n-\tstrbuf_reset(&contents);\n-\tstrbuf_add(&contents, buf+8, len-8);\n+\tstrbuf_add(contents, buf+8, len-8);\n \n cleanup_return:\n-\t*return_error_code = error_code;\n \tfree(buf);\n-\treturn error_code ? NULL : contents.buf;\n+\treturn error_code;\n }\n \n static void apply_gitdir_and_environment(struct repository *repo, const char *path)\ndiff --git c/setup.h w/setup.h\nindex 4c2fcbbeda..7394473e95 100644\n--- c/setup.h\n+++ w/setup.h\n@@ -40,7 +40,7 @@ int is_nonbare_repository_dir(struct strbuf *path);\n #define READ_GITFILE_ERR_IS_A_DIR 10\n void read_gitfile_error_die(int error_code, const char *path);\n const char *read_gitfile_gently(const char *path, int *return_error_code);\n-const char *read_gitfile_raw(const char *path, int *return_error_code);\n+int read_gitfile_raw(struct strbuf *contents, const char *path);\n #define read_gitfile(path) read_gitfile_gently((path), NULL)\n const char *resolve_gitdir_gently(const char *suspect, int *return_error_code);\n #define resolve_gitdir(path) resolve_gitdir_gently((path), NULL)\n"},{"id":"551037","messageId":"pull.2205.v3.git.1787344586470.gitgitgadget@gmail.com","threadId":"66180","inReplyTo":"pull.2205.git.1786799480344.gitgitgadget@gmail.com","subject":"[PATCH v3] worktree repair: detect relative path in .git file correctly","fromName":"Yoichi NAKAYAMA via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-21T20:36:26Z","receivedAt":"2026-08-21T20:36:28Z","isPatch":true,"body":"From: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>\n\nGiven a state in which the cross-references between the worktree and\nthe repository (specifically worktree/id/gitdir in the main repository\nand the .git file in the worktree) are recorded using absolute paths,\nsetting 'worktree.useRelativePaths=true' and running 'git worktree\nrepair' within the main worktree converts them to relative paths.\n\nConversely, given a state in which the cross-references are recorded\nusing relative paths, one would expect that setting\n'worktree.useRelativePaths=false' and running 'git worktree repair'\nwould convert them to absolute paths. However, they remain as relative\npaths.\n\nThis is because we incorrectly use read_gitfile_gently(), which always\nreturns an absolute path. To fix this, introduce read_gitfile_raw(),\nwhich is almost identical to read_gitfile_gently(), but skips checking\nthe existence of the referenced repository and returns the path as-is\nfrom the .git file.\n\nSigned-off-by: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>\n---\n    worktree repair: detect relative path in .git file correctly\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2205%2Fyoichi%2Fworktree-repair-relative-path-handling-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2205/yoichi/worktree-repair-relative-path-handling-v3\nPull-Request: https://github.com/gitgitgadget/git/pull/2205\n\nRange-diff vs v2:\n\n 1:  5bcf19ef50 ! 1:  1cd25e315e worktree repair: detect relative path in .git file correctly\n     @@ Metadata\n       ## Commit message ##\n          worktree repair: detect relative path in .git file correctly\n      \n     -    Given a state where the cross references between the worktree and the\n     -    repository (specifically worktree/id/gitdir in the main repository and\n     -    the .git file in the worktree) are recorded using absolute paths,\n     +    Given a state in which the cross-references between the worktree and\n     +    the repository (specifically worktree/id/gitdir in the main repository\n     +    and the .git file in the worktree) are recorded using absolute paths,\n          setting 'worktree.useRelativePaths=true' and running 'git worktree\n          repair' within the main worktree converts them to relative paths.\n      \n     -    On the other hand, given a state where the cross references are\n     -    recorded using relative paths, one would expect (by symmetry) that\n     -    setting 'worktree.useRelativePath=false' and running 'git worktree\n     -    repair' would convert them to absolute paths. However, they remain as\n     -    relative paths.\n     +    Conversely, given a state in which the cross-references are recorded\n     +    using relative paths, one would expect that setting\n     +    'worktree.useRelativePaths=false' and running 'git worktree repair'\n     +    would convert them to absolute paths. However, they remain as relative\n     +    paths.\n      \n     -    This is because we wrongly use read_gitfile_gently() which always\n     -    returns an absolute path. To fix this, introduce read_gitfile_raw()\n     -    that is almost same as read_gitfile_gently(), but it skips existence\n     -    check of the referenced repository and returns the unmodified path\n     -    read from .git file.\n     +    This is because we incorrectly use read_gitfile_gently(), which always\n     +    returns an absolute path. To fix this, introduce read_gitfile_raw(),\n     +    which is almost identical to read_gitfile_gently(), but skips checking\n     +    the existence of the referenced repository and returns the path as-is\n     +    from the .git file.\n      \n          Signed-off-by: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>\n      \n       ## setup.c ##\n      @@ setup.c: void read_gitfile_error_die(int error_code, const char *path)\n     +  * cases).\n        */\n       const char *read_gitfile_gently(const char *path, int *return_error_code)\n     - {\n     --\tconst int max_file_size = 1 << 20;  /* 1MB */\n     - \tint error_code = 0;\n     - \tchar *buf = NULL;\n     --\tchar *dir = NULL;\n     - \tconst char *slash;\n     ++{\n     ++\tint error_code = 0;\n     ++\tconst char *slash;\n     ++\tstruct strbuf contents = STRBUF_INIT;\n      +\tstatic struct strbuf realpath = STRBUF_INIT;\n      +\n     -+\tbuf = xstrdup_or_null(read_gitfile_raw(path, &error_code));\n     ++\terror_code = read_gitfile_raw(&contents, path);\n      +\tif (error_code)\n      +\t\tgoto cleanup_return;\n      +\n     -+\tif (!is_absolute_path(buf) && (slash = strrchr(path, '/'))) {\n     ++\tif (!is_absolute_path(contents.buf) && (slash = strrchr(path, '/'))) {\n      +\t\tsize_t pathlen = slash+1 - path;\n     -+\t\tchar *dir = xstrfmt(\"%.*s%.*s\", (int)pathlen, path,\n     -+\t\t\t\t    (int)strlen(buf), buf);\n     -+\t\tfree(buf);\n     -+\t\tbuf = dir;\n     ++\t\tchar *dir = xstrfmt(\"%.*s%s\", (int)pathlen, path, contents.buf);\n     ++\t\tstrbuf_reset(&contents);\n     ++\t\tstrbuf_addstr(&contents, dir);\n     ++\t\tfree(dir);\n      +\t}\n     -+\tif (!is_git_directory(buf)) {\n     ++\tif (!is_git_directory(contents.buf)) {\n      +\t\terror_code = READ_GITFILE_ERR_NOT_A_REPO;\n      +\t\tgoto cleanup_return;\n      +\t}\n      +\n     -+\tstrbuf_realpath(&realpath, buf, 1);\n     ++\tstrbuf_realpath(&realpath, contents.buf, 1);\n      +\n      +cleanup_return:\n      +\tif (return_error_code)\n     @@ setup.c: void read_gitfile_error_die(int error_code, const char *path)\n      +\telse if (error_code)\n      +\t\tread_gitfile_error_die(error_code, path);\n      +\n     -+\tfree(buf);\n     ++\tstrbuf_release(&contents);\n      +\treturn error_code ? NULL : realpath.buf;\n      +}\n      +\n     -+const char *read_gitfile_raw(const char *path, int *return_error_code)\n     -+{\n     -+\tconst int max_file_size = 1 << 20;  /* 1MB */\n     -+\tint error_code = 0;\n     -+\tchar *buf = NULL;\n     ++int read_gitfile_raw(struct strbuf *contents, const char *path)\n     + {\n     + \tconst int max_file_size = 1 << 20;  /* 1MB */\n     + \tint error_code = 0;\n     + \tchar *buf = NULL;\n     +-\tchar *dir = NULL;\n     +-\tconst char *slash;\n       \tstruct stat st;\n       \tint fd;\n       \tssize_t len;\n      -\tstatic struct strbuf realpath = STRBUF_INIT;\n     -+\tstatic struct strbuf contents = STRBUF_INIT;\n       \n       \tif (stat(path, &st)) {\n       \t\tif (errno == ENOENT || errno == ENOTDIR)\n     @@ setup.c: const char *read_gitfile_gently(const char *path, int *return_error_cod\n      -\n      -\tstrbuf_realpath(&realpath, dir, 1);\n      -\tpath = realpath.buf;\n     -+\tstrbuf_reset(&contents);\n     -+\tstrbuf_add(&contents, buf+8, len-8);\n     ++\tstrbuf_add(contents, buf+8, len-8);\n       \n       cleanup_return:\n      -\tif (return_error_code)\n     @@ setup.c: const char *read_gitfile_gently(const char *path, int *return_error_cod\n      -\telse if (error_code)\n      -\t\tread_gitfile_error_die(error_code, path);\n      -\n     -+\t*return_error_code = error_code;\n       \tfree(buf);\n      -\treturn error_code ? NULL : path;\n     -+\treturn error_code ? NULL : contents.buf;\n     ++\treturn error_code;\n       }\n       \n       static void apply_gitdir_and_environment(struct repository *repo, const char *path)\n     @@ setup.h: int is_nonbare_repository_dir(struct strbuf *path);\n       #define READ_GITFILE_ERR_IS_A_DIR 10\n       void read_gitfile_error_die(int error_code, const char *path);\n       const char *read_gitfile_gently(const char *path, int *return_error_code);\n     -+const char *read_gitfile_raw(const char *path, int *return_error_code);\n     ++int read_gitfile_raw(struct strbuf *contents, const char *path);\n       #define read_gitfile(path) read_gitfile_gently((path), NULL)\n       const char *resolve_gitdir_gently(const char *suspect, int *return_error_code);\n       #define resolve_gitdir(path) resolve_gitdir_gently((path), NULL)\n     @@ t/t2406-worktree-repair.sh: test_expect_success 'repair worktree with relative p\n       test_done\n      \n       ## worktree.c ##\n     +@@ worktree.c: static void repair_gitfile(struct worktree *wt,\n     + \tstruct strbuf gitdir = STRBUF_INIT;\n     + \tstruct strbuf repo = STRBUF_INIT;\n     + \tstruct strbuf backlink = STRBUF_INIT;\n     +-\tchar *dotgit_contents = NULL;\n     ++\tstruct strbuf contents = STRBUF_INIT;\n     ++\tconst char *dotgit_contents = NULL;\n     + \tconst char *repair = NULL;\n     + \tchar *path = NULL;\n     + \tint err;\n      @@ worktree.c: static void repair_gitfile(struct worktree *wt,\n       \tstrbuf_realpath(&repo, path, 1);\n       \tstrbuf_addf(&dotgit, \"%s/.git\", wt->path);\n       \tstrbuf_addf(&gitdir, \"%s/gitdir\", repo.buf);\n      -\tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));\n     -+\tdotgit_contents = xstrdup_or_null(read_gitfile_raw(dotgit.buf, &err));\n     ++\terr = read_gitfile_raw(&contents, dotgit.buf);\n     ++\tif (!err)\n     ++\t\tdotgit_contents = contents.buf;\n       \n       \tif (dotgit_contents) {\n       \t\tif (is_absolute_path(dotgit_contents)) {\n     @@ worktree.c: static void repair_gitfile(struct worktree *wt,\n       \t\trepair = _(\".git file broken\");\n       \telse if (fspathcmp(backlink.buf, repo.buf))\n       \t\trepair = _(\".git file incorrect\");\n     +@@ worktree.c: static void repair_gitfile(struct worktree *wt,\n     + \t}\n     + \n     + done:\n     +-\tfree(dotgit_contents);\n     + \tfree(path);\n     + \tstrbuf_release(&repo);\n     + \tstrbuf_release(&dotgit);\n     + \tstrbuf_release(&gitdir);\n     + \tstrbuf_release(&backlink);\n     ++\tstrbuf_release(&contents);\n     + }\n     + \n     + static void repair_noop(int iserr UNUSED,\n      @@ worktree.c: void repair_worktree_at_path(struct repository *repo,\n       \tstrbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0);\n       \tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));\n\n\n setup.c                    | 63 ++++++++++++++++++++++----------------\n setup.h                    |  1 +\n t/t2406-worktree-repair.sh | 54 ++++++++++++++++++++++++--------\n worktree.c                 | 20 +++++-------\n 4 files changed, 88 insertions(+), 50 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex 95909e9603..9041827336 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -962,16 +962,48 @@ void read_gitfile_error_die(int error_code, const char *path)\n  * cases).\n  */\n const char *read_gitfile_gently(const char *path, int *return_error_code)\n+{\n+\tint error_code = 0;\n+\tconst char *slash;\n+\tstruct strbuf contents = STRBUF_INIT;\n+\tstatic struct strbuf realpath = STRBUF_INIT;\n+\n+\terror_code = read_gitfile_raw(&contents, path);\n+\tif (error_code)\n+\t\tgoto cleanup_return;\n+\n+\tif (!is_absolute_path(contents.buf) && (slash = strrchr(path, '/'))) {\n+\t\tsize_t pathlen = slash+1 - path;\n+\t\tchar *dir = xstrfmt(\"%.*s%s\", (int)pathlen, path, contents.buf);\n+\t\tstrbuf_reset(&contents);\n+\t\tstrbuf_addstr(&contents, dir);\n+\t\tfree(dir);\n+\t}\n+\tif (!is_git_directory(contents.buf)) {\n+\t\terror_code = READ_GITFILE_ERR_NOT_A_REPO;\n+\t\tgoto cleanup_return;\n+\t}\n+\n+\tstrbuf_realpath(&realpath, contents.buf, 1);\n+\n+cleanup_return:\n+\tif (return_error_code)\n+\t\t*return_error_code = error_code;\n+\telse if (error_code)\n+\t\tread_gitfile_error_die(error_code, path);\n+\n+\tstrbuf_release(&contents);\n+\treturn error_code ? NULL : realpath.buf;\n+}\n+\n+int read_gitfile_raw(struct strbuf *contents, const char *path)\n {\n \tconst int max_file_size = 1 << 20;  /* 1MB */\n \tint error_code = 0;\n \tchar *buf = NULL;\n-\tchar *dir = NULL;\n-\tconst char *slash;\n \tstruct stat st;\n \tint fd;\n \tssize_t len;\n-\tstatic struct strbuf realpath = STRBUF_INIT;\n \n \tif (stat(path, &st)) {\n \t\tif (errno == ENOENT || errno == ENOTDIR)\n@@ -1014,32 +1046,11 @@ const char *read_gitfile_gently(const char *path, int *return_error_code)\n \t\terror_code = READ_GITFILE_ERR_NO_PATH;\n \t\tgoto cleanup_return;\n \t}\n-\tbuf[len] = '\\0';\n-\tdir = buf + 8;\n-\n-\tif (!is_absolute_path(dir) && (slash = strrchr(path, '/'))) {\n-\t\tsize_t pathlen = slash+1 - path;\n-\t\tdir = xstrfmt(\"%.*s%.*s\", (int)pathlen, path,\n-\t\t\t      (int)(len - 8), buf + 8);\n-\t\tfree(buf);\n-\t\tbuf = dir;\n-\t}\n-\tif (!is_git_directory(dir)) {\n-\t\terror_code = READ_GITFILE_ERR_NOT_A_REPO;\n-\t\tgoto cleanup_return;\n-\t}\n-\n-\tstrbuf_realpath(&realpath, dir, 1);\n-\tpath = realpath.buf;\n+\tstrbuf_add(contents, buf+8, len-8);\n \n cleanup_return:\n-\tif (return_error_code)\n-\t\t*return_error_code = error_code;\n-\telse if (error_code)\n-\t\tread_gitfile_error_die(error_code, path);\n-\n \tfree(buf);\n-\treturn error_code ? NULL : path;\n+\treturn error_code;\n }\n \n static void apply_gitdir_and_environment(struct repository *repo, const char *path)\ndiff --git a/setup.h b/setup.h\nindex 654f10e059..e6e71bda3d 100644\n--- a/setup.h\n+++ b/setup.h\n@@ -40,6 +40,7 @@ int is_nonbare_repository_dir(struct strbuf *path);\n #define READ_GITFILE_ERR_IS_A_DIR 10\n void read_gitfile_error_die(int error_code, const char *path);\n const char *read_gitfile_gently(const char *path, int *return_error_code);\n+int read_gitfile_raw(struct strbuf *contents, const char *path);\n #define read_gitfile(path) read_gitfile_gently((path), NULL)\n const char *resolve_gitdir_gently(const char *suspect, int *return_error_code);\n #define resolve_gitdir(path) resolve_gitdir_gently((path), NULL)\ndiff --git a/t/t2406-worktree-repair.sh b/t/t2406-worktree-repair.sh\nindex f5f19b3169..d4e53d492b 100755\n--- a/t/t2406-worktree-repair.sh\n+++ b/t/t2406-worktree-repair.sh\n@@ -228,30 +228,60 @@ test_expect_success 'repair worktree with relative path with missing gitfile' '\n \ttest_cmp expect wt/.git\n '\n \n-test_expect_success 'repair absolute worktree to use relative paths' '\n-\ttest_when_finished \"rm -rf main side sidemoved\" &&\n+test_expect_success 'repair absolute to relative from side worktree' '\n+\ttest_when_finished \"rm -rf main side\" &&\n \ttest_create_repo main &&\n \ttest_commit -C main init &&\n \tgit -C main worktree add --detach ../side &&\n-\techo \"../../../../sidemoved/.git\" >expect-gitdir &&\n+\techo \"../../../../side/.git\" >expect-gitdir &&\n \techo \"gitdir: ../main/.git/worktrees/side\" >expect-gitfile &&\n-\tmv side sidemoved &&\n-\tgit -C main worktree repair --relative-paths ../sidemoved &&\n+\tgit -C main worktree repair --relative-paths ../side 2>main/err &&\n+\ttest_grep \"gitdir absolute/relative path mismatch\" main/err &&\n \ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n-\ttest_cmp expect-gitfile sidemoved/.git\n+\ttest_cmp expect-gitfile side/.git\n '\n \n-test_expect_success 'repair relative worktree to use absolute paths' '\n-\ttest_when_finished \"rm -rf main side sidemoved\" &&\n+test_expect_success 'repair relative to absolute from side worktree' '\n+\ttest_when_finished \"rm -rf main side\" &&\n \ttest_create_repo main &&\n \ttest_commit -C main init &&\n \tgit -C main worktree add --relative-paths --detach ../side &&\n-\techo \"$(pwd)/sidemoved/.git\" >expect-gitdir &&\n+\techo \"$(pwd)/side/.git\" >expect-gitdir &&\n \techo \"gitdir: $(pwd)/main/.git/worktrees/side\" >expect-gitfile &&\n-\tmv side sidemoved &&\n-\tgit -C main worktree repair ../sidemoved &&\n+\tgit -C main worktree repair ../side 2>main/err &&\n+\ttest_grep \"gitdir absolute/relative path mismatch\" main/err &&\n \ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n-\ttest_cmp expect-gitfile sidemoved/.git\n+\ttest_cmp expect-gitfile side/.git\n+'\n+\n+test_expect_success 'repair absolute to relative from main worktree' '\n+\ttest_when_finished \"rm -rf main side\" &&\n+\ttest_create_repo main &&\n+\tgit -C main config worktree.useRelativePaths false &&\n+\ttest_commit -C main init &&\n+\tgit -C main worktree add --detach ../side &&\n+\techo \"../../../../side/.git\" >expect-gitdir &&\n+\techo \"gitdir: ../main/.git/worktrees/side\" >expect-gitfile &&\n+\tgit -C main config worktree.useRelativePaths true &&\n+\tgit -C main worktree repair 2>main/err &&\n+\ttest_grep \".git file absolute/relative path mismatch\" main/err &&\n+\ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n+\ttest_cmp expect-gitfile side/.git\n+'\n+\n+test_expect_success 'repair relative to absolute from main worktree' '\n+\ttest_when_finished \"rm -rf main side\" &&\n+\ttest_create_repo main &&\n+\tgit -C main config worktree.useRelativePaths true &&\n+\ttest_commit -C main init &&\n+\tgit -C main worktree add --detach ../side &&\n+\techo \"$(pwd)/side/.git\" >expect-gitdir &&\n+\techo \"gitdir: $(pwd)/main/.git/worktrees/side\" >expect-gitfile &&\n+\tgit -C main config worktree.useRelativePaths false &&\n+\tgit -C main worktree repair 2>main/err &&\n+\ttest_grep \".git file absolute/relative path mismatch\" main/err &&\n+\ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n+\ttest_cmp expect-gitfile side/.git\n '\n \n test_done\ndiff --git a/worktree.c b/worktree.c\nindex cbf95328a3..8cb8637b18 100644\n--- a/worktree.c\n+++ b/worktree.c\n@@ -649,7 +649,8 @@ static void repair_gitfile(struct worktree *wt,\n \tstruct strbuf gitdir = STRBUF_INIT;\n \tstruct strbuf repo = STRBUF_INIT;\n \tstruct strbuf backlink = STRBUF_INIT;\n-\tchar *dotgit_contents = NULL;\n+\tstruct strbuf contents = STRBUF_INIT;\n+\tconst char *dotgit_contents = NULL;\n \tconst char *repair = NULL;\n \tchar *path = NULL;\n \tint err;\n@@ -667,7 +668,9 @@ static void repair_gitfile(struct worktree *wt,\n \tstrbuf_realpath(&repo, path, 1);\n \tstrbuf_addf(&dotgit, \"%s/.git\", wt->path);\n \tstrbuf_addf(&gitdir, \"%s/gitdir\", repo.buf);\n-\tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));\n+\terr = read_gitfile_raw(&contents, dotgit.buf);\n+\tif (!err)\n+\t\tdotgit_contents = contents.buf;\n \n \tif (dotgit_contents) {\n \t\tif (is_absolute_path(dotgit_contents)) {\n@@ -681,7 +684,7 @@ static void repair_gitfile(struct worktree *wt,\n \tif (err == READ_GITFILE_ERR_NOT_A_FILE ||\n \t\terr == READ_GITFILE_ERR_IS_A_DIR)\n \t\tfn(1, wt->path, _(\".git is not a file\"), cb_data);\n-\telse if (err)\n+\telse if (err || !is_git_directory(backlink.buf))\n \t\trepair = _(\".git file broken\");\n \telse if (fspathcmp(backlink.buf, repo.buf))\n \t\trepair = _(\".git file incorrect\");\n@@ -695,12 +698,12 @@ static void repair_gitfile(struct worktree *wt,\n \t}\n \n done:\n-\tfree(dotgit_contents);\n \tfree(path);\n \tstrbuf_release(&repo);\n \tstrbuf_release(&dotgit);\n \tstrbuf_release(&gitdir);\n \tstrbuf_release(&backlink);\n+\tstrbuf_release(&contents);\n }\n \n static void repair_noop(int iserr UNUSED,\n@@ -857,14 +860,7 @@ void repair_worktree_at_path(struct repository *repo,\n \tstrbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0);\n \tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));\n \tif (dotgit_contents) {\n-\t\tif (is_absolute_path(dotgit_contents)) {\n-\t\t\tstrbuf_addstr(&backlink, dotgit_contents);\n-\t\t} else {\n-\t\t\tstrbuf_addbuf(&backlink, &dotgit);\n-\t\t\tstrbuf_strip_suffix(&backlink, \".git\");\n-\t\t\tstrbuf_addstr(&backlink, dotgit_contents);\n-\t\t\tstrbuf_realpath_forgiving(&backlink, backlink.buf, 0);\n-\t\t}\n+\t\tstrbuf_addstr(&backlink, dotgit_contents);\n \t} else if (err == READ_GITFILE_ERR_NOT_A_FILE ||\n \t\t\terr == READ_GITFILE_ERR_IS_A_DIR) {\n \t\tfn(1, dotgit.buf, _(\"unable to locate repository; .git is not a file\"), cb_data);\n\nbase-commit: dea0ea3582e6980ddbc1173cc8e3e9f9db91cde0\n-- \ngitgitgadget\n"},{"id":"551039","messageId":"xmqq8q5zyvwd.fsf@gitster.g","threadId":"66180","inReplyTo":"pull.2205.v3.git.1787344586470.gitgitgadget@gmail.com","subject":"Re: [PATCH v3] worktree repair: detect relative path in .git file correctly","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-21T22:02:42Z","receivedAt":"2026-08-21T22:02:45Z","isPatch":true,"body":"\"Yoichi NAKAYAMA via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>\n>\n> Given a state in which the cross-references between the worktree and\n> the repository (specifically worktree/id/gitdir in the main repository\n> and the .git file in the worktree) are recorded using absolute paths,\n> setting 'worktree.useRelativePaths=true' and running 'git worktree\n> repair' within the main worktree converts them to relative paths.\n>\n> Conversely, given a state in which the cross-references are recorded\n> using relative paths, one would expect that setting\n> 'worktree.useRelativePaths=false' and running 'git worktree repair'\n> would convert them to absolute paths. However, they remain as relative\n> paths.\n>\n> This is because we incorrectly use read_gitfile_gently(), which always\n> returns an absolute path. To fix this, introduce read_gitfile_raw(),\n> which is almost identical to read_gitfile_gently(), but skips checking\n> the existence of the referenced repository and returns the path as-is\n> from the .git file.\n\nExcellent observation of the problem addressed by the patch.  I wish\neverybody wrote his or her proposed log message this clearly.\n\n> diff --git a/setup.c b/setup.c\n> index 95909e9603..9041827336 100644\n> --- a/setup.c\n> +++ b/setup.c\n> @@ -962,16 +962,48 @@ void read_gitfile_error_die(int error_code, const char *path)\n>   * cases).\n>   */\n>  const char *read_gitfile_gently(const char *path, int *return_error_code)\n> +{\n> +\tint error_code = 0;\n> +\tconst char *slash;\n> +\tstruct strbuf contents = STRBUF_INIT;\n> +\tstatic struct strbuf realpath = STRBUF_INIT;\n> +\n> +\terror_code = read_gitfile_raw(&contents, path);\n> +\tif (error_code)\n> +\t\tgoto cleanup_return;\n> +\n> +\tif (!is_absolute_path(contents.buf) && (slash = strrchr(path, '/'))) {\n> +\t\tsize_t pathlen = slash+1 - path;\n> +\t\tchar *dir = xstrfmt(\"%.*s%s\", (int)pathlen, path, contents.buf);\n> +\t\tstrbuf_reset(&contents);\n> +\t\tstrbuf_addstr(&contents, dir);\n> +\t\tfree(dir);\n> +\t}\n\nThis massages path = \"worktrees/foo/.git\" into \"worktrees/foo\". And\nthe non-absolute contents.buf \"../main/.git/worktrees/foo\" that is\nrelative to gitfile is turned into relative to cwd of our process by\nprepending \"worktrees/foo\" to it.\n\n> +\tif (!is_git_directory(contents.buf)) {\n> +\t\terror_code = READ_GITFILE_ERR_NOT_A_REPO;\n> +\t\tgoto cleanup_return;\n> +\t}\n\nThis ensures that the thing referenced by .git file (i.e., what\ncomes after \"gitdir:\") is a sanely formatted git directory.\n\n> +\tstrbuf_realpath(&realpath, contents.buf, 1);\n\nThis turns the thing into an absolute path.\n\nAmong these three, the last one obviously belongs here.  Leaving the\nrelative path relative was the reason why we wanted to add\nread_gitfile_raw() in the first place.\n\nBut moving the other two to here is a bit iffy.  The worktree repair\njob used to call read_gitfile_gently(), which means it used to\ndepend on what the first two did for it, namely, to make the\nrelative path after \"gitdir:\" from the .git file relative to the\ncurrent process to make it usable, and to ensure that the directory\npointed at by .git is indeed a git directory.  Is it correct to drop\nthese from the caller, which now calls read_gitfile_raw() instead?\n\nIOW, I am not sure if the two functions are split correctly.  I\nexpected that the only two things read_gitfile_gently() would do\nafter read_gitfile_raw() are (1) upon error, jump to cleanup_return,\nand (2) otherwise call strbuf_realpath().\n\nThanks.\n"},{"id":"551040","messageId":"xmqq4ignyv1z.fsf@gitster.g","threadId":"66180","inReplyTo":"xmqq8q5zyvwd.fsf@gitster.g","subject":"Re: [PATCH v3] worktree repair: detect relative path in .git file correctly","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-21T22:20:56Z","receivedAt":"2026-08-21T22:20:58Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Among these three, the last one obviously belongs here.  Leaving the\n> relative path relative was the reason why we wanted to add\n> read_gitfile_raw() in the first place.\n>\n> But moving the other two to here is a bit iffy.  The worktree repair\n> job used to call read_gitfile_gently(), which means it used to\n> depend on what the first two did for it, namely, to make the\n> relative path after \"gitdir:\" from the .git file relative to the\n> current process to make it usable, and to ensure that the directory\n> pointed at by .git is indeed a git directory.  Is it correct to drop\n> these from the caller, which now calls read_gitfile_raw() instead?\n>\n> IOW, I am not sure if the two functions are split correctly.  I\n> expected that the only two things read_gitfile_gently() would do\n> after read_gitfile_raw() are (1) upon error, jump to cleanup_return,\n> and (2) otherwise call strbuf_realpath().\n\nActually, I take half of that back.  If we pretend the leading part\nof the \"path\", which could be absolute, the result will lose the\nrelative-ness of the original.  Keeping the tweaking of the relative\npath in read_gitfile_gently() is reasonable.  As is_git_directory()\nneeds to be called on a usable path, if the relative path tweaking\ncannot be done inside read_gitfile_raw(), it cannot check if the\ndirectory is is_git_directory(), either.\n\nSo, the change to setup.c is fine as is.  I didn't look at the\nchanges to worktree.c, though.\n\nThanks.\n"},{"id":"551359","messageId":"CAF5D8-vocLWba-rvKxy3WWB1ZHTh1+eRcRWiMqv0M-CX56Y71A@mail.gmail.com","threadId":"66180","inReplyTo":"xmqq4ignyv1z.fsf@gitster.g","subject":"Re: [PATCH v3] worktree repair: detect relative path in .git file correctly","fromName":"Yoichi Nakayama","fromEmail":"yoichi.nakayama@gmail.com","sentAt":"2026-08-27T14:38:15Z","receivedAt":"2026-08-27T14:38:27Z","isPatch":true,"body":"On Sat, Aug 22, 2026 at 7:21 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n> > Among these three, the last one obviously belongs here.  Leaving the\n> > relative path relative was the reason why we wanted to add\n> > read_gitfile_raw() in the first place.\n> >\n> > But moving the other two to here is a bit iffy.  The worktree repair\n> > job used to call read_gitfile_gently(), which means it used to\n> > depend on what the first two did for it, namely, to make the\n> > relative path after \"gitdir:\" from the .git file relative to the\n> > current process to make it usable, and to ensure that the directory\n> > pointed at by .git is indeed a git directory.  Is it correct to drop\n> > these from the caller, which now calls read_gitfile_raw() instead?\n> >\n> > IOW, I am not sure if the two functions are split correctly.  I\n> > expected that the only two things read_gitfile_gently() would do\n> > after read_gitfile_raw() are (1) upon error, jump to cleanup_return,\n> > and (2) otherwise call strbuf_realpath().\n>\n> Actually, I take half of that back.  If we pretend the leading part\n> of the \"path\", which could be absolute, the result will lose the\n> relative-ness of the original.  Keeping the tweaking of the relative\n> path in read_gitfile_gently() is reasonable.  As is_git_directory()\n> needs to be called on a usable path, if the relative path tweaking\n> cannot be done inside read_gitfile_raw(), it cannot check if the\n> directory is is_git_directory(), either.\n>\n> So, the change to setup.c is fine as is.  I didn't look at the\n> changes to worktree.c, though.\n\nIf we were to keep the call to `is_git_directory()` inside `read_gitfile_raw()`,\nit is necessary to calculate the absolute path of the candidate.\nWhile it is possible to calculate the path in `read_gitfile_raw()`,\ncall `is_git_directory()`, and then discard the calculated path,\nI felt it was wasteful to calculate the absolute path twice when\n`read_gitfile_raw()` is called from `read_gitfile_gently()`.\n\nFrom another perspective, while the function name `read_gitfile_*()`\nsuggests its role is simply to read the `.git` file, I felt that verifying\nwhether the resulting path is a valid git directory went beyond that\nscope.\n\nI understand the desire to minimize the functional differences\nbetween `read_gitfile_raw()` and `read_gitfile_gently()`, but for the\nreasons mentioned above, I have moved the check performed by\n`is_git_directory()` to the caller of `read_gitfile_raw()` within worktree.c.\n\nI have moved the `is_git_directory()` call to worktree.c so as not\nto alter the behavior when a `.git` file points to a location other\nthan any git directory, but I didn't mention it in the commit message.\n\nAre you concerned about the lack of explanation in the commit\nmessage, or about the functional differences between\n`read_gitfile_raw()` and `read_gitfile_gently()`?\n\nThanks,\n-- \nYoichi NAKAYAMA\n"},{"id":"551378","messageId":"xmqqse3za47f.fsf@gitster.g","threadId":"66180","inReplyTo":"CAF5D8-vocLWba-rvKxy3WWB1ZHTh1+eRcRWiMqv0M-CX56Y71A@mail.gmail.com","subject":"Re: [PATCH v3] worktree repair: detect relative path in .git file correctly","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-27T17:00:36Z","receivedAt":"2026-08-27T17:00:38Z","isPatch":true,"body":"Yoichi Nakayama <yoichi.nakayama@gmail.com> writes:\n\n> Are you concerned about the lack of explanation in the commit\n> message, or about the functional differences between\n> `read_gitfile_raw()` and `read_gitfile_gently()`?\n\nMostly the former, i.e., the commit message too sketchy.\n\nI now understand that these two functions need to be more different\nthan just the _raw() not calling strbuf_realpath() and the other\ncalling strbuf_realpath().  The functional split is fine.\n\nThanks.\n\n"},{"id":"551422","messageId":"pull.2205.v4.git.1787930386252.gitgitgadget@gmail.com","threadId":"66180","inReplyTo":"pull.2205.git.1786799480344.gitgitgadget@gmail.com","subject":"[PATCH v4] worktree repair: detect relative path in .git file correctly","fromName":"Yoichi NAKAYAMA via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-28T15:19:46Z","receivedAt":"2026-08-28T15:19:49Z","isPatch":true,"body":"From: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>\n\nGiven a state in which the cross-references between the worktree and\nthe repository (specifically worktree/id/gitdir in the main repository\nand the .git file in the worktree) are recorded using absolute paths,\nsetting 'worktree.useRelativePaths=true' and running 'git worktree\nrepair' within the main worktree converts them to relative paths.\n\nConversely, given a state in which the cross-references are recorded\nusing relative paths, one would expect that setting\n'worktree.useRelativePaths=false' and running 'git worktree repair'\nwould convert them to absolute paths. However, they remain as relative\npaths.\n\nThis is because we incorrectly use read_gitfile_gently(), which always\nreturns an absolute path. To fix this, introduce read_gitfile_raw(),\nwhich reads the path from the .git file without resolving it to an\nabsolute path.\n\nBecause read_gitfile_raw() does not validate the path with\nis_git_directory(), repair_gitfile() performs this validation to\npreserve the existing behavior.\n\nSigned-off-by: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>\n---\n    worktree repair: detect relative path in .git file correctly\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2205%2Fyoichi%2Fworktree-repair-relative-path-handling-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2205/yoichi/worktree-repair-relative-path-handling-v4\nPull-Request: https://github.com/gitgitgadget/git/pull/2205\n\nRange-diff vs v3:\n\n 1:  1cd25e315e ! 1:  abff315880 worktree repair: detect relative path in .git file correctly\n     @@ Commit message\n      \n          This is because we incorrectly use read_gitfile_gently(), which always\n          returns an absolute path. To fix this, introduce read_gitfile_raw(),\n     -    which is almost identical to read_gitfile_gently(), but skips checking\n     -    the existence of the referenced repository and returns the path as-is\n     -    from the .git file.\n     +    which reads the path from the .git file without resolving it to an\n     +    absolute path.\n     +\n     +    Because read_gitfile_raw() does not validate the path with\n     +    is_git_directory(), repair_gitfile() performs this validation to\n     +    preserve the existing behavior.\n      \n          Signed-off-by: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>\n      \n     @@ setup.c: void read_gitfile_error_die(int error_code, const char *path)\n      +\treturn error_code ? NULL : realpath.buf;\n      +}\n      +\n     ++/*\n     ++ * Read the path following \"gitdir: \" from the .git file into strbuf.\n     ++ *\n     ++ * Unlike read_gitfile_gently(), this function does not resolve a\n     ++ * relative path or validate it using is_git_directory().\n     ++ */\n      +int read_gitfile_raw(struct strbuf *contents, const char *path)\n       {\n       \tconst int max_file_size = 1 << 20;  /* 1MB */\n\n\n setup.c                    | 69 ++++++++++++++++++++++++--------------\n setup.h                    |  1 +\n t/t2406-worktree-repair.sh | 54 ++++++++++++++++++++++-------\n worktree.c                 | 20 +++++------\n 4 files changed, 94 insertions(+), 50 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex 95909e9603..d6a00cdbb4 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -962,16 +962,54 @@ void read_gitfile_error_die(int error_code, const char *path)\n  * cases).\n  */\n const char *read_gitfile_gently(const char *path, int *return_error_code)\n+{\n+\tint error_code = 0;\n+\tconst char *slash;\n+\tstruct strbuf contents = STRBUF_INIT;\n+\tstatic struct strbuf realpath = STRBUF_INIT;\n+\n+\terror_code = read_gitfile_raw(&contents, path);\n+\tif (error_code)\n+\t\tgoto cleanup_return;\n+\n+\tif (!is_absolute_path(contents.buf) && (slash = strrchr(path, '/'))) {\n+\t\tsize_t pathlen = slash+1 - path;\n+\t\tchar *dir = xstrfmt(\"%.*s%s\", (int)pathlen, path, contents.buf);\n+\t\tstrbuf_reset(&contents);\n+\t\tstrbuf_addstr(&contents, dir);\n+\t\tfree(dir);\n+\t}\n+\tif (!is_git_directory(contents.buf)) {\n+\t\terror_code = READ_GITFILE_ERR_NOT_A_REPO;\n+\t\tgoto cleanup_return;\n+\t}\n+\n+\tstrbuf_realpath(&realpath, contents.buf, 1);\n+\n+cleanup_return:\n+\tif (return_error_code)\n+\t\t*return_error_code = error_code;\n+\telse if (error_code)\n+\t\tread_gitfile_error_die(error_code, path);\n+\n+\tstrbuf_release(&contents);\n+\treturn error_code ? NULL : realpath.buf;\n+}\n+\n+/*\n+ * Read the path following \"gitdir: \" from the .git file into strbuf.\n+ *\n+ * Unlike read_gitfile_gently(), this function does not resolve a\n+ * relative path or validate it using is_git_directory().\n+ */\n+int read_gitfile_raw(struct strbuf *contents, const char *path)\n {\n \tconst int max_file_size = 1 << 20;  /* 1MB */\n \tint error_code = 0;\n \tchar *buf = NULL;\n-\tchar *dir = NULL;\n-\tconst char *slash;\n \tstruct stat st;\n \tint fd;\n \tssize_t len;\n-\tstatic struct strbuf realpath = STRBUF_INIT;\n \n \tif (stat(path, &st)) {\n \t\tif (errno == ENOENT || errno == ENOTDIR)\n@@ -1014,32 +1052,11 @@ const char *read_gitfile_gently(const char *path, int *return_error_code)\n \t\terror_code = READ_GITFILE_ERR_NO_PATH;\n \t\tgoto cleanup_return;\n \t}\n-\tbuf[len] = '\\0';\n-\tdir = buf + 8;\n-\n-\tif (!is_absolute_path(dir) && (slash = strrchr(path, '/'))) {\n-\t\tsize_t pathlen = slash+1 - path;\n-\t\tdir = xstrfmt(\"%.*s%.*s\", (int)pathlen, path,\n-\t\t\t      (int)(len - 8), buf + 8);\n-\t\tfree(buf);\n-\t\tbuf = dir;\n-\t}\n-\tif (!is_git_directory(dir)) {\n-\t\terror_code = READ_GITFILE_ERR_NOT_A_REPO;\n-\t\tgoto cleanup_return;\n-\t}\n-\n-\tstrbuf_realpath(&realpath, dir, 1);\n-\tpath = realpath.buf;\n+\tstrbuf_add(contents, buf+8, len-8);\n \n cleanup_return:\n-\tif (return_error_code)\n-\t\t*return_error_code = error_code;\n-\telse if (error_code)\n-\t\tread_gitfile_error_die(error_code, path);\n-\n \tfree(buf);\n-\treturn error_code ? NULL : path;\n+\treturn error_code;\n }\n \n static void apply_gitdir_and_environment(struct repository *repo, const char *path)\ndiff --git a/setup.h b/setup.h\nindex 654f10e059..e6e71bda3d 100644\n--- a/setup.h\n+++ b/setup.h\n@@ -40,6 +40,7 @@ int is_nonbare_repository_dir(struct strbuf *path);\n #define READ_GITFILE_ERR_IS_A_DIR 10\n void read_gitfile_error_die(int error_code, const char *path);\n const char *read_gitfile_gently(const char *path, int *return_error_code);\n+int read_gitfile_raw(struct strbuf *contents, const char *path);\n #define read_gitfile(path) read_gitfile_gently((path), NULL)\n const char *resolve_gitdir_gently(const char *suspect, int *return_error_code);\n #define resolve_gitdir(path) resolve_gitdir_gently((path), NULL)\ndiff --git a/t/t2406-worktree-repair.sh b/t/t2406-worktree-repair.sh\nindex f5f19b3169..d4e53d492b 100755\n--- a/t/t2406-worktree-repair.sh\n+++ b/t/t2406-worktree-repair.sh\n@@ -228,30 +228,60 @@ test_expect_success 'repair worktree with relative path with missing gitfile' '\n \ttest_cmp expect wt/.git\n '\n \n-test_expect_success 'repair absolute worktree to use relative paths' '\n-\ttest_when_finished \"rm -rf main side sidemoved\" &&\n+test_expect_success 'repair absolute to relative from side worktree' '\n+\ttest_when_finished \"rm -rf main side\" &&\n \ttest_create_repo main &&\n \ttest_commit -C main init &&\n \tgit -C main worktree add --detach ../side &&\n-\techo \"../../../../sidemoved/.git\" >expect-gitdir &&\n+\techo \"../../../../side/.git\" >expect-gitdir &&\n \techo \"gitdir: ../main/.git/worktrees/side\" >expect-gitfile &&\n-\tmv side sidemoved &&\n-\tgit -C main worktree repair --relative-paths ../sidemoved &&\n+\tgit -C main worktree repair --relative-paths ../side 2>main/err &&\n+\ttest_grep \"gitdir absolute/relative path mismatch\" main/err &&\n \ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n-\ttest_cmp expect-gitfile sidemoved/.git\n+\ttest_cmp expect-gitfile side/.git\n '\n \n-test_expect_success 'repair relative worktree to use absolute paths' '\n-\ttest_when_finished \"rm -rf main side sidemoved\" &&\n+test_expect_success 'repair relative to absolute from side worktree' '\n+\ttest_when_finished \"rm -rf main side\" &&\n \ttest_create_repo main &&\n \ttest_commit -C main init &&\n \tgit -C main worktree add --relative-paths --detach ../side &&\n-\techo \"$(pwd)/sidemoved/.git\" >expect-gitdir &&\n+\techo \"$(pwd)/side/.git\" >expect-gitdir &&\n \techo \"gitdir: $(pwd)/main/.git/worktrees/side\" >expect-gitfile &&\n-\tmv side sidemoved &&\n-\tgit -C main worktree repair ../sidemoved &&\n+\tgit -C main worktree repair ../side 2>main/err &&\n+\ttest_grep \"gitdir absolute/relative path mismatch\" main/err &&\n \ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n-\ttest_cmp expect-gitfile sidemoved/.git\n+\ttest_cmp expect-gitfile side/.git\n+'\n+\n+test_expect_success 'repair absolute to relative from main worktree' '\n+\ttest_when_finished \"rm -rf main side\" &&\n+\ttest_create_repo main &&\n+\tgit -C main config worktree.useRelativePaths false &&\n+\ttest_commit -C main init &&\n+\tgit -C main worktree add --detach ../side &&\n+\techo \"../../../../side/.git\" >expect-gitdir &&\n+\techo \"gitdir: ../main/.git/worktrees/side\" >expect-gitfile &&\n+\tgit -C main config worktree.useRelativePaths true &&\n+\tgit -C main worktree repair 2>main/err &&\n+\ttest_grep \".git file absolute/relative path mismatch\" main/err &&\n+\ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n+\ttest_cmp expect-gitfile side/.git\n+'\n+\n+test_expect_success 'repair relative to absolute from main worktree' '\n+\ttest_when_finished \"rm -rf main side\" &&\n+\ttest_create_repo main &&\n+\tgit -C main config worktree.useRelativePaths true &&\n+\ttest_commit -C main init &&\n+\tgit -C main worktree add --detach ../side &&\n+\techo \"$(pwd)/side/.git\" >expect-gitdir &&\n+\techo \"gitdir: $(pwd)/main/.git/worktrees/side\" >expect-gitfile &&\n+\tgit -C main config worktree.useRelativePaths false &&\n+\tgit -C main worktree repair 2>main/err &&\n+\ttest_grep \".git file absolute/relative path mismatch\" main/err &&\n+\ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n+\ttest_cmp expect-gitfile side/.git\n '\n \n test_done\ndiff --git a/worktree.c b/worktree.c\nindex cbf95328a3..8cb8637b18 100644\n--- a/worktree.c\n+++ b/worktree.c\n@@ -649,7 +649,8 @@ static void repair_gitfile(struct worktree *wt,\n \tstruct strbuf gitdir = STRBUF_INIT;\n \tstruct strbuf repo = STRBUF_INIT;\n \tstruct strbuf backlink = STRBUF_INIT;\n-\tchar *dotgit_contents = NULL;\n+\tstruct strbuf contents = STRBUF_INIT;\n+\tconst char *dotgit_contents = NULL;\n \tconst char *repair = NULL;\n \tchar *path = NULL;\n \tint err;\n@@ -667,7 +668,9 @@ static void repair_gitfile(struct worktree *wt,\n \tstrbuf_realpath(&repo, path, 1);\n \tstrbuf_addf(&dotgit, \"%s/.git\", wt->path);\n \tstrbuf_addf(&gitdir, \"%s/gitdir\", repo.buf);\n-\tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));\n+\terr = read_gitfile_raw(&contents, dotgit.buf);\n+\tif (!err)\n+\t\tdotgit_contents = contents.buf;\n \n \tif (dotgit_contents) {\n \t\tif (is_absolute_path(dotgit_contents)) {\n@@ -681,7 +684,7 @@ static void repair_gitfile(struct worktree *wt,\n \tif (err == READ_GITFILE_ERR_NOT_A_FILE ||\n \t\terr == READ_GITFILE_ERR_IS_A_DIR)\n \t\tfn(1, wt->path, _(\".git is not a file\"), cb_data);\n-\telse if (err)\n+\telse if (err || !is_git_directory(backlink.buf))\n \t\trepair = _(\".git file broken\");\n \telse if (fspathcmp(backlink.buf, repo.buf))\n \t\trepair = _(\".git file incorrect\");\n@@ -695,12 +698,12 @@ static void repair_gitfile(struct worktree *wt,\n \t}\n \n done:\n-\tfree(dotgit_contents);\n \tfree(path);\n \tstrbuf_release(&repo);\n \tstrbuf_release(&dotgit);\n \tstrbuf_release(&gitdir);\n \tstrbuf_release(&backlink);\n+\tstrbuf_release(&contents);\n }\n \n static void repair_noop(int iserr UNUSED,\n@@ -857,14 +860,7 @@ void repair_worktree_at_path(struct repository *repo,\n \tstrbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0);\n \tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));\n \tif (dotgit_contents) {\n-\t\tif (is_absolute_path(dotgit_contents)) {\n-\t\t\tstrbuf_addstr(&backlink, dotgit_contents);\n-\t\t} else {\n-\t\t\tstrbuf_addbuf(&backlink, &dotgit);\n-\t\t\tstrbuf_strip_suffix(&backlink, \".git\");\n-\t\t\tstrbuf_addstr(&backlink, dotgit_contents);\n-\t\t\tstrbuf_realpath_forgiving(&backlink, backlink.buf, 0);\n-\t\t}\n+\t\tstrbuf_addstr(&backlink, dotgit_contents);\n \t} else if (err == READ_GITFILE_ERR_NOT_A_FILE ||\n \t\t\terr == READ_GITFILE_ERR_IS_A_DIR) {\n \t\tfn(1, dotgit.buf, _(\"unable to locate repository; .git is not a file\"), cb_data);\n\nbase-commit: dea0ea3582e6980ddbc1173cc8e3e9f9db91cde0\n-- \ngitgitgadget\n"},{"id":"551430","messageId":"xmqqh5ke3zxt.fsf@gitster.g","threadId":"66180","inReplyTo":"pull.2205.v4.git.1787930386252.gitgitgadget@gmail.com","subject":"Re: [PATCH v4] worktree repair: detect relative path in .git file correctly","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-28T17:41:34Z","receivedAt":"2026-08-28T17:41:37Z","isPatch":true,"body":"\"Yoichi NAKAYAMA via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>\n>\n> Given a state in which the cross-references between the worktree and\n> the repository (specifically worktree/id/gitdir in the main repository\n> and the .git file in the worktree) are recorded using absolute paths,\n> setting 'worktree.useRelativePaths=true' and running 'git worktree\n> repair' within the main worktree converts them to relative paths.\n> ...\n\nThis iteration looks very good to me.  Will mark for 'next' unless\nthere are comments that say otherwise in the next few days.\n\nThanks.\n"}]}