* [PATCH v7 1/3] config: refactor include_by_gitdir() into include_by_path()
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 via B4 Relay
2026-07-09 2:41 ` [PATCH v7 2/3] repository: keep a symlink-preserving copy of the worktree path Chen Linxuan via B4 Relay
` (4 subsequent siblings)
5 siblings, 0 replies; 11+ messages in thread
From: Chen Linxuan via B4 Relay @ 2026-07-09 2:41 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Patrick Steinhardt,
Chen Linxuan, Phillip Wood
From: Chen Linxuan <me@black-desk.cn>
The include_by_gitdir() function matches the realpath of a given
path against a glob pattern, but its interface is tightly coupled to
the gitdir condition: it takes a struct config_options *opts and
extracts opts->git_dir internally.
Refactor it into a more generic include_by_path() helper that takes
a const char *path parameter directly, and update the gitdir and
gitdir/i callers to pass opts->git_dir explicitly. No behavior
change, just preparing for the addition of a new worktree condition
that will reuse the same path-matching logic with a different path.
Signed-off-by: Chen Linxuan <me@black-desk.cn>
---
config.c | 19 ++++++++-----------
1 file changed, 8 insertions(+), 11 deletions(-)
diff --git a/config.c b/config.c
index 6a0de86e3ae9..00eeeea370c9 100644
--- a/config.c
+++ b/config.c
@@ -235,23 +235,20 @@ static int prepare_include_condition_pattern(const struct key_value_info *kvi,
return 0;
}
-static int include_by_gitdir(const struct key_value_info *kvi,
- const struct config_options *opts,
- const char *cond, size_t cond_len, int icase)
+static int include_by_path(const struct key_value_info *kvi,
+ const char *path,
+ const char *cond, size_t cond_len, int icase)
{
struct strbuf text = STRBUF_INIT;
struct strbuf pattern = STRBUF_INIT;
size_t prefix;
int ret = 0;
- const char *git_dir;
int already_tried_absolute = 0;
- if (opts->git_dir)
- git_dir = opts->git_dir;
- else
+ if (!path)
goto done;
- strbuf_realpath(&text, git_dir, 1);
+ strbuf_realpath(&text, path, 1);
strbuf_add(&pattern, cond, cond_len);
ret = prepare_include_condition_pattern(kvi, &pattern, &prefix);
if (ret < 0)
@@ -284,7 +281,7 @@ static int include_by_gitdir(const struct key_value_info *kvi,
* which'll do the right thing
*/
strbuf_reset(&text);
- strbuf_add_absolute_path(&text, git_dir);
+ strbuf_add_absolute_path(&text, path);
already_tried_absolute = 1;
goto again;
}
@@ -400,9 +397,9 @@ static int include_condition_is_true(const struct key_value_info *kvi,
const struct config_options *opts = inc->opts;
if (skip_prefix_mem(cond, cond_len, "gitdir:", &cond, &cond_len))
- return include_by_gitdir(kvi, opts, cond, cond_len, 0);
+ return include_by_path(kvi, opts->git_dir, cond, cond_len, 0);
else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len))
- return include_by_gitdir(kvi, opts, cond, cond_len, 1);
+ return include_by_path(kvi, opts->git_dir, cond, cond_len, 1);
else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len))
return include_by_branch(inc, cond, cond_len);
else if (skip_prefix_mem(cond, cond_len, "hasconfig:remote.*.url:", &cond,
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v7 2/3] repository: keep a symlink-preserving copy of the worktree path
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
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
` (3 subsequent siblings)
5 siblings, 1 reply; 11+ messages in thread
From: Chen Linxuan via B4 Relay @ 2026-07-09 2:41 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Patrick Steinhardt,
Chen Linxuan, Phillip Wood
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
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH v7 2/3] repository: keep a symlink-preserving copy of the worktree path
2026-07-09 2:41 ` [PATCH v7 2/3] repository: keep a symlink-preserving copy of the worktree path Chen Linxuan via B4 Relay
@ 2026-07-09 10:09 ` Patrick Steinhardt
0 siblings, 0 replies; 11+ messages in thread
From: Patrick Steinhardt @ 2026-07-09 10:09 UTC (permalink / raw)
To: me; +Cc: git, Kristoffer Haugsbakk, Junio C Hamano, Phillip Wood
On Thu, Jul 09, 2026 at 10:41:42AM +0800, Chen Linxuan via B4 Relay wrote:
> 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;
> +}
Feels a bit heavy-handed to have such an accessor, as we could've just
as well accessed the member directly via the structure.
> 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.
> + */
Oof.
> +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);
> +}
This feels quite awkward to me, and I assume that these changes will
lead to conflicts with ps/setup-split-discovery-and-setup.
I wonder whether we can maybe avoid this whole mess by removing the call
to chdir(3p) when discovering Git directories in the first place.
Instead, we'd only chdir(3p) after we have fully discovered the Git
repository's paths, and that may allow us to not have to worry about
reconstructing the logical path?
It's something that I wanted to explore after the mentioned patch series
has landed, but maybe it's something we should try to do as part of this
patch series here.
Alternatively, I'm less certain that this complexity is ultimately
really worth it now... so another alternative could be to document the
issue and fix it at a later point in time.
Patrick
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v7 3/3] config: add "worktree" and "worktree/i" includeIf conditions
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 ` [PATCH v7 2/3] repository: keep a symlink-preserving copy of the worktree path Chen Linxuan via B4 Relay
@ 2026-07-09 2:41 ` 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
` (2 subsequent siblings)
5 siblings, 0 replies; 11+ messages in thread
From: Chen Linxuan via B4 Relay @ 2026-07-09 2:41 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Patrick Steinhardt,
Chen Linxuan, Phillip Wood
From: Chen Linxuan <me@black-desk.cn>
The includeIf mechanism already supports matching on the .git
directory path (gitdir) and the currently checked out branch
(onbranch). But in multi-worktree setups the .git directory of a
linked worktree points into the main repository's .git/worktrees/
area, which makes gitdir patterns cumbersome when one wants to
include config based on the working tree's checkout path instead.
Introduce two new condition keywords:
- worktree:<pattern> matches the working directory of the current
worktree (the path returned by git rev-parse --show-toplevel)
against a glob pattern.
- worktree/i:<pattern> is the case-insensitive variant.
The implementation reuses the include_by_path() helper, passing
repo_get_work_tree_original() (added in the previous commit; it keeps
the symlink-preserving spelling of the worktree path) in place of the
gitdir. As with gitdir, include_by_path() then matches both the
realpath and the original spelling, so a pattern may use either. The
condition never matches in bare repositories (where there is no
worktree) or during early config reading (where no repository is
available).
Add documentation describing the new conditions, including a comparison
with extensions.worktreeConfig. Add tests covering bare repositories,
multiple worktrees, symlinked and subdir-of-symlinked worktree paths,
case-sensitive and case-insensitive matching, early config reading,
and non-repository scenarios.
Signed-off-by: Chen Linxuan <me@black-desk.cn>
---
Documentation/config.adoc | 48 +++++++++++++
config.c | 6 ++
t/t1305-config-include.sh | 171 ++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 225 insertions(+)
diff --git a/Documentation/config.adoc b/Documentation/config.adoc
index 15b1a4d59347..c153da986e4a 100644
--- a/Documentation/config.adoc
+++ b/Documentation/config.adoc
@@ -146,6 +146,46 @@ refer to linkgit:gitignore[5] for details. For convenience:
This is the same as `gitdir` except that matching is done
case-insensitively (e.g. on case-insensitive file systems)
+`worktree`::
+ The data that follows the keyword `worktree` and a colon is used as a
+ glob pattern. If the working directory of the current worktree matches
+ the pattern, the include condition is met.
++
+The worktree location is the path where files are checked out (as returned
+by `git rev-parse --show-toplevel`). This is different from `gitdir`, which
+matches the `.git` directory path. In a linked worktree, the worktree path
+is the directory where that worktree's files are located, not the main
+repository's `.git` directory.
++
+The pattern uses the same glob syntax as `gitdir` (including `~/`, `./`,
+`**/`, and trailing-`/` prefix matching). This condition will never match
+in a bare repository (which has no worktree).
++
+This is useful when you want to apply configuration based on where the
+working tree is located on the filesystem. For example, a contributor who
+works on the same project both personally and as an employee can use
+different `user.name` and `user.email` values depending on which directory
+the worktree is checked out under:
++
+----
+[includeIf "worktree:/home/user/work/"]
+ path = ~/.config/git/work.inc
+[includeIf "worktree:/home/user/personal/"]
+ path = ~/.config/git/personal.inc
+----
++
+While `extensions.worktreeConfig` (see linkgit:git-worktree[1]) also supports
+per-worktree configuration, it stores the config inside each repository's
+`.git/config.worktree` file and requires running `git config --worktree`
+inside each worktree individually. In contrast, `includeIf "worktree:..."`
+can be set once in a global or system-level configuration file (e.g.
+`~/.config/git/config`) and applies to all repositories at once based on
+their worktree location.
+
+`worktree/i`::
+ This is the same as `worktree` except that matching is done
+ case-insensitively (e.g. on case-insensitive file systems)
+
`onbranch`::
The data that follows the keyword `onbranch` and a colon is taken to be a
pattern with standard globbing wildcards and two additional
@@ -244,6 +284,14 @@ Example
[includeIf "gitdir:~/to/group/"]
path = /path/to/foo.inc
+; include if the worktree is at /path/to/project-build
+[includeIf "worktree:/path/to/project-build"]
+ path = build-config.inc
+
+; include for all worktrees inside /path/to/group
+[includeIf "worktree:/path/to/group/"]
+ path = group-config.inc
+
; relative paths are always relative to the including
; file (if the condition is true); their location is not
; affected by the condition
diff --git a/config.c b/config.c
index 00eeeea370c9..652711ec5e0b 100644
--- a/config.c
+++ b/config.c
@@ -400,6 +400,12 @@ static int include_condition_is_true(const struct key_value_info *kvi,
return include_by_path(kvi, opts->git_dir, cond, cond_len, 0);
else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len))
return include_by_path(kvi, opts->git_dir, cond, cond_len, 1);
+ else if (skip_prefix_mem(cond, cond_len, "worktree:", &cond, &cond_len))
+ return include_by_path(kvi, inc->repo ? repo_get_work_tree_original(inc->repo) : NULL,
+ cond, cond_len, 0);
+ else if (skip_prefix_mem(cond, cond_len, "worktree/i:", &cond, &cond_len))
+ return include_by_path(kvi, inc->repo ? repo_get_work_tree_original(inc->repo) : NULL,
+ cond, cond_len, 1);
else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len))
return include_by_branch(inc, cond, cond_len);
else if (skip_prefix_mem(cond, cond_len, "hasconfig:remote.*.url:", &cond,
diff --git a/t/t1305-config-include.sh b/t/t1305-config-include.sh
index f3892578e4ff..99eae656a3f7 100755
--- a/t/t1305-config-include.sh
+++ b/t/t1305-config-include.sh
@@ -396,4 +396,175 @@ test_expect_success 'onbranch without repository but explicit nonexistent Git di
test_must_fail nongit git --git-dir=nonexistent config get foo.bar
'
+# worktree: conditional include tests
+
+test_expect_success 'conditional include, worktree bare repo' '
+ git init --bare wt-bare &&
+ (
+ cd wt-bare &&
+ echo "[includeIf \"worktree:/\"]path=bar-bare" >>config &&
+ echo "[test]wtbare=1" >bar-bare &&
+ test_must_fail git config test.wtbare
+ )
+'
+
+test_expect_success 'conditional include, worktree multiple worktrees' '
+ git init wt-multi &&
+ (
+ cd wt-multi &&
+ test_commit initial &&
+ git worktree add -b linked-branch ../wt-linked HEAD &&
+ git worktree add -b prefix-branch ../wt-prefix/linked HEAD
+ ) &&
+ wt_main="$(cd wt-multi && pwd)" &&
+ wt_linked="$(cd wt-linked && pwd)" &&
+ wt_prefix_parent="$(cd wt-prefix && pwd)" &&
+ cat >>wt-multi/.git/config <<-EOF &&
+ [includeIf "worktree:$wt_main"]
+ path = main-config
+ [includeIf "worktree:$wt_linked"]
+ path = linked-config
+ [includeIf "worktree:$wt_prefix_parent/"]
+ path = prefix-config
+ EOF
+ echo "[test]mainvar=main" >wt-multi/.git/main-config &&
+ echo "[test]linkedvar=linked" >wt-multi/.git/linked-config &&
+ echo "[test]prefixvar=prefix" >wt-multi/.git/prefix-config &&
+ echo main >expect &&
+ git -C wt-multi config test.mainvar >actual &&
+ test_cmp expect actual &&
+ test_must_fail git -C wt-multi config test.linkedvar &&
+ test_must_fail git -C wt-multi config test.prefixvar &&
+ echo linked >expect &&
+ git -C wt-linked config test.linkedvar >actual &&
+ test_cmp expect actual &&
+ test_must_fail git -C wt-linked config test.mainvar &&
+ test_must_fail git -C wt-linked config test.prefixvar &&
+ echo prefix >expect &&
+ git -C wt-prefix/linked config test.prefixvar >actual &&
+ test_cmp expect actual &&
+ test_must_fail git -C wt-prefix/linked config test.mainvar &&
+ test_must_fail git -C wt-prefix/linked config test.linkedvar
+'
+
+test_expect_success SYMLINKS 'conditional include, worktree matching symlink' '
+ mkdir sym-real &&
+ ln -s sym-real sym-link &&
+ git init sym-link/repo &&
+ (
+ cd sym-link/repo &&
+ link_path="$(pwd)" &&
+ real_path="$(test-tool path-utils real_path "$link_path")" &&
+ cat >>.git/config <<-EOF &&
+ [includeIf "gitdir:$link_path/.git"]
+ path = gitdir-link
+ [includeIf "gitdir:$real_path/.git"]
+ path = gitdir-real
+ [includeIf "worktree:$link_path"]
+ path = worktree-link
+ [includeIf "worktree:$real_path"]
+ path = worktree-real
+ EOF
+ echo "[test]gitdirlink=1" >.git/gitdir-link &&
+ echo "[test]gitdirreal=1" >.git/gitdir-real &&
+ echo "[test]worktreelink=1" >.git/worktree-link &&
+ echo "[test]worktreereal=1" >.git/worktree-real &&
+ git config get test.gitdirlink &&
+ git config get test.gitdirreal &&
+ git config get test.worktreelink &&
+ git config get test.worktreereal &&
+ # from a subdirectory, the logical worktree path is recovered by
+ # stripping the below-root suffix, so both spellings still match
+ mkdir d &&
+ cd d &&
+ git config get test.worktreelink &&
+ git config get test.worktreereal
+ )
+'
+
+test_expect_success SYMLINKS 'conditional include, worktree matching symlink of a linked worktree' '
+ git init wt-main &&
+ ( cd wt-main && test_commit initial ) &&
+ git -C wt-main worktree add --detach ../wt-real &&
+ ln -s wt-real wt-link &&
+ wt_main="$(cd wt-main && pwd)" &&
+ (
+ cd wt-link &&
+ link_path="$(pwd)" &&
+ real_path="$(test-tool path-utils real_path "$link_path")" &&
+ cat >>"$wt_main/.git/config" <<-EOF &&
+ [includeIf "worktree:$link_path"]
+ path = wt-link
+ [includeIf "worktree:$real_path"]
+ path = wt-real
+ EOF
+ echo "[test]wtlink=1" >"$wt_main/.git/wt-link" &&
+ echo "[test]wtreal=1" >"$wt_main/.git/wt-real" &&
+ test "$(git config get test.wtlink)" = "1" &&
+ test "$(git config get test.wtreal)" = "1"
+ )
+'
+
+test_expect_success !CASE_INSENSITIVE_FS 'conditional include, worktree, case sensitive' '
+ git init wt-case &&
+ (
+ cd wt-case &&
+ test_commit initial &&
+ wt_path="$(pwd)" &&
+ wt_upper=$(echo "$wt_path" | tr a-z A-Z) &&
+ echo "[includeIf \"worktree:$wt_upper\"]path=case-inc" >>.git/config &&
+ echo "[test]wtcase=1" >.git/case-inc &&
+ test_must_fail git config test.wtcase
+ )
+'
+
+test_expect_success 'conditional include, worktree, icase' '
+ git init wt-icase &&
+ (
+ cd wt-icase &&
+ test_commit initial &&
+ wt_path="$(pwd)" &&
+ wt_upper=$(echo "$wt_path" | tr a-z A-Z) &&
+ echo "[includeIf \"worktree/i:$wt_upper\"]path=icase-inc" >>.git/config &&
+ echo "[test]wticase=1" >.git/icase-inc &&
+ echo 1 >expect &&
+ git config test.wticase >actual &&
+ test_cmp expect actual
+ )
+'
+
+# The "worktree" condition cannot match during early config reading
+# because the repository object is not yet fully initialized and
+# repo_get_work_tree() returns NULL.
+test_expect_success 'conditional include, worktree does not match in early config' '
+ git init wt-early &&
+ (
+ cd wt-early &&
+ test_commit initial &&
+ wt_path="$(pwd)" &&
+ echo "[includeIf \"worktree:$wt_path\"]path=early-inc" >>.git/config &&
+ echo "[test]wtearly=1" >.git/early-inc &&
+ test-tool config read_early_config test.wtearly >actual &&
+ test_must_be_empty actual
+ )
+'
+
+# Use a loose pattern so the "present in non-worktree cases" check works
+# for Unix-style absolute paths and Windows paths like D:/a/git/...
+test_expect_success 'conditional include, worktree without repository' '
+ test_when_finished "rm -f .gitconfig config.inc" &&
+ git config set -f .gitconfig "includeIf.worktree:**.path" config.inc &&
+ git config set -f config.inc foo.bar baz &&
+ git config get foo.bar &&
+ test_must_fail nongit git config get foo.bar
+'
+
+test_expect_success 'conditional include, worktree without repository but explicit nonexistent Git directory' '
+ test_when_finished "rm -f .gitconfig config.inc" &&
+ git config set -f .gitconfig "includeIf.worktree:**.path" config.inc &&
+ git config set -f config.inc foo.bar baz &&
+ git config get foo.bar &&
+ test_must_fail nongit git --git-dir=nonexistent config get foo.bar
+'
+
test_done
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH v7 0/3] includeIf: add "worktree" condition for matching working tree path
2026-07-09 2:41 [PATCH v7 0/3] includeIf: add "worktree" condition for matching working tree path Chen Linxuan via B4 Relay
` (2 preceding siblings ...)
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 ` Patrick Steinhardt
2026-07-09 20:40 ` Junio C Hamano
2026-07-10 6:43 ` [PATCH v8 0/2] " Chen Linxuan via B4 Relay
5 siblings, 0 replies; 11+ messages in thread
From: Patrick Steinhardt @ 2026-07-09 10:09 UTC (permalink / raw)
To: me; +Cc: git, Kristoffer Haugsbakk, Junio C Hamano, Phillip Wood
On Thu, Jul 09, 2026 at 10:41:40AM +0800, Chen Linxuan via B4 Relay wrote:
> Changes in v7:
> - Preserve the symlinked spelling of the worktree path and match
> includeIf "worktree:" against it, so the condition now matches both
> the symlinked and the real path, consistent with "gitdir:"
> (Patrick Steinhardt, v6 review).
> - Split the work into a preparatory commit that stores a non-realpath
> worktree path and a follow-up that wires it into includeIf.
> - Extend symlink test coverage to subdirectories and linked worktrees.
> - Link to v6: https://lore.kernel.org/r/20260703-includeif-worktree-v6-0-a13893ad9a7f@black-desk.cn
One note: it would be nice if you could send newer versions of your
patch series in reply to the old version. I see you're using the b4
relay, so this should be configurable via `b4.send-same-thread`.
Thanks!
Patrick
^ permalink raw reply [flat|nested] 11+ messages in thread* Re: [PATCH v7 0/3] includeIf: add "worktree" condition for matching working tree path
2026-07-09 2:41 [PATCH v7 0/3] includeIf: add "worktree" condition for matching working tree path Chen Linxuan via B4 Relay
` (3 preceding siblings ...)
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
5 siblings, 0 replies; 11+ messages in thread
From: Junio C Hamano @ 2026-07-09 20:40 UTC (permalink / raw)
To: Chen Linxuan via B4 Relay
Cc: git, Kristoffer Haugsbakk, Patrick Steinhardt, Chen Linxuan,
Phillip Wood
Chen Linxuan via B4 Relay <devnull+me.black-desk.cn@kernel.org>
writes:
> The `includeIf` mechanism already supports matching on the `.git`
> directory path (`gitdir`) and the currently checked out branch
> (`onbranch`). But in multi-worktree setups the `.git` directory of a
> linked worktree points into the main repository's `.git/worktrees/`
> area, which makes `gitdir` patterns cumbersome when one wants to
> include config based on the working tree's checkout path instead.
Thanks.
This seems to break t1305 when merged to 'seen', even though all of
them pass standalone. I did not have time to figure out what
interactions with which other topic are causing the breakages.
^ permalink raw reply [flat|nested] 11+ messages in thread* [PATCH v8 0/2] includeIf: add "worktree" condition for matching working tree path
2026-07-09 2:41 [PATCH v7 0/3] includeIf: add "worktree" condition for matching working tree path Chen Linxuan via B4 Relay
` (4 preceding siblings ...)
2026-07-09 20:40 ` Junio C Hamano
@ 2026-07-10 6:43 ` 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
5 siblings, 2 replies; 11+ messages in thread
From: Chen Linxuan via B4 Relay @ 2026-07-10 6:43 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Patrick Steinhardt,
Chen Linxuan, Phillip Wood
The `includeIf` mechanism already supports matching on the `.git`
directory path (`gitdir`) and the currently checked out branch
(`onbranch`). But in multi-worktree setups the `.git` directory of a
linked worktree points into the main repository's `.git/worktrees/`
area, which makes `gitdir` patterns cumbersome when one wants to
include config based on the working tree's checkout path instead.
Introduce two new condition keywords:
- `worktree:<pattern>` matches the working directory of the current
worktree against a glob pattern.
- `worktree/i:<pattern>` is the case-insensitive variant.
Supported pattern features: glob wildcards, `**/` and `/**`, `~`
expansion, `./` relative paths, and trailing-`/` prefix matching.
The condition never matches in a bare repository.
Signed-off-by: Chen Linxuan <me@black-desk.cn>
---
Changes in v8:
- Drop the v7 symlink-preserving worktree path implementation. Patrick
pointed out that the setup-side plumbing was too invasive and likely to
conflict with the ongoing setup discovery work.
- Document the current limitation instead: includeIf "worktree:" matches
the realpath-resolved worktree location, so symlink spellings may not
match.
- Return the series to two patches, based on v6 plus the documentation
update.
- Link to v7: https://lore.kernel.org/r/20260709-includeif-worktree-v7-0-e87e705e8df6@black-desk.cn
Changes in v7:
- Preserve the symlinked spelling of the worktree path and match
includeIf "worktree:" against it, so the condition now matches both
the symlinked and the real path, consistent with "gitdir:"
(Patrick Steinhardt, v6 review).
- Split the work into a preparatory commit that stores a non-realpath
worktree path and a follow-up that wires it into includeIf.
- Extend symlink test coverage to subdirectories and linked worktrees.
- Link to v6: https://lore.kernel.org/r/20260703-includeif-worktree-v6-0-a13893ad9a7f@black-desk.cn
Changes in v6:
- Rebase onto current `master` at Git 2.55.
- Add an in-code comment explaining why the non-repository worktree
tests use the loose `**.path` pattern (suggested by Junio C Hamano).
- Link to v5: https://lore.kernel.org/r/20260525-includeif-worktree-v5-0-1efe525d025a@black-desk.cn
Changes in v5:
- Fix Windows CI failure: use `**` glob pattern instead of `/` in the
"worktree without repository" tests, since `/` as a path pattern is
Unix-specific and does not match Windows paths.
Github CI pass: https://github.com/black-desk/git/actions/runs/26380466288
- Add a test verifying case-sensitive matching by default, with the
`!CASE_INSENSITIVE_FS` prerequisite (suggested by Patrick Steinhardt).
- Link to v4: https://lore.kernel.org/r/20260513-includeif-worktree-v4-0-f8e6212d1fba@black-desk.cn
Changes in v4:
- Deduplicate the worktree pattern documentation by referencing the
gitdir syntax instead of repeating the full pattern description
(suggested by Patrick Steinhardt).
- Add documentation comparing includeIf "worktree:" with
extensions.worktreeConfig, including a concrete use case example
(suggested by Phillip Wood, Junio C Hamano).
- Add a test verifying that the worktree condition does not match
during early config reading (suggested by Patrick Steinhardt).
- Add tests for the non-repository (nongit) scenario (suggested by
Patrick Steinhardt).
- Add a test for the case-insensitive "worktree/i" variant
- Link to v3: https://lore.kernel.org/r/20260403-includeif-worktree-v3-0-109ce5782b03@black-desk.cn
Changes in v3:
- Apply Junio's suggestion.
- Link to v2: https://lore.kernel.org/r/20260402-includeif-worktree-v2-0-36e339b898d7@black-desk.cn
Changes in v2:
- Add missing signed-off-by lines.
- Link to v1: https://lore.kernel.org/r/20260401-includeif-worktree-v1-0-906db69f2c79@black-desk.cn
---
Chen Linxuan (2):
config: refactor include_by_gitdir() into include_by_path()
config: add "worktree" and "worktree/i" includeIf conditions
Documentation/config.adoc | 53 +++++++++++++++++++
config.c | 25 +++++----
t/t1305-config-include.sh | 128 ++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 195 insertions(+), 11 deletions(-)
Range-diff versus v7:
1: 510f28d207f8 = 1: 731d928b1dfa config: refactor include_by_gitdir() into include_by_path()
2: 0f83ffee0338 < -: ------------ repository: keep a symlink-preserving copy of the worktree path
3: 18d1abc325fc ! 2: 56c792090625 config: add "worktree" and "worktree/i" includeIf conditions
@@ Commit message
Introduce two new condition keywords:
- - worktree:<pattern> matches the working directory of the current
- worktree (the path returned by git rev-parse --show-toplevel)
- against a glob pattern.
+ - worktree:<pattern> matches the realpath of the current worktree's
+ working directory (i.e. repo_get_work_tree()) against a glob
+ pattern. This is the path returned by git rev-parse
+ --show-toplevel.
- worktree/i:<pattern> is the case-insensitive variant.
- The implementation reuses the include_by_path() helper, passing
- repo_get_work_tree_original() (added in the previous commit; it keeps
- the symlink-preserving spelling of the worktree path) in place of the
- gitdir. As with gitdir, include_by_path() then matches both the
- realpath and the original spelling, so a pattern may use either. The
- condition never matches in bare repositories (where there is no
- worktree) or during early config reading (where no repository is
- available).
+ The implementation reuses the include_by_path() helper introduced in
+ the previous commit, passing the worktree path in place of the
+ gitdir. The condition never matches in bare repositories (where
+ there is no worktree) or during early config reading (where no
+ repository is available).
Add documentation describing the new conditions, including a comparison
- with extensions.worktreeConfig. Add tests covering bare repositories,
- multiple worktrees, symlinked and subdir-of-symlinked worktree paths,
- case-sensitive and case-insensitive matching, early config reading,
+ with extensions.worktreeConfig and a note that worktree matching currently
+ uses the realpath-resolved worktree location. Add tests covering bare
+ repositories, multiple worktrees, realpath-resolved symlinked worktree
+ paths, case-sensitive and case-insensitive matching, early config reading,
and non-repository scenarios.
Signed-off-by: Chen Linxuan <me@black-desk.cn>
@@ Documentation/config.adoc: refer to linkgit:gitignore[5] for details. For conven
+`**/`, and trailing-`/` prefix matching). This condition will never match
+in a bare repository (which has no worktree).
++
++Unlike `gitdir`, the `worktree` condition currently matches only the
++realpath-resolved worktree location. If the working tree was entered via a
++symbolic link, a pattern that uses the symbolic-link spelling may not match;
++use the real path instead.
+++
+This is useful when you want to apply configuration based on where the
+working tree is located on the filesystem. For example, a contributor who
+works on the same project both personally and as an employee can use
@@ config.c: static int include_condition_is_true(const struct key_value_info *kvi,
else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len))
return include_by_path(kvi, opts->git_dir, cond, cond_len, 1);
+ else if (skip_prefix_mem(cond, cond_len, "worktree:", &cond, &cond_len))
-+ return include_by_path(kvi, inc->repo ? repo_get_work_tree_original(inc->repo) : NULL,
++ return include_by_path(kvi, inc->repo ? repo_get_work_tree(inc->repo) : NULL,
+ cond, cond_len, 0);
+ else if (skip_prefix_mem(cond, cond_len, "worktree/i:", &cond, &cond_len))
-+ return include_by_path(kvi, inc->repo ? repo_get_work_tree_original(inc->repo) : NULL,
++ return include_by_path(kvi, inc->repo ? repo_get_work_tree(inc->repo) : NULL,
+ cond, cond_len, 1);
else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len))
return include_by_branch(inc, cond, cond_len);
@@ t/t1305-config-include.sh: test_expect_success 'onbranch without repository but
+ test_must_fail git -C wt-prefix/linked config test.linkedvar
+'
+
-+test_expect_success SYMLINKS 'conditional include, worktree matching symlink' '
-+ mkdir sym-real &&
-+ ln -s sym-real sym-link &&
-+ git init sym-link/repo &&
-+ (
-+ cd sym-link/repo &&
-+ link_path="$(pwd)" &&
-+ real_path="$(test-tool path-utils real_path "$link_path")" &&
-+ cat >>.git/config <<-EOF &&
-+ [includeIf "gitdir:$link_path/.git"]
-+ path = gitdir-link
-+ [includeIf "gitdir:$real_path/.git"]
-+ path = gitdir-real
-+ [includeIf "worktree:$link_path"]
-+ path = worktree-link
-+ [includeIf "worktree:$real_path"]
-+ path = worktree-real
-+ EOF
-+ echo "[test]gitdirlink=1" >.git/gitdir-link &&
-+ echo "[test]gitdirreal=1" >.git/gitdir-real &&
-+ echo "[test]worktreelink=1" >.git/worktree-link &&
-+ echo "[test]worktreereal=1" >.git/worktree-real &&
-+ git config get test.gitdirlink &&
-+ git config get test.gitdirreal &&
-+ git config get test.worktreelink &&
-+ git config get test.worktreereal &&
-+ # from a subdirectory, the logical worktree path is recovered by
-+ # stripping the below-root suffix, so both spellings still match
-+ mkdir d &&
-+ cd d &&
-+ git config get test.worktreelink &&
-+ git config get test.worktreereal
-+ )
-+'
-+
-+test_expect_success SYMLINKS 'conditional include, worktree matching symlink of a linked worktree' '
-+ git init wt-main &&
-+ ( cd wt-main && test_commit initial ) &&
-+ git -C wt-main worktree add --detach ../wt-real &&
-+ ln -s wt-real wt-link &&
-+ wt_main="$(cd wt-main && pwd)" &&
++test_expect_success SYMLINKS 'conditional include, worktree resolves symlinks' '
++ mkdir real-wt &&
++ ln -s real-wt link-wt &&
++ git init link-wt/repo &&
+ (
-+ cd wt-link &&
-+ link_path="$(pwd)" &&
-+ real_path="$(test-tool path-utils real_path "$link_path")" &&
-+ cat >>"$wt_main/.git/config" <<-EOF &&
-+ [includeIf "worktree:$link_path"]
-+ path = wt-link
-+ [includeIf "worktree:$real_path"]
-+ path = wt-real
-+ EOF
-+ echo "[test]wtlink=1" >"$wt_main/.git/wt-link" &&
-+ echo "[test]wtreal=1" >"$wt_main/.git/wt-real" &&
-+ test "$(git config get test.wtlink)" = "1" &&
-+ test "$(git config get test.wtreal)" = "1"
++ cd link-wt/repo &&
++ # repo->worktree resolves symlinks, so use real path in pattern
++ echo "[includeIf \"worktree:**/real-wt/repo\"]path=bar-link" >>.git/config &&
++ echo "[test]wtlink=2" >.git/bar-link &&
++ echo 2 >expect &&
++ git config test.wtlink >actual &&
++ test_cmp expect actual
+ )
+'
+
---
base-commit: f85a7e662054a7b0d9070e432508831afa214b47
^ permalink raw reply [flat|nested] 11+ messages in thread* [PATCH v8 1/2] config: refactor include_by_gitdir() into include_by_path()
2026-07-10 6:43 ` [PATCH v8 0/2] " Chen Linxuan via B4 Relay
@ 2026-07-10 6:43 ` 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
1 sibling, 0 replies; 11+ messages in thread
From: Chen Linxuan via B4 Relay @ 2026-07-10 6:43 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Patrick Steinhardt,
Chen Linxuan, Phillip Wood
From: Chen Linxuan <me@black-desk.cn>
The include_by_gitdir() function matches the realpath of a given
path against a glob pattern, but its interface is tightly coupled to
the gitdir condition: it takes a struct config_options *opts and
extracts opts->git_dir internally.
Refactor it into a more generic include_by_path() helper that takes
a const char *path parameter directly, and update the gitdir and
gitdir/i callers to pass opts->git_dir explicitly. No behavior
change, just preparing for the addition of a new worktree condition
that will reuse the same path-matching logic with a different path.
Signed-off-by: Chen Linxuan <me@black-desk.cn>
---
config.c | 19 ++++++++-----------
1 file changed, 8 insertions(+), 11 deletions(-)
diff --git a/config.c b/config.c
index 6a0de86e3ae9..00eeeea370c9 100644
--- a/config.c
+++ b/config.c
@@ -235,23 +235,20 @@ static int prepare_include_condition_pattern(const struct key_value_info *kvi,
return 0;
}
-static int include_by_gitdir(const struct key_value_info *kvi,
- const struct config_options *opts,
- const char *cond, size_t cond_len, int icase)
+static int include_by_path(const struct key_value_info *kvi,
+ const char *path,
+ const char *cond, size_t cond_len, int icase)
{
struct strbuf text = STRBUF_INIT;
struct strbuf pattern = STRBUF_INIT;
size_t prefix;
int ret = 0;
- const char *git_dir;
int already_tried_absolute = 0;
- if (opts->git_dir)
- git_dir = opts->git_dir;
- else
+ if (!path)
goto done;
- strbuf_realpath(&text, git_dir, 1);
+ strbuf_realpath(&text, path, 1);
strbuf_add(&pattern, cond, cond_len);
ret = prepare_include_condition_pattern(kvi, &pattern, &prefix);
if (ret < 0)
@@ -284,7 +281,7 @@ static int include_by_gitdir(const struct key_value_info *kvi,
* which'll do the right thing
*/
strbuf_reset(&text);
- strbuf_add_absolute_path(&text, git_dir);
+ strbuf_add_absolute_path(&text, path);
already_tried_absolute = 1;
goto again;
}
@@ -400,9 +397,9 @@ static int include_condition_is_true(const struct key_value_info *kvi,
const struct config_options *opts = inc->opts;
if (skip_prefix_mem(cond, cond_len, "gitdir:", &cond, &cond_len))
- return include_by_gitdir(kvi, opts, cond, cond_len, 0);
+ return include_by_path(kvi, opts->git_dir, cond, cond_len, 0);
else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len))
- return include_by_gitdir(kvi, opts, cond, cond_len, 1);
+ return include_by_path(kvi, opts->git_dir, cond, cond_len, 1);
else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len))
return include_by_branch(inc, cond, cond_len);
else if (skip_prefix_mem(cond, cond_len, "hasconfig:remote.*.url:", &cond,
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v8 2/2] config: add "worktree" and "worktree/i" includeIf conditions
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 ` Chen Linxuan via B4 Relay
2026-07-13 11:16 ` Patrick Steinhardt
1 sibling, 1 reply; 11+ messages in thread
From: Chen Linxuan via B4 Relay @ 2026-07-10 6:43 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Patrick Steinhardt,
Chen Linxuan, Phillip Wood
From: Chen Linxuan <me@black-desk.cn>
The includeIf mechanism already supports matching on the .git
directory path (gitdir) and the currently checked out branch
(onbranch). But in multi-worktree setups the .git directory of a
linked worktree points into the main repository's .git/worktrees/
area, which makes gitdir patterns cumbersome when one wants to
include config based on the working tree's checkout path instead.
Introduce two new condition keywords:
- worktree:<pattern> matches the realpath of the current worktree's
working directory (i.e. repo_get_work_tree()) against a glob
pattern. This is the path returned by git rev-parse
--show-toplevel.
- worktree/i:<pattern> is the case-insensitive variant.
The implementation reuses the include_by_path() helper introduced in
the previous commit, passing the worktree path in place of the
gitdir. The condition never matches in bare repositories (where
there is no worktree) or during early config reading (where no
repository is available).
Add documentation describing the new conditions, including a comparison
with extensions.worktreeConfig and a note that worktree matching currently
uses the realpath-resolved worktree location. Add tests covering bare
repositories, multiple worktrees, realpath-resolved symlinked worktree
paths, case-sensitive and case-insensitive matching, early config reading,
and non-repository scenarios.
Signed-off-by: Chen Linxuan <me@black-desk.cn>
---
Documentation/config.adoc | 53 +++++++++++++++++++
config.c | 6 +++
t/t1305-config-include.sh | 128 ++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 187 insertions(+)
diff --git a/Documentation/config.adoc b/Documentation/config.adoc
index 15b1a4d59347..1ef72de62f2b 100644
--- a/Documentation/config.adoc
+++ b/Documentation/config.adoc
@@ -146,6 +146,51 @@ refer to linkgit:gitignore[5] for details. For convenience:
This is the same as `gitdir` except that matching is done
case-insensitively (e.g. on case-insensitive file systems)
+`worktree`::
+ The data that follows the keyword `worktree` and a colon is used as a
+ glob pattern. If the working directory of the current worktree matches
+ the pattern, the include condition is met.
++
+The worktree location is the path where files are checked out (as returned
+by `git rev-parse --show-toplevel`). This is different from `gitdir`, which
+matches the `.git` directory path. In a linked worktree, the worktree path
+is the directory where that worktree's files are located, not the main
+repository's `.git` directory.
++
+The pattern uses the same glob syntax as `gitdir` (including `~/`, `./`,
+`**/`, and trailing-`/` prefix matching). This condition will never match
+in a bare repository (which has no worktree).
++
+Unlike `gitdir`, the `worktree` condition currently matches only the
+realpath-resolved worktree location. If the working tree was entered via a
+symbolic link, a pattern that uses the symbolic-link spelling may not match;
+use the real path instead.
++
+This is useful when you want to apply configuration based on where the
+working tree is located on the filesystem. For example, a contributor who
+works on the same project both personally and as an employee can use
+different `user.name` and `user.email` values depending on which directory
+the worktree is checked out under:
++
+----
+[includeIf "worktree:/home/user/work/"]
+ path = ~/.config/git/work.inc
+[includeIf "worktree:/home/user/personal/"]
+ path = ~/.config/git/personal.inc
+----
++
+While `extensions.worktreeConfig` (see linkgit:git-worktree[1]) also supports
+per-worktree configuration, it stores the config inside each repository's
+`.git/config.worktree` file and requires running `git config --worktree`
+inside each worktree individually. In contrast, `includeIf "worktree:..."`
+can be set once in a global or system-level configuration file (e.g.
+`~/.config/git/config`) and applies to all repositories at once based on
+their worktree location.
+
+`worktree/i`::
+ This is the same as `worktree` except that matching is done
+ case-insensitively (e.g. on case-insensitive file systems)
+
`onbranch`::
The data that follows the keyword `onbranch` and a colon is taken to be a
pattern with standard globbing wildcards and two additional
@@ -244,6 +289,14 @@ Example
[includeIf "gitdir:~/to/group/"]
path = /path/to/foo.inc
+; include if the worktree is at /path/to/project-build
+[includeIf "worktree:/path/to/project-build"]
+ path = build-config.inc
+
+; include for all worktrees inside /path/to/group
+[includeIf "worktree:/path/to/group/"]
+ path = group-config.inc
+
; relative paths are always relative to the including
; file (if the condition is true); their location is not
; affected by the condition
diff --git a/config.c b/config.c
index 00eeeea370c9..9d6d7872d76c 100644
--- a/config.c
+++ b/config.c
@@ -400,6 +400,12 @@ static int include_condition_is_true(const struct key_value_info *kvi,
return include_by_path(kvi, opts->git_dir, cond, cond_len, 0);
else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len))
return include_by_path(kvi, opts->git_dir, cond, cond_len, 1);
+ else if (skip_prefix_mem(cond, cond_len, "worktree:", &cond, &cond_len))
+ return include_by_path(kvi, inc->repo ? repo_get_work_tree(inc->repo) : NULL,
+ cond, cond_len, 0);
+ else if (skip_prefix_mem(cond, cond_len, "worktree/i:", &cond, &cond_len))
+ return include_by_path(kvi, inc->repo ? repo_get_work_tree(inc->repo) : NULL,
+ cond, cond_len, 1);
else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len))
return include_by_branch(inc, cond, cond_len);
else if (skip_prefix_mem(cond, cond_len, "hasconfig:remote.*.url:", &cond,
diff --git a/t/t1305-config-include.sh b/t/t1305-config-include.sh
index f3892578e4ff..4e840dfdb35b 100755
--- a/t/t1305-config-include.sh
+++ b/t/t1305-config-include.sh
@@ -396,4 +396,132 @@ test_expect_success 'onbranch without repository but explicit nonexistent Git di
test_must_fail nongit git --git-dir=nonexistent config get foo.bar
'
+# worktree: conditional include tests
+
+test_expect_success 'conditional include, worktree bare repo' '
+ git init --bare wt-bare &&
+ (
+ cd wt-bare &&
+ echo "[includeIf \"worktree:/\"]path=bar-bare" >>config &&
+ echo "[test]wtbare=1" >bar-bare &&
+ test_must_fail git config test.wtbare
+ )
+'
+
+test_expect_success 'conditional include, worktree multiple worktrees' '
+ git init wt-multi &&
+ (
+ cd wt-multi &&
+ test_commit initial &&
+ git worktree add -b linked-branch ../wt-linked HEAD &&
+ git worktree add -b prefix-branch ../wt-prefix/linked HEAD
+ ) &&
+ wt_main="$(cd wt-multi && pwd)" &&
+ wt_linked="$(cd wt-linked && pwd)" &&
+ wt_prefix_parent="$(cd wt-prefix && pwd)" &&
+ cat >>wt-multi/.git/config <<-EOF &&
+ [includeIf "worktree:$wt_main"]
+ path = main-config
+ [includeIf "worktree:$wt_linked"]
+ path = linked-config
+ [includeIf "worktree:$wt_prefix_parent/"]
+ path = prefix-config
+ EOF
+ echo "[test]mainvar=main" >wt-multi/.git/main-config &&
+ echo "[test]linkedvar=linked" >wt-multi/.git/linked-config &&
+ echo "[test]prefixvar=prefix" >wt-multi/.git/prefix-config &&
+ echo main >expect &&
+ git -C wt-multi config test.mainvar >actual &&
+ test_cmp expect actual &&
+ test_must_fail git -C wt-multi config test.linkedvar &&
+ test_must_fail git -C wt-multi config test.prefixvar &&
+ echo linked >expect &&
+ git -C wt-linked config test.linkedvar >actual &&
+ test_cmp expect actual &&
+ test_must_fail git -C wt-linked config test.mainvar &&
+ test_must_fail git -C wt-linked config test.prefixvar &&
+ echo prefix >expect &&
+ git -C wt-prefix/linked config test.prefixvar >actual &&
+ test_cmp expect actual &&
+ test_must_fail git -C wt-prefix/linked config test.mainvar &&
+ test_must_fail git -C wt-prefix/linked config test.linkedvar
+'
+
+test_expect_success SYMLINKS 'conditional include, worktree resolves symlinks' '
+ mkdir real-wt &&
+ ln -s real-wt link-wt &&
+ git init link-wt/repo &&
+ (
+ cd link-wt/repo &&
+ # repo->worktree resolves symlinks, so use real path in pattern
+ echo "[includeIf \"worktree:**/real-wt/repo\"]path=bar-link" >>.git/config &&
+ echo "[test]wtlink=2" >.git/bar-link &&
+ echo 2 >expect &&
+ git config test.wtlink >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success !CASE_INSENSITIVE_FS 'conditional include, worktree, case sensitive' '
+ git init wt-case &&
+ (
+ cd wt-case &&
+ test_commit initial &&
+ wt_path="$(pwd)" &&
+ wt_upper=$(echo "$wt_path" | tr a-z A-Z) &&
+ echo "[includeIf \"worktree:$wt_upper\"]path=case-inc" >>.git/config &&
+ echo "[test]wtcase=1" >.git/case-inc &&
+ test_must_fail git config test.wtcase
+ )
+'
+
+test_expect_success 'conditional include, worktree, icase' '
+ git init wt-icase &&
+ (
+ cd wt-icase &&
+ test_commit initial &&
+ wt_path="$(pwd)" &&
+ wt_upper=$(echo "$wt_path" | tr a-z A-Z) &&
+ echo "[includeIf \"worktree/i:$wt_upper\"]path=icase-inc" >>.git/config &&
+ echo "[test]wticase=1" >.git/icase-inc &&
+ echo 1 >expect &&
+ git config test.wticase >actual &&
+ test_cmp expect actual
+ )
+'
+
+# The "worktree" condition cannot match during early config reading
+# because the repository object is not yet fully initialized and
+# repo_get_work_tree() returns NULL.
+test_expect_success 'conditional include, worktree does not match in early config' '
+ git init wt-early &&
+ (
+ cd wt-early &&
+ test_commit initial &&
+ wt_path="$(pwd)" &&
+ echo "[includeIf \"worktree:$wt_path\"]path=early-inc" >>.git/config &&
+ echo "[test]wtearly=1" >.git/early-inc &&
+ test-tool config read_early_config test.wtearly >actual &&
+ test_must_be_empty actual
+ )
+'
+
+# Use a loose pattern so the "present in non-worktree cases" check works
+# for Unix-style absolute paths and Windows paths like D:/a/git/...
+test_expect_success 'conditional include, worktree without repository' '
+ test_when_finished "rm -f .gitconfig config.inc" &&
+ git config set -f .gitconfig "includeIf.worktree:**.path" config.inc &&
+ git config set -f config.inc foo.bar baz &&
+ git config get foo.bar &&
+ test_must_fail nongit git config get foo.bar
+'
+
+test_expect_success 'conditional include, worktree without repository but explicit nonexistent Git directory' '
+ test_when_finished "rm -f .gitconfig config.inc" &&
+ git config set -f .gitconfig "includeIf.worktree:**.path" config.inc &&
+ git config set -f config.inc foo.bar baz &&
+ git config get foo.bar &&
+ test_must_fail nongit git --git-dir=nonexistent config get foo.bar
+'
+
test_done
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH v8 2/2] config: add "worktree" and "worktree/i" includeIf conditions
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
0 siblings, 0 replies; 11+ messages in thread
From: Patrick Steinhardt @ 2026-07-13 11:16 UTC (permalink / raw)
To: me; +Cc: git, Kristoffer Haugsbakk, Junio C Hamano, Phillip Wood
On Fri, Jul 10, 2026 at 02:43:30PM +0800, Chen Linxuan via B4 Relay wrote:
> diff --git a/Documentation/config.adoc b/Documentation/config.adoc
> index 15b1a4d59347..1ef72de62f2b 100644
> --- a/Documentation/config.adoc
> +++ b/Documentation/config.adoc
> @@ -146,6 +146,51 @@ refer to linkgit:gitignore[5] for details. For convenience:
> This is the same as `gitdir` except that matching is done
> case-insensitively (e.g. on case-insensitive file systems)
>
> +`worktree`::
> + The data that follows the keyword `worktree` and a colon is used as a
> + glob pattern. If the working directory of the current worktree matches
> + the pattern, the include condition is met.
> ++
> +The worktree location is the path where files are checked out (as returned
> +by `git rev-parse --show-toplevel`). This is different from `gitdir`, which
> +matches the `.git` directory path. In a linked worktree, the worktree path
> +is the directory where that worktree's files are located, not the main
> +repository's `.git` directory.
> ++
> +The pattern uses the same glob syntax as `gitdir` (including `~/`, `./`,
> +`**/`, and trailing-`/` prefix matching). This condition will never match
> +in a bare repository (which has no worktree).
> ++
> +Unlike `gitdir`, the `worktree` condition currently matches only the
> +realpath-resolved worktree location. If the working tree was entered via a
> +symbolic link, a pattern that uses the symbolic-link spelling may not match;
> +use the real path instead.
It might be worth noticing that this is essentially a limitation and
that this limitation may be fixed at a later point in time. But that
alone is not worth a reroll.
> diff --git a/t/t1305-config-include.sh b/t/t1305-config-include.sh
> index f3892578e4ff..4e840dfdb35b 100755
> --- a/t/t1305-config-include.sh
> +++ b/t/t1305-config-include.sh
> @@ -396,4 +396,132 @@ test_expect_success 'onbranch without repository but explicit nonexistent Git di
> test_must_fail nongit git --git-dir=nonexistent config get foo.bar
> '
>
> +# worktree: conditional include tests
> +
> +test_expect_success 'conditional include, worktree bare repo' '
> + git init --bare wt-bare &&
> + (
> + cd wt-bare &&
> + echo "[includeIf \"worktree:/\"]path=bar-bare" >>config &&
> + echo "[test]wtbare=1" >bar-bare &&
> + test_must_fail git config test.wtbare
> + )
> +'
> +
> +test_expect_success 'conditional include, worktree multiple worktrees' '
> + git init wt-multi &&
> + (
> + cd wt-multi &&
> + test_commit initial &&
> + git worktree add -b linked-branch ../wt-linked HEAD &&
> + git worktree add -b prefix-branch ../wt-prefix/linked HEAD
> + ) &&
> + wt_main="$(cd wt-multi && pwd)" &&
> + wt_linked="$(cd wt-linked && pwd)" &&
> + wt_prefix_parent="$(cd wt-prefix && pwd)" &&
> + cat >>wt-multi/.git/config <<-EOF &&
> + [includeIf "worktree:$wt_main"]
> + path = main-config
> + [includeIf "worktree:$wt_linked"]
> + path = linked-config
> + [includeIf "worktree:$wt_prefix_parent/"]
> + path = prefix-config
> + EOF
> + echo "[test]mainvar=main" >wt-multi/.git/main-config &&
> + echo "[test]linkedvar=linked" >wt-multi/.git/linked-config &&
> + echo "[test]prefixvar=prefix" >wt-multi/.git/prefix-config &&
> + echo main >expect &&
> + git -C wt-multi config test.mainvar >actual &&
> + test_cmp expect actual &&
> + test_must_fail git -C wt-multi config test.linkedvar &&
> + test_must_fail git -C wt-multi config test.prefixvar &&
> + echo linked >expect &&
> + git -C wt-linked config test.linkedvar >actual &&
> + test_cmp expect actual &&
> + test_must_fail git -C wt-linked config test.mainvar &&
> + test_must_fail git -C wt-linked config test.prefixvar &&
> + echo prefix >expect &&
> + git -C wt-prefix/linked config test.prefixvar >actual &&
> + test_cmp expect actual &&
> + test_must_fail git -C wt-prefix/linked config test.mainvar &&
> + test_must_fail git -C wt-prefix/linked config test.linkedvar
> +'
> +
> +test_expect_success SYMLINKS 'conditional include, worktree resolves symlinks' '
> + mkdir real-wt &&
> + ln -s real-wt link-wt &&
> + git init link-wt/repo &&
> + (
> + cd link-wt/repo &&
> + # repo->worktree resolves symlinks, so use real path in pattern
> + echo "[includeIf \"worktree:**/real-wt/repo\"]path=bar-link" >>.git/config &&
> + echo "[test]wtlink=2" >.git/bar-link &&
> + echo 2 >expect &&
> + git config test.wtlink >actual &&
> + test_cmp expect actual
> + )
> +'
There should arguably be a test with `test_expect_failure` that shows
that we in theory _want_ to use both the realpath, but also the
symlinked path to resolve this.
In any case, I think this version is good enough. It's a bit sad that we
cannot easily handle symlinked paths right now, but I'd say that this is
acceptable for now. We might fix this eventually, once we have cleaned
up "setup.c" to not modify global state left and right.
Thanks!
Patrick
^ permalink raw reply [flat|nested] 11+ messages in thread