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
WARNING: multiple messages have this Message-ID (diff)
From: Chen Linxuan <me@black-desk.cn>
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>
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: 18+ 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 ` Chen Linxuan
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
2026-07-09 2:41 ` Chen Linxuan via B4 Relay [this message]
2026-07-09 2:41 ` [PATCH v7 2/3] repository: keep a symlink-preserving copy of the worktree path Chen Linxuan
2026-07-09 10:09 ` 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 2:41 ` Chen Linxuan
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 ` Chen Linxuan
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 ` Chen Linxuan
2026-07-10 6:43 ` [PATCH v8 2/2] config: add "worktree" and "worktree/i" includeIf conditions Chen Linxuan via B4 Relay
2026-07-10 6:43 ` Chen Linxuan
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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.