From: Chen Linxuan <me@black-desk.cn>
repo_set_worktree() stores only the realpath-resolved working directory in repo->worktree, which discards any symlinks the user followed to get there. A follow-up commit needs to match that path the way "gitdir:" does, i.e. against both the real and the symlinked spelling, which requires the original spelling to still be available.
Add repo->worktree_original, plus a repo_get_work_tree_original() accessor, to hold that symlink-preserving spelling. repo_set_worktree() derives it from the given path; for the discovered-repository case, where the setup code has already chdir()d to the worktree root by the time set_git_work_tree(repo, ".") runs, logical_path_from_cwd() recovers it from $PWD instead.
repo->worktree is unchanged; repo_get_work_tree_original() has no callers yet and is wired up in the next commit.
Signed-off-by: Chen Linxuan <me@black-desk.cn> --- repository.c | 26 ++++++++++++++++++ repository.h | 10 +++++++ setup.c | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 121 insertions(+), 1 deletion(-)
Show changes to 3 files +121 −1
repository.c, repository.h, setup.c
diff --git a/repository.c b/repository.c index 73d80bcffdf5..a29d55a6fcd3 100644 --- a/repository.c +++ b/repository.c @@ -149,6 +149,11 @@ const char *repo_get_work_tree(struct repository *repo) return repo->worktree; } +const char *repo_get_work_tree_original(struct repository *repo) +{ + return repo->worktree_original; +} + static void repo_set_commondir(struct repository *repo, const char *commondir) { @@ -252,8 +257,28 @@ static int repo_init_gitdir(struct repository *repo, const char *gitdir) void repo_set_worktree(struct repository *repo, const char *path) { + struct strbuf worktree = STRBUF_INIT; + + /* + * Resolve the canonical path first. This preserves the historical + * behaviour for unusable worktree paths (e.g. a bogus GIT_WORK_TREE): + * strbuf_realpath() dies on error before we touch the copy below. + */ repo->worktree = real_pathdup(path, 1); + /* + * Keep a symlink-preserving copy: absolute and normalized, but not + * realpath-resolved. Normalization can only fail for inputs that + * realpath tolerates (the rest already died above); fall back to the + * physical path so callers never see a NULL. + */ + strbuf_add_absolute_path(&worktree, path); + if (strbuf_normalize_path(&worktree) < 0) + repo->worktree_original = xstrdup(repo->worktree); + else + repo->worktree_original = strbuf_detach(&worktree, NULL); + strbuf_release(&worktree); + trace2_def_repo(repo); } @@ -379,6 +404,7 @@ void repo_clear(struct repository *repo) FREE_AND_NULL(repo->graft_file); FREE_AND_NULL(repo->index_file); FREE_AND_NULL(repo->worktree); + FREE_AND_NULL(repo->worktree_original); FREE_AND_NULL(repo->submodule_prefix); FREE_AND_NULL(repo->ref_storage_payload); diff --git a/repository.h b/repository.h index 7d649e32e7fa..f08fbfde4a07 100644 --- a/repository.h +++ b/repository.h @@ -114,6 +114,15 @@ struct repository { * A NULL value indicates that there is no working directory. */ char *worktree; + /* + * Symlink-preserving spelling of the working directory: absolute and + * normalized, but NOT realpath-resolved (keeps any symlinks the user + * followed to get here). Used by includeIf "worktree:" so it can match + * both the real and the symlinked spelling, the way "gitdir:" does. + * Falls back to the same value as "worktree" when no logical path is + * available. + */ + char *worktree_original; bool worktree_initialized; bool worktree_config_is_bogus; @@ -221,6 +230,7 @@ const char *repo_get_object_directory(struct repository *repo); const char *repo_get_index_file(struct repository *repo); const char *repo_get_graft_file(struct repository *repo); const char *repo_get_work_tree(struct repository *repo); +const char *repo_get_work_tree_original(struct repository *repo); /* * Define a custom repository layout. Any field can be NULL, which diff --git a/setup.c b/setup.c index 0de56a074f7c..fbbeb95f99db 100644 --- a/setup.c +++ b/setup.c @@ -1213,12 +1213,94 @@ static const char *setup_explicit_git_dir(struct repository *repo, return NULL; } +/* + * Do "a" and "b" refer to the same filesystem entry? Both must report a + * nonzero (dev,ino): some filesystems return (0,0) for unrelated paths, + * which would otherwise look identical. + */ +static int same_entry(const char *a, const char *b) +{ + struct stat sa, sb; + + if (stat(a, &sa) || stat(b, &sb)) + return 0; + return (sa.st_dev || sa.st_ino) && + sa.st_dev == sb.st_dev && sa.st_ino == sb.st_ino; +} + +/* + * Recover the symlink-preserving spelling of the worktree root. + * + * strbuf_add_absolute_path() already consults $PWD to keep symlinks when + * resolving a relative path, so set_git_work_tree()'s other callers get a + * symlink-preserving worktree path for free. This function exists for the + * discovered-repository case: setup_git_directory_gently() chdir()s to the + * worktree root *before* set_git_work_tree(repo, ".") runs, so by the time + * "." is resolved $PWD still names the caller's original directory and no + * longer agrees with the physical cwd, and strbuf_add_absolute_path() + * falls back to the realpath. We close that gap by deriving the logical + * root here, from $PWD, while we still have the original physical cwd and + * the root offset in hand. + * + * "cwd" is the physical current directory (getcwd), and "root_len" is the + * length of the worktree root within it; cwd->buf[root_len..] is therefore + * the part of the path below the root (empty when git ran at the root). + * + * $PWD, maintained by the shell, may spell that same directory through + * symlinks. If we can confirm $PWD really names cwd's directory (same + * device/inode) and that the below-root suffix matches, we swap the + * physical root prefix for $PWD's prefix and keep the user's symlinks. + * Only symlinks in the root prefix itself are preserved: the below-root + * suffix is matched byte-for-byte, so a symlink below the root is not. + * + * Returns the allocated logical path, or NULL when $PWD is missing, already + * physical, or untrustworthy. + */ +static char *logical_path_from_cwd(struct strbuf *cwd, int root_len) +{ + const char *pwd = getenv("PWD"); + size_t suffix_len, pwd_len; + struct strbuf path = STRBUF_INIT; + + if (!pwd || !is_absolute_path(pwd) || !strcmp(pwd, cwd->buf)) + return NULL; + /* + * $PWD is a plain environment variable: it can be set to anything, + * or left stale after a chdir. Only borrow its symlink-preserving + * spelling once we prove it still points at the same directory as + * the physical cwd; otherwise give up and return NULL. + */ + if (!same_entry(cwd->buf, pwd)) + return NULL; + + /* + * Drop the below-root suffix from $PWD. It must match the physical + * suffix exactly; the only spelling difference we accept is in the + * root prefix -- i.e. the symlinks we want to preserve. + */ + suffix_len = cwd->len - root_len; + pwd_len = strlen(pwd); + if (suffix_len) { + const char *suffix = cwd->buf + root_len; + + if (suffix_len > pwd_len || + fspathcmp(pwd + pwd_len - suffix_len, suffix)) + return NULL; + pwd_len -= suffix_len; + } + + strbuf_add(&path, pwd, pwd_len); + return strbuf_detach(&path, NULL); +} + static const char *setup_discovered_git_dir(struct repository *repo, const char *gitdir, struct strbuf *cwd, int offset, struct repository_format *repo_fmt, int *nongit_ok) { + char *worktree = NULL; + if (check_repository_format_gently(gitdir, repo_fmt, nongit_ok)) return NULL; @@ -1245,7 +1327,9 @@ static const char *setup_discovered_git_dir(struct repository *repo, } /* #0, #1, #5, #8, #9, #12, #13 */ - set_git_work_tree(repo, "."); + worktree = logical_path_from_cwd(cwd, offset); + set_git_work_tree(repo, worktree ? worktree : "."); + free(worktree); if (strcmp(gitdir, DEFAULT_GIT_DIR_ENVIRONMENT)) set_git_dir(repo, gitdir, 0); if (offset >= cwd->len)
-- 2.53.0