Skip to content

feat(config): set environment variables per directory with [[context]] rules - #145

Merged
timvw merged 1 commit into
mainfrom
feat/context-path-rules
Aug 20, 2026
Merged

feat(config): set environment variables per directory with [[context]] rules#145
timvw merged 1 commit into
mainfrom
feat/context-path-rules

Conversation

@timvw

@timvw timvw commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Closes #138.

Path patterns can already read {.env.X}, but supplying the value meant exporting it per command or wiring up direnv. This adds includeIf-style rules that bind variables to a directory tree instead:

[[context]]
when_path = "~/dev/repos/work"
env = { WT_CATEGORY = "work" }
wt clone acme/api      # ~/dev/repos/work/acme/api/main, no export needed

Semantics

Matching is a path prefix, not a glob — filepath.Match has no **, and a glob library would be a new dependency for a module with 8 direct ones. Paths are resolved (~ expanded, absolute, symlinks followed) before comparison, and compared case-insensitively on macOS and Windows. ~/dev/work does not match ~/dev/workshop.

Composition: every matching rule applies, later rules winning per variable — the same way git config treats repeated keys. A broad rule can set a default that a narrower one overrides.

Which path is matched differs by command, and in neither case is it the rendered destination, so nothing is circular:

Command Matched against
wt create, wt co, wt pr, wt mr, wt migrate the repository's main checkout
wt clone the working directory

Matching the main checkout rather than the cwd means every linked worktree of a repo resolves to the same rule, even when worktree_root puts them in a completely different tree. For wt clone no repo exists yet, so the cwd is the only signal available.

Rules are rendering-only. They are merged underneath os.Environ() into the template's env map and never exported. An actual export still wins — including an empty one, so WT_CATEGORY= wt clone still collapses the segment — and hooks see an unchanged environment.

Source: user config file only. A repo's .wt.toml is committed by someone else; letting it inject variables would let a cloned repository redirect where its own worktrees land. Reading rules from git config needs multi-value support in the wt.* parser (cmd/config.go:153 flattens into a map[string]string, so a rule with two env entries would collide) — left as a follow-up on #138.

Notes

  • docs/configuration.md now leads with [[context]]; the direnv recipe added in docs: add direnv recipe for per-directory categories #143 is kept as a "without wt configuration" subsection.
  • wt config init emits a commented [[context]] example.
  • 16 new tests in cmd/context_test.go cover matching (including the workshop/work sibling case), per-variable composition, export precedence, symlink resolution, nonexistent paths, ~ expansion, the config-file/.wt.toml split, and both call sites.

…] rules

Path patterns can already read {.env.X}, but supplying the value meant
exporting it per command or wiring up direnv. Add includeIf-style rules
that bind variables to a directory tree instead:

    [[context]]
    when_path = "~/dev/repos/work"
    env = { WT_CATEGORY = "work" }

Matching is a path prefix (not a glob), evaluated after resolving
symlinks, case-insensitively on macOS and Windows. Every matching rule
applies, later rules winning per variable, mirroring how git config
treats repeated keys.

Which path is matched differs by command, and neither is the rendered
destination, so nothing is circular:

  - repo-scoped commands match the repository's main checkout, so all
    linked worktrees of a repo resolve to the same rule even when they
    live in a different tree;
  - wt clone matches the working directory, the only signal available
    before a repo exists.

Rules are rendering-only: they are merged underneath os.Environ() into
the template's env map and never exported, so an actual export still
wins (including an empty one) and hooks see an unchanged environment.

They are read from the user config file only. A repo's .wt.toml is
committed by someone else and must not be able to redirect where
worktrees land. Reading them from git config needs multi-value support
in the wt.* parser and is left as a follow-up.

Closes #138
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.46154% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 43.78%. Comparing base (ecd4f2c) to head (ddf2c6d).

Files with missing lines Patch % Lines
cmd/context.go 88.88% 5 Missing ⚠️
cmd/clone_path.go 75.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #145      +/-   ##
==========================================
+ Coverage   43.06%   43.78%   +0.72%     
==========================================
  Files          34       35       +1     
  Lines        3597     3647      +50     
==========================================
+ Hits         1549     1597      +48     
- Misses       2048     2050       +2     
Files with missing lines Coverage Δ
cmd/config.go 88.77% <100.00%> (+0.12%) ⬆️
cmd/worktree_path.go 61.06% <100.00%> (ø)
cmd/clone_path.go 69.04% <75.00%> (-0.19%) ⬇️
cmd/context.go 88.88% <88.88%> (ø)

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@timvw
timvw merged commit 47629d7 into main Aug 20, 2026
16 checks passed
@timvw
timvw deleted the feat/context-path-rules branch August 20, 2026 10:09
timvw added a commit that referenced this pull request Aug 20, 2026
#145 shipped context rules with the config file as their only source, which
left them as the one setting that could not live in git config — awkward for
a feature modelled on git's own includeIf, which is git config.

    git config --global wt.context.work.whenpath "~/dev/repos/work"
    git config --global --add wt.context.work.env "WT_CATEGORY=work"
    git config --global --add wt.context.work.env "WT_ORG=acme"

The loader kept one value per key, which is right for every scalar setting but
loses all but the last variable of a rule. It now returns entries in the order
git listed them, and gitConfigValues collapses them to last-wins for the
scalars, so their behaviour is unchanged. Order is kept because it is what
decides how rules compose.

The two sources compose rather than replace: git config rules are evaluated
first, then the config file's, under the same later-definitions-win rule that
already governs rules within one source. So the config file wins wherever both
cover the same path — the documented precedence — while a git config rule for
an unrelated tree keeps working instead of vanishing the moment a [[context]]
block is added to the file.

Global scope only. A rule scoped to one repository is redundant, since that
repository could set wt.pattern directly, and keeping rules out of --local
holds the same user-owned line that already keeps them out of a committed
.wt.toml. The system scope is skipped for a duller reason: wt reads no system
git config for any setting, and making this the exception would be worse than
the gap.

Closes #146
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

design: per-group context — where a category value comes from for a set of repos

1 participant