v0.3.0
Two commits since v0.2.0, both features: worktrees can now materialise the untracked files they need, and [[context]] rules can live in your global git config instead of the config file.
Features
-
Built-in file copy (#144, #148) — a
[files]section materialises the untracked files a new worktree needs (.env,.envrc, local settings), with reflink/CoW where the filesystem offers it:[files] copy = [".env", ".envrc", ".claude/settings.local.json"] # gitignore syntax link = ["node_modules"] # symlink instead exclude = ["*.pem"] # applied last, non-overridable copy_ignored = false # or: copy every ignored file
Runs automatically on
wt create,wt checkout,wt prandwt mr, aftergit worktree addand before thepost_*hooks — including when the worktree already exists, so one made before[files]was configured catches up. Never fatal: a failure is reported and the worktree survives.A
.worktreeincludeat the main-worktree root is unioned intocopy, in gitignore syntax, for repos that would rather commit the list than a.wt.toml. The list keys accumulate across layers (config file → repo.wt.toml→.worktreeinclude), deduped first-seen;copy_ignoredfollows the ordinary scalar precedence chain, spelledwt.copyIgnoredin git config.wt copy [branch]does the same on demand, with--dry-run,--forceand--from;--no-copyskips it once andWT_FILES_DISABLED=1turns it off entirely. Copies useclonefile(2)on APFS,FICLONEon Btrfs/XFS andcopy_file_range(2)where available, through a bounded worker pool, falling back to a buffered copy — which is what turns "copy a few dotfiles" into "clone a multi-gigabyte build cache".wt infogains a Files section,wt config showreportscopy_ignored, and every command supports--format json.Why this needs no
wt trust, unlike hooks.[files]cannot execute anything. It names paths inside two worktrees, and every path is checked against seven invariants, each with a test incmd/files_security_test.go: source patterns resolve strictly inside the main worktree (F1); destination paths strictly inside the new one (F2);..segments are rejected at config-load time, gitignore escapes included (F3); symlinks are copied as symlinks, never dereferenced (F4); files git tracks are never copied, in either worktree, by construction (F5); an existing destination is never overwritten without--force(F6); nothing outside the two worktrees andos.TempDir()is read or written (F7). Candidates come fromgit ls-files --others --ignored --exclude-standard, so a tracked file is never a candidate to begin with, and gitignore semantics are git's rather than a reimplementation. VCS directories and registered worktrees of the same repo are excluded unconditionally.copy_ignoreddefaults tofalse. Copying every ignored file is a deliberate opt-in: silently moving gigabytes, or a.env.production, into a new worktree is not a default worth having.Edge cases worth knowing about: a path the destination branch tracks is never written,
--forceincluded — including when it is absent there (deleted, or a sparse checkout), and including a tracked ancestor or subtree. Case-insensitive destinations (APFS, NTFS, and also ext4 casefold or CIFS — probed, not assumed) report two source paths differing only in case as a collision rather than merging them, directories included, settled before anything is created.--dry-runpredicts exactly what the real run does, down to which paths a copy claims before a link can have them. No-clobber is TOCTOU-free (O_CREAT|O_EXCL);--forcereplaces atomically via a temp file and rename. -
[[context]]rules from global git config (#146, #147) — a follow-up to #145, which shipped[[context]]rules with the config file as their only source. Rules can now live in~/.gitconfig: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"
wt.context.<name>.<key>is a git subsection, so<name>is a handle that makes the rule removable withgit config --global --remove-section 'wt.context.work'.Git config rules are evaluated first, then the config file's, under the same "later definitions win per variable" rule that already governs rules within a single source — so the two compose rather than one replacing the other. Wholesale replacement would have matched how scalars behave, but it makes adding one unrelated
[[context]]block toconfig.tomlsilently delete every rule in~/.gitconfig.Global only. Not
--local: a path rule scoped to a single repository is redundant, since that repository could setwt.patterndirectly, and.git/configis shared by every worktree of a repo. Not--systemeither, for a duller reason —wtreads no system git config for any setting, and makingwt.context.*obey a scope thatwt.rootignores would be a worse inconsistency than the gap.Internally,
gitConfigFnnow returns[]gitConfigEntryin the order git listed them rather than a last-wins map, because a rule setsenvonce per variable with--add. Scalar settings are unchanged — they collapse to last-wins as before.
Docs
docs/configuration.mdgains a Files section (patterns, precedence,.worktreeinclude, case-insensitive destinations, dry-run output) and documentswt.copyIgnoredalongside the other git-config keys.docs/examples.mdgains copy recipes.
Full commit list: v0.2.0...v0.3.0