From: Chen Linxuan via B4 Relay <devnull+me.black-desk.cn@kernel.org>
To: git@vger.kernel.org
Cc: Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>,
Junio C Hamano <gitster@pobox.com>,
Patrick Steinhardt <ps@pks.im>, Chen Linxuan <me@black-desk.cn>,
Phillip Wood <phillip.wood@dunelm.org.uk>
Subject: [PATCH v7 2/3] repository: keep a symlink-preserving copy of the worktree path
Date: Thu, 09 Jul 2026 10:41:42 +0800 [thread overview]
Message-ID: <20260709-includeif-worktree-v7-2-e87e705e8df6@black-desk.cn> (raw)
In-Reply-To: <20260709-includeif-worktree-v7-0-e87e705e8df6@black-desk.cn>
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(-)
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
next prev parent reply other threads:[~2026-07-09 2:42 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-09 2:41 [PATCH v7 0/3] includeIf: add "worktree" condition for matching working tree path Chen Linxuan via B4 Relay
2026-07-09 2:41 ` [PATCH v7 1/3] config: refactor include_by_gitdir() into include_by_path() Chen Linxuan via B4 Relay
2026-07-09 2:41 ` Chen Linxuan via B4 Relay [this message]
2026-07-09 10:09 ` [PATCH v7 2/3] repository: keep a symlink-preserving copy of the worktree path Patrick Steinhardt
2026-07-09 2:41 ` [PATCH v7 3/3] config: add "worktree" and "worktree/i" includeIf conditions Chen Linxuan via B4 Relay
2026-07-09 10:09 ` [PATCH v7 0/3] includeIf: add "worktree" condition for matching working tree path Patrick Steinhardt
2026-07-09 20:40 ` Junio C Hamano
2026-07-10 6:43 ` [PATCH v8 0/2] " Chen Linxuan via B4 Relay
2026-07-10 6:43 ` [PATCH v8 1/2] config: refactor include_by_gitdir() into include_by_path() Chen Linxuan via B4 Relay
2026-07-10 6:43 ` [PATCH v8 2/2] config: add "worktree" and "worktree/i" includeIf conditions Chen Linxuan via B4 Relay
2026-07-13 11:16 ` Patrick Steinhardt
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260709-includeif-worktree-v7-2-e87e705e8df6@black-desk.cn \
--to=devnull+me.black-desk.cn@kernel.org \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=kristofferhaugsbakk@fastmail.com \
--cc=me@black-desk.cn \
--cc=phillip.wood@dunelm.org.uk \
--cc=ps@pks.im \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox