config: add "worktree" and "worktree/i" includeIf conditions

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> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Chen Linxuan committed Jul 10, 2026 at 14:43 UTC 5004829756596498a20305b3fce8387629396835
3 files changed +187
Documentation/config.adoc
+53
@@ -146,6 +146,51 @@ refer to linkgit:gitignore[5] for details. For convenience:
146 This is the same as `gitdir` except that matching is done
147 case-insensitively (e.g. on case-insensitive file systems)
148
149 +`worktree`::
150 + The data that follows the keyword `worktree` and a colon is used as a
151 + glob pattern. If the working directory of the current worktree matches
152 + the pattern, the include condition is met.
153 ++
154 +The worktree location is the path where files are checked out (as returned
155 +by `git rev-parse --show-toplevel`). This is different from `gitdir`, which
156 +matches the `.git` directory path. In a linked worktree, the worktree path
157 +is the directory where that worktree's files are located, not the main
158 +repository's `.git` directory.
159 ++
160 +The pattern uses the same glob syntax as `gitdir` (including `~/`, `./`,
161 +`**/`, and trailing-`/` prefix matching). This condition will never match
162 +in a bare repository (which has no worktree).
163 ++
164 +Unlike `gitdir`, the `worktree` condition currently matches only the
165 +realpath-resolved worktree location. If the working tree was entered via a
166 +symbolic link, a pattern that uses the symbolic-link spelling may not match;
167 +use the real path instead.
168 ++
169 +This is useful when you want to apply configuration based on where the
170 +working tree is located on the filesystem. For example, a contributor who
171 +works on the same project both personally and as an employee can use
172 +different `user.name` and `user.email` values depending on which directory
173 +the worktree is checked out under:
174 ++
175 +----
176 +[includeIf "worktree:/home/user/work/"]
177 + path = ~/.config/git/work.inc
178 +[includeIf "worktree:/home/user/personal/"]
179 + path = ~/.config/git/personal.inc
180 +----
181 ++
182 +While `extensions.worktreeConfig` (see linkgit:git-worktree[1]) also supports
183 +per-worktree configuration, it stores the config inside each repository's
184 +`.git/config.worktree` file and requires running `git config --worktree`
185 +inside each worktree individually. In contrast, `includeIf "worktree:..."`
186 +can be set once in a global or system-level configuration file (e.g.
187 +`~/.config/git/config`) and applies to all repositories at once based on
188 +their worktree location.
189 +
190 +`worktree/i`::
191 + This is the same as `worktree` except that matching is done
192 + case-insensitively (e.g. on case-insensitive file systems)
193 +
194 `onbranch`::
195 The data that follows the keyword `onbranch` and a colon is taken to be a
196 pattern with standard globbing wildcards and two additional
@@ -244,6 +289,14 @@ Example
289 [includeIf "gitdir:~/to/group/"]
290 path = /path/to/foo.inc
291
292 +; include if the worktree is at /path/to/project-build
293 +[includeIf "worktree:/path/to/project-build"]
294 + path = build-config.inc
295 +
296 +; include for all worktrees inside /path/to/group
297 +[includeIf "worktree:/path/to/group/"]
298 + path = group-config.inc
299 +
300 ; relative paths are always relative to the including
301 ; file (if the condition is true); their location is not
302 ; affected by the condition
config.c
+6
@@ -400,6 +400,12 @@ static int include_condition_is_true(const struct key_value_info *kvi,
400 return include_by_path(kvi, opts->git_dir, cond, cond_len, 0);
401 else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len))
402 return include_by_path(kvi, opts->git_dir, cond, cond_len, 1);
403 + else if (skip_prefix_mem(cond, cond_len, "worktree:", &cond, &cond_len))
404 + return include_by_path(kvi, inc->repo ? repo_get_work_tree(inc->repo) : NULL,
405 + cond, cond_len, 0);
406 + else if (skip_prefix_mem(cond, cond_len, "worktree/i:", &cond, &cond_len))
407 + return include_by_path(kvi, inc->repo ? repo_get_work_tree(inc->repo) : NULL,
408 + cond, cond_len, 1);
409 else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len))
410 return include_by_branch(inc, cond, cond_len);
411 else if (skip_prefix_mem(cond, cond_len, "hasconfig:remote.*.url:", &cond,
t/t1305-config-include.sh
+128
@@ -396,4 +396,132 @@ test_expect_success 'onbranch without repository but explicit nonexistent Git di
396 test_must_fail nongit git --git-dir=nonexistent config get foo.bar
397 '
398
399 +# worktree: conditional include tests
400 +
401 +test_expect_success 'conditional include, worktree bare repo' '
402 + git init --bare wt-bare &&
403 + (
404 + cd wt-bare &&
405 + echo "[includeIf \"worktree:/\"]path=bar-bare" >>config &&
406 + echo "[test]wtbare=1" >bar-bare &&
407 + test_must_fail git config test.wtbare
408 + )
409 +'
410 +
411 +test_expect_success 'conditional include, worktree multiple worktrees' '
412 + git init wt-multi &&
413 + (
414 + cd wt-multi &&
415 + test_commit initial &&
416 + git worktree add -b linked-branch ../wt-linked HEAD &&
417 + git worktree add -b prefix-branch ../wt-prefix/linked HEAD
418 + ) &&
419 + wt_main="$(cd wt-multi && pwd)" &&
420 + wt_linked="$(cd wt-linked && pwd)" &&
421 + wt_prefix_parent="$(cd wt-prefix && pwd)" &&
422 + cat >>wt-multi/.git/config <<-EOF &&
423 + [includeIf "worktree:$wt_main"]
424 + path = main-config
425 + [includeIf "worktree:$wt_linked"]
426 + path = linked-config
427 + [includeIf "worktree:$wt_prefix_parent/"]
428 + path = prefix-config
429 + EOF
430 + echo "[test]mainvar=main" >wt-multi/.git/main-config &&
431 + echo "[test]linkedvar=linked" >wt-multi/.git/linked-config &&
432 + echo "[test]prefixvar=prefix" >wt-multi/.git/prefix-config &&
433 + echo main >expect &&
434 + git -C wt-multi config test.mainvar >actual &&
435 + test_cmp expect actual &&
436 + test_must_fail git -C wt-multi config test.linkedvar &&
437 + test_must_fail git -C wt-multi config test.prefixvar &&
438 + echo linked >expect &&
439 + git -C wt-linked config test.linkedvar >actual &&
440 + test_cmp expect actual &&
441 + test_must_fail git -C wt-linked config test.mainvar &&
442 + test_must_fail git -C wt-linked config test.prefixvar &&
443 + echo prefix >expect &&
444 + git -C wt-prefix/linked config test.prefixvar >actual &&
445 + test_cmp expect actual &&
446 + test_must_fail git -C wt-prefix/linked config test.mainvar &&
447 + test_must_fail git -C wt-prefix/linked config test.linkedvar
448 +'
449 +
450 +test_expect_success SYMLINKS 'conditional include, worktree resolves symlinks' '
451 + mkdir real-wt &&
452 + ln -s real-wt link-wt &&
453 + git init link-wt/repo &&
454 + (
455 + cd link-wt/repo &&
456 + # repo->worktree resolves symlinks, so use real path in pattern
457 + echo "[includeIf \"worktree:**/real-wt/repo\"]path=bar-link" >>.git/config &&
458 + echo "[test]wtlink=2" >.git/bar-link &&
459 + echo 2 >expect &&
460 + git config test.wtlink >actual &&
461 + test_cmp expect actual
462 + )
463 +'
464 +
465 +test_expect_success !CASE_INSENSITIVE_FS 'conditional include, worktree, case sensitive' '
466 + git init wt-case &&
467 + (
468 + cd wt-case &&
469 + test_commit initial &&
470 + wt_path="$(pwd)" &&
471 + wt_upper=$(echo "$wt_path" | tr a-z A-Z) &&
472 + echo "[includeIf \"worktree:$wt_upper\"]path=case-inc" >>.git/config &&
473 + echo "[test]wtcase=1" >.git/case-inc &&
474 + test_must_fail git config test.wtcase
475 + )
476 +'
477 +
478 +test_expect_success 'conditional include, worktree, icase' '
479 + git init wt-icase &&
480 + (
481 + cd wt-icase &&
482 + test_commit initial &&
483 + wt_path="$(pwd)" &&
484 + wt_upper=$(echo "$wt_path" | tr a-z A-Z) &&
485 + echo "[includeIf \"worktree/i:$wt_upper\"]path=icase-inc" >>.git/config &&
486 + echo "[test]wticase=1" >.git/icase-inc &&
487 + echo 1 >expect &&
488 + git config test.wticase >actual &&
489 + test_cmp expect actual
490 + )
491 +'
492 +
493 +# The "worktree" condition cannot match during early config reading
494 +# because the repository object is not yet fully initialized and
495 +# repo_get_work_tree() returns NULL.
496 +test_expect_success 'conditional include, worktree does not match in early config' '
497 + git init wt-early &&
498 + (
499 + cd wt-early &&
500 + test_commit initial &&
501 + wt_path="$(pwd)" &&
502 + echo "[includeIf \"worktree:$wt_path\"]path=early-inc" >>.git/config &&
503 + echo "[test]wtearly=1" >.git/early-inc &&
504 + test-tool config read_early_config test.wtearly >actual &&
505 + test_must_be_empty actual
506 + )
507 +'
508 +
509 +# Use a loose pattern so the "present in non-worktree cases" check works
510 +# for Unix-style absolute paths and Windows paths like D:/a/git/...
511 +test_expect_success 'conditional include, worktree without repository' '
512 + test_when_finished "rm -f .gitconfig config.inc" &&
513 + git config set -f .gitconfig "includeIf.worktree:**.path" config.inc &&
514 + git config set -f config.inc foo.bar baz &&
515 + git config get foo.bar &&
516 + test_must_fail nongit git config get foo.bar
517 +'
518 +
519 +test_expect_success 'conditional include, worktree without repository but explicit nonexistent Git directory' '
520 + test_when_finished "rm -f .gitconfig config.inc" &&
521 + git config set -f .gitconfig "includeIf.worktree:**.path" config.inc &&
522 + git config set -f config.inc foo.bar baz &&
523 + git config get foo.bar &&
524 + test_must_fail nongit git --git-dir=nonexistent config get foo.bar
525 +'
526 +
527 test_done