From 21b2878aa552a931788aa7bb14720b24e7809c75 Mon Sep 17 00:00:00 2001 From: Yoichi NAKAYAMA Date: Tue, 13 Oct 2026 23:34:37 +0900 Subject: [PATCH 1/5] worktree repair: align .git file handling with repair_gitfile() Use read_gitfile_raw() instead of read_gitfile_gently() in repair_worktree_at_path(), moving path resolution and repository validation to the caller. This makes .git file handling consistent with repair_gitfile() and avoids using the non-reentrant read_gitfile_gently(). A subsequent commit will update infer_backlink() to use read_gitfile_raw() as well. Signed-off-by: Yoichi NAKAYAMA --- worktree.c | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/worktree.c b/worktree.c index 8cb8637b189ab8..4492ca835da2d7 100644 --- a/worktree.c +++ b/worktree.c @@ -840,7 +840,7 @@ void repair_worktree_at_path(struct repository *repo, struct strbuf inferred_backlink = STRBUF_INIT; struct strbuf gitdir = STRBUF_INIT; struct strbuf olddotgit = STRBUF_INIT; - char *dotgit_contents = NULL; + struct strbuf contents = STRBUF_INIT; const char *repair = NULL; int err; @@ -858,14 +858,26 @@ void repair_worktree_at_path(struct repository *repo, infer_backlink(repo, dotgit.buf, &inferred_backlink); strbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0); - dotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err)); - if (dotgit_contents) { - strbuf_addstr(&backlink, dotgit_contents); - } else if (err == READ_GITFILE_ERR_NOT_A_FILE || - err == READ_GITFILE_ERR_IS_A_DIR) { - fn(1, dotgit.buf, _("unable to locate repository; .git is not a file"), cb_data); + err = read_gitfile_raw(&contents, dotgit.buf); + if (!err) { + const char *dotgit_contents = contents.buf; + if (is_absolute_path(dotgit_contents)) { + strbuf_addstr(&backlink, dotgit_contents); + } else { + strbuf_addbuf(&backlink, &dotgit); + strbuf_strip_suffix(&backlink, ".git"); + strbuf_addstr(&backlink, dotgit_contents); + strbuf_realpath_forgiving(&backlink, backlink.buf, 0); + } + } + if (err == READ_GITFILE_ERR_NOT_A_FILE || + err == READ_GITFILE_ERR_IS_A_DIR) { + fn(1, dotgit.buf, + _("unable to locate repository; .git is not a file"), + cb_data); goto done; - } else if (err == READ_GITFILE_ERR_NOT_A_REPO) { + } else if (!err && !is_git_directory(backlink.buf)) { + strbuf_release(&backlink); if (inferred_backlink.len) { /* * Worktree's .git file does not point at a repository @@ -876,11 +888,15 @@ void repair_worktree_at_path(struct repository *repo, */ strbuf_swap(&backlink, &inferred_backlink); } else { - fn(1, dotgit.buf, _("unable to locate repository; .git file does not reference a repository"), cb_data); + fn(1, dotgit.buf, + _("unable to locate repository; .git file does not reference a repository"), + cb_data); goto done; } - } else { - fn(1, dotgit.buf, _("unable to locate repository; .git file broken"), cb_data); + } else if (err) { + fn(1, dotgit.buf, + _("unable to locate repository; .git file broken"), + cb_data); goto done; } @@ -926,12 +942,12 @@ void repair_worktree_at_path(struct repository *repo, gitdir.buf, use_relative_paths); } done: - free(dotgit_contents); strbuf_release(&olddotgit); strbuf_release(&backlink); strbuf_release(&inferred_backlink); strbuf_release(&gitdir); strbuf_release(&dotgit); + strbuf_release(&contents); } int should_prune_worktree(struct repository *repo, From 30ac7acad32750609befc3049c0f4174f6ec2f2e Mon Sep 17 00:00:00 2001 From: Yoichi NAKAYAMA Date: Tue, 13 Oct 2026 23:34:55 +0900 Subject: [PATCH 2/5] worktree repair: centralize inferred backlink selection Reorder the processing steps to centralize the logic for preferring the inferred_backlink. Signed-off-by: Yoichi NAKAYAMA --- worktree.c | 69 +++++++++++++++++++++++------------------------------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/worktree.c b/worktree.c index 4492ca835da2d7..f8e9850f6f4d9d 100644 --- a/worktree.c +++ b/worktree.c @@ -841,6 +841,7 @@ void repair_worktree_at_path(struct repository *repo, struct strbuf gitdir = STRBUF_INIT; struct strbuf olddotgit = STRBUF_INIT; struct strbuf contents = STRBUF_INIT; + const char *dotgit_contents = NULL; const char *repair = NULL; int err; @@ -856,43 +857,13 @@ void repair_worktree_at_path(struct repository *repo, goto done; } - infer_backlink(repo, dotgit.buf, &inferred_backlink); - strbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0); err = read_gitfile_raw(&contents, dotgit.buf); - if (!err) { - const char *dotgit_contents = contents.buf; - if (is_absolute_path(dotgit_contents)) { - strbuf_addstr(&backlink, dotgit_contents); - } else { - strbuf_addbuf(&backlink, &dotgit); - strbuf_strip_suffix(&backlink, ".git"); - strbuf_addstr(&backlink, dotgit_contents); - strbuf_realpath_forgiving(&backlink, backlink.buf, 0); - } - } if (err == READ_GITFILE_ERR_NOT_A_FILE || err == READ_GITFILE_ERR_IS_A_DIR) { fn(1, dotgit.buf, _("unable to locate repository; .git is not a file"), cb_data); goto done; - } else if (!err && !is_git_directory(backlink.buf)) { - strbuf_release(&backlink); - if (inferred_backlink.len) { - /* - * Worktree's .git file does not point at a repository - * but we found a .git/worktrees/ in this - * repository with the same as recorded in the - * worktree's .git file so make the worktree point at - * the discovered .git/worktrees/. - */ - strbuf_swap(&backlink, &inferred_backlink); - } else { - fn(1, dotgit.buf, - _("unable to locate repository; .git file does not reference a repository"), - cb_data); - goto done; - } } else if (err) { fn(1, dotgit.buf, _("unable to locate repository; .git file broken"), @@ -900,23 +871,41 @@ void repair_worktree_at_path(struct repository *repo, goto done; } + dotgit_contents = contents.buf; + infer_backlink(repo, dotgit.buf, &inferred_backlink); + strbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0); + + if (is_absolute_path(dotgit_contents)) { + strbuf_addstr(&backlink, dotgit_contents); + } else { + strbuf_addbuf(&backlink, &dotgit); + strbuf_strip_suffix(&backlink, ".git"); + strbuf_addstr(&backlink, dotgit_contents); + strbuf_realpath_forgiving(&backlink, backlink.buf, 0); + } + + if (!is_git_directory(backlink.buf) && !inferred_backlink.len) { + fn(1, dotgit.buf, + _("unable to locate repository; .git file does not reference a repository"), + cb_data); + goto done; + } + /* * If we got this far, either the worktree's .git file pointed at a - * valid repository (i.e. read_gitfile_gently() returned success) or + * valid repository (i.e. is_git_directory() returned true) or * the .git file did not point at a repository but we were able to * infer a suitable new value for the .git file by locating a * .git/worktrees/ in *this* repository corresponding to the * recorded in the worktree's .git file. * - * However, if, at this point, inferred_backlink is non-NULL (i.e. we - * found a suitable .git/worktrees/ in *this* repository) *and* the - * worktree's .git file points at a valid repository *and* those two - * paths differ, then that indicates that the user probably *copied* - * the main and linked worktrees to a new location as a unit rather - * than *moving* them. Thus, the copied worktree's .git file actually - * points at the .git/worktrees/ in the *original* repository, not - * in the "copy" repository. In this case, point the "copy" worktree's - * .git file at the "copy" repository. + * Even if the worktree's .git file pointed at a valid repository, + * it doesn't always mean that the backlink is correct. For example, + * the user might have *copied* the main and linked worktrees to a + * new location as a unit rather than *moving* them (the copied + * worktree's .git file actually points at the .git/worktrees/ + * in the *original* repository, not in the "copy" repository). + * Therefore, we prioritize inferred_backlink over backlink. */ if (inferred_backlink.len && fspathcmp(backlink.buf, inferred_backlink.buf)) strbuf_swap(&backlink, &inferred_backlink); From 2cc7b817ee4105b57450ed80d80cfa224f2ed431 Mon Sep 17 00:00:00 2001 From: Yoichi NAKAYAMA Date: Tue, 13 Oct 2026 23:35:00 +0900 Subject: [PATCH 3/5] worktree repair: use read_gitfile_raw() in infer_backlink() Use read_gitfile_raw() in infer_backlink() to parse .git files consistently with repair_gitfile(). Unlike strbuf_trim(), read_gitfile_raw() does not silently discard trailing whitespace. Such whitespace is treated as part of the path and may cause the path to be treated as not referencing a repository. Extract get_worktree_id() for use in a subsequent commit. Signed-off-by: Yoichi NAKAYAMA --- worktree.c | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/worktree.c b/worktree.c index f8e9850f6f4d9d..3627dbc044dfb7 100644 --- a/worktree.c +++ b/worktree.c @@ -637,6 +637,14 @@ int other_head_refs(struct repository *repo, return ret; } +static const char *get_worktree_id(const char *dotgit_contents) +{ + const char *slash = find_last_dir_sep(dotgit_contents); + if (!slash) + return ""; + return slash + 1; +} + /* * Repair worktree's /path/to/worktree/.git file if missing, corrupt, or not * pointing at /worktrees/. @@ -801,27 +809,24 @@ static ssize_t infer_backlink(struct repository *repo, const char *gitfile, struct strbuf *inferred) { - struct strbuf actual = STRBUF_INIT; + struct strbuf contents = STRBUF_INIT; const char *id; + int err; - if (strbuf_read_file(&actual, gitfile, 0) < 0) + err = read_gitfile_raw(&contents, gitfile); + if (err) goto error; - if (!starts_with(actual.buf, "gitdir:")) - goto error; - if (!(id = find_last_dir_sep(actual.buf))) - goto error; - strbuf_trim(&actual); - id++; /* advance past '/' to point at */ + id = get_worktree_id(contents.buf); if (!*id) goto error; repo_common_path_replace(repo, inferred, "worktrees/%s", id); if (!is_directory(inferred->buf)) goto error; - strbuf_release(&actual); + strbuf_release(&contents); return inferred->len; error: - strbuf_release(&actual); + strbuf_release(&contents); strbuf_reset(inferred); /* clear invalid path */ return -1; } From 8c80b9ae05035b3bb864e17e08be9604447dd0e5 Mon Sep 17 00:00:00 2001 From: Yoichi NAKAYAMA Date: Tue, 13 Oct 2026 23:35:06 +0900 Subject: [PATCH 4/5] worktree repair: reduce .git file reads Pass the loaded contents of the .git file to infer_backlink(). Signed-off-by: Yoichi NAKAYAMA --- worktree.c | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/worktree.c b/worktree.c index 3627dbc044dfb7..879941a977dd68 100644 --- a/worktree.c +++ b/worktree.c @@ -806,27 +806,20 @@ static int is_main_worktree_path(struct repository *repo, const char *path) * Returns -1 on failure and strbuf.len on success. */ static ssize_t infer_backlink(struct repository *repo, - const char *gitfile, + const char *dotgit_contents, struct strbuf *inferred) { - struct strbuf contents = STRBUF_INIT; const char *id; - int err; - err = read_gitfile_raw(&contents, gitfile); - if (err) - goto error; - id = get_worktree_id(contents.buf); + id = get_worktree_id(dotgit_contents); if (!*id) goto error; repo_common_path_replace(repo, inferred, "worktrees/%s", id); if (!is_directory(inferred->buf)) goto error; - strbuf_release(&contents); return inferred->len; error: - strbuf_release(&contents); strbuf_reset(inferred); /* clear invalid path */ return -1; } @@ -877,7 +870,7 @@ void repair_worktree_at_path(struct repository *repo, } dotgit_contents = contents.buf; - infer_backlink(repo, dotgit.buf, &inferred_backlink); + infer_backlink(repo, dotgit_contents, &inferred_backlink); strbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0); if (is_absolute_path(dotgit_contents)) { From 015602168c8532c07b94c9f50925bf5d5a2ef7c8 Mon Sep 17 00:00:00 2001 From: Yoichi NAKAYAMA Date: Tue, 13 Oct 2026 23:35:12 +0900 Subject: [PATCH 5/5] worktree repair: avoid breaking unrelated .git file and gitdir Currently, `repair_gitfile()` does not verify whether the worktree ID recorded in the .git file matches the worktree being repaired, which can result in an unrelated .git file being corrupted. For instance, if two worktree directories are swapped without using 'git worktree move', running 'git worktree repair' in the main worktree accidentally swaps the links between their .git files and gitdirs. `repair_worktree_at_path()` proceeds even if it fails to infer the gitdir path. This can result in the corruption of an unrelated gitdir. For instance, if we copied a linked worktree to a new location X, running 'git worktree repair X' in a working tree which does not belong to the original repository can accidentally overwrite the gitdir in the original repository (the scope of impact should be limited to the repository where the command was executed). Resolve these issues by validating the worktree ID and stopping the repair when the ID does not match or the gitdir path cannot be inferred. Signed-off-by: Yoichi NAKAYAMA --- t/t2406-worktree-repair.sh | 33 +++++++++++++++++++++++++++------ worktree.c | 20 ++++++++++++++++---- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/t/t2406-worktree-repair.sh b/t/t2406-worktree-repair.sh index d4e53d492b833d..2ffa123f421aed 100755 --- a/t/t2406-worktree-repair.sh +++ b/t/t2406-worktree-repair.sh @@ -56,15 +56,12 @@ test_expect_success 'repair missing .git file' ' ' test_expect_success 'repair bogus .git file' ' - test_corrupt_gitfile "echo \"gitdir: /nowhere\" >corrupt/.git" \ + test_corrupt_gitfile "echo \"contents not started with gitdir:\" >corrupt/.git" \ ".git file broken" ' -test_expect_success 'repair incorrect .git file' ' - test_when_finished "rm -rf other && git worktree prune" && - test_create_repo other && - other=$(git -C other rev-parse --absolute-git-dir) && - test_corrupt_gitfile "echo \"gitdir: $other\" >corrupt/.git" \ +test_expect_success 'repair unlinked .git file' ' + test_corrupt_gitfile "echo \"gitdir: /nowhere/worktrees/corrupt\" >corrupt/.git" \ ".git file incorrect" ' @@ -89,6 +86,18 @@ test_expect_success 'repair .git file from bare.git' ' test_cmp expect actual ' +test_expect_success 'skip unrelated .git file' ' + test_when_finished "rm -rf corrupt other && git worktree prune" && + git worktree add --detach corrupt && + rm -rf corrupt && + git worktree add --detach other && + mv other corrupt && + cat corrupt/.git >expect && + test_must_fail git worktree repair 2>err && + test_cmp expect corrupt/.git && + test_grep "unrelated .git file" err +' + test_expect_success 'invalid worktree path' ' test_must_fail git worktree repair /notvalid >out 2>err && test_must_be_empty out && @@ -113,6 +122,18 @@ test_expect_success 'repo not found; .git not referencing repo' ' test_grep ".git file does not reference a repository" err ' +test_expect_success 'repo not found; .git not for worktree' ' + test_when_finished "rm -rf side other-repo && git worktree prune" && + test_create_repo other-repo && + git worktree add --detach side && + cat .git/worktrees/side/gitdir >expect && + cp -R side other-repo/side && + test_must_fail git -C other-repo worktree repair side >out 2>err && + test_cmp expect .git/worktrees/side/gitdir && + test_must_be_empty out && + test_grep ".git file is not for a linked worktree" err +' + test_expect_success 'repo not found; .git file broken' ' test_when_finished "rm -rf orig moved && git worktree prune" && git worktree add --detach orig && diff --git a/worktree.c b/worktree.c index 879941a977dd68..86094307b199e4 100644 --- a/worktree.c +++ b/worktree.c @@ -640,7 +640,11 @@ int other_head_refs(struct repository *repo, static const char *get_worktree_id(const char *dotgit_contents) { const char *slash = find_last_dir_sep(dotgit_contents); - if (!slash) + const char *prefix = "/worktrees"; + int prefixlen = strlen(prefix); + if (!slash || + slash - dotgit_contents < prefixlen || + strncmp(slash - prefixlen, prefix, prefixlen)) return ""; return slash + 1; } @@ -692,8 +696,10 @@ static void repair_gitfile(struct worktree *wt, if (err == READ_GITFILE_ERR_NOT_A_FILE || err == READ_GITFILE_ERR_IS_A_DIR) fn(1, wt->path, _(".git is not a file"), cb_data); - else if (err || !is_git_directory(backlink.buf)) + else if (err) repair = _(".git file broken"); + else if (strcmp(get_worktree_id(dotgit_contents), wt->id)) + fn(1, wt->path, _("unrelated .git file"), cb_data); else if (fspathcmp(backlink.buf, repo.buf)) repair = _(".git file incorrect"); else if (use_relative_paths == is_absolute_path(dotgit_contents)) @@ -815,7 +821,7 @@ static ssize_t infer_backlink(struct repository *repo, if (!*id) goto error; repo_common_path_replace(repo, inferred, "worktrees/%s", id); - if (!is_directory(inferred->buf)) + if (!is_git_directory(inferred->buf)) goto error; return inferred->len; @@ -888,6 +894,12 @@ void repair_worktree_at_path(struct repository *repo, cb_data); goto done; } + if (!inferred_backlink.len) { + fn(1, dotgit.buf, + _("unable to locate repository; .git file is not for a linked worktree"), + cb_data); + goto done; + } /* * If we got this far, either the worktree's .git file pointed at a @@ -905,7 +917,7 @@ void repair_worktree_at_path(struct repository *repo, * in the *original* repository, not in the "copy" repository). * Therefore, we prioritize inferred_backlink over backlink. */ - if (inferred_backlink.len && fspathcmp(backlink.buf, inferred_backlink.buf)) + if (fspathcmp(backlink.buf, inferred_backlink.buf)) strbuf_swap(&backlink, &inferred_backlink); strbuf_addf(&gitdir, "%s/gitdir", backlink.buf);