Git development
 help / color / mirror / Atom feed
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



  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