Commit Graph

100 Commits

Author SHA1 Message Date
Worktrunk Bot 96c6c846f7 fix(shell): register completions under the --cmd name, not clap's (#3817)
## Problem

`wt config shell init <shell> --cmd <name>` renames the shell wrapper
and its lazy completion loader, but the registration that loader evals
comes from clap, which derives every identifier in it from its own
compile-time `Command` name (`wt`) — not from `argv[0]` and not from
`--cmd`. The two halves never agreed:

```console
$ wt config shell init zsh --cmd wot | grep _clap
        if ! (( $+functions[_clap_dynamic_completer_wot] )); then
        _clap_dynamic_completer_wot "$@"

$ COMPLETE=zsh wt | grep -oE '_clap_dynamic_completer_[a-z_]*' | sort -u
_clap_dynamic_completer_wt
```

Nothing completed, and because the guard never became true the
completion script was regenerated and re-evaluated on *every* TAB. Same
shape in bash (`_clap_complete_*`); PowerShell emitted
`Register-ArgumentCompleter -Native -CommandName wt`, so the `--cmd`
name was never registered at all. The documented `--cmd=git-wt` case
(the Windows Terminal conflict) was broken too — including for a binary
genuinely installed under that name, since clap's name comes from the
declaration rather than `argv[0]`.

There is a second, sharper edge: zsh's registration ends with `compdef
<completer> <cmd>`, so the first TAB on `wot` also bound worktrunk's
completer to plain `wt` — handing completions to the *other* `wt` that
`--cmd` exists to step around.

fish and nushell were unaffected. Both register a completer that shells
out to the binary rather than depending on a clap-emitted identifier, so
the reporter's "unverified" row for fish is a pass.

## Solution

The bash, zsh, and PowerShell loaders now pass the name they bind in
`WORKTRUNK_COMPLETE_NAME`, and `registration_name()` in
`src/completion.rs` emits the registration under that name (validated
through the same `validate_shell_command_name` guard `--cmd` uses, since
the value lands verbatim in generated shell code). The fallback is
`binary_name()`, which covers a binary installed as `git-wt` and invoked
directly. The templates apply clap's own `-` → `_` escaping to the
function they call, so `--cmd git-wt` guards on `_clap_complete_git_wt`
rather than the invalid `_clap_complete_git-wt`.

That fixes all four shells and the stray `compdef` in one place, rather
than pinning the templates to clap's internal naming:

```console
$ WORKTRUNK_COMPLETE_NAME=wot COMPLETE=zsh wt | grep -oE '_clap_dynamic_completer_[a-z_]*|compdef .*' | sort -u
_clap_dynamic_completer_wot
compdef _clap_dynamic_completer_wot wot
```

## Testing

Two reproduction tests in `tests/integration_tests/completion.rs`, both
failing before the change:

- `test_init_custom_cmd_defines_clap_completer_in_bash` drives the whole
chain through a real bash — generate the init script with `--cmd`, call
the loader it defines, then assert clap's completer function exists
afterwards. Printed `MISSING` before, `DEFINED` after. Cases for `wot`
and `git-wt`.
- `test_completion_registration_uses_shell_integration_cmd_name` covers
zsh and PowerShell, which CI can't drive: the identifier the init script
references must be the one the registration defines, and the `compdef` /
`-CommandName` target must be the `--cmd` name.

`cargo test --lib --bins` and `cargo test --test integration` are
otherwise green (one unrelated failure locally,
`test_copy_ignored_preserves_file_executable_permissions`, from this
sandbox's `umask 0002`), and `cargo clippy --all-targets --all-features`
/ `cargo fmt --check` are clean.

---
Closes #3816 — automated triage

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-08-13 16:44:02 -07:00
Maximilian Roos dd453304ed test: fold the mock stub into the wt binary (#3712)
`cargo test --test integration` neither built nor rebuilt `mock-stub`: a
target filter deselects the dummy test that pulled it in, so a fresh
tree panicked ("mock-stub binary not found") and a warm one could run a
stale stub. The chain that compensated — the separate helper package,
its dummy `builds.rs`, the `default-members` entry, nextest's
experimental `build-bins` setup script, `workspace_bin()` — existed only
because cargo-dist ships every `[[bin]]`, and carried a TODO to collapse
once that changed. dist 0.30.2 does support per-binary exclusion now
(`[dist.binaries]`, since 0.29.0; verified with `dist plan`), but the
TODO's plan has a hole it predates: `cargo install worktrunk` installs
every feature-satisfied `[[bin]]`, and dist config doesn't govern
crates.io installs.

So the mock commands are now the `wt` binary itself. `mock_commands`
links `wt` under the mock's name (`gh`, `glab`, …), and `main()`
dispatches to the ported playback (`testing::mock_stub`) when
`WORKTRUNK_TEST_MOCK_CONFIG_DIR` is set, argv[0] is a foreign name, and
the config dir holds `<argv0>.json` for it. The existence check keeps wt
under a foreign argv[0] *without* a config being wt — the
argv0-validation security test symlinks it as `wt;touch` and must reach
wt's own rejection. The shipped binary already compiles the whole
`testing` module unconditionally, so this adds no new category of test
code to it. Windows links `wt.exe` with `hard_link` (copy fallback for
cross-drive dirs); the debug binary is ~67 MB, so per-mock copies stay
the fallback.

Every runner is now safe by construction — cargo rebuilds a package's
own binaries whenever its integration tests build, so there is no
separate artifact to go missing or stale. Deleted: the helper package,
the setup script plus `experimental = ["setup-scripts"]`, the
`default-members` trick, `workspace_bin()`, and `wt_bin()`'s dead
compile-time branch (unit-test targets get neither the runtime variable
nor the `option_env!` value, so the runtime resolution is the one
mechanism).

Validated locally: the pre-merge gate's full suite passes (4542/4542;
its doc step also caught unescaped `argv[0]` intra-doc links in the new
comments, fixed and `cargo doc -Dwarnings` re-verified). With all stub
artifacts purged from `target/`, plain `cargo test --test integration`
on mock-dependent tests builds them and passes — the command that used
to hit the trap. Two integration tests pin the dispatch's argv[0] edges
(an empty argv[0], and a non-UTF8 one), alongside the existing
`wt;touch` carve-out test.

The first commit is the investigation that preceded the fix: it verified
the `wt` binary itself was never subject to the staleness the mock-stub
was, and documented that in `tests/CLAUDE.md`; the fix then narrows that
paragraph further, since the gap it scoped no longer exists.

A two-reviewer subagent round (one prosecuting the diff against the
failure modes documented in the repo's own mock history — the #401/#407
Windows era, #547, #654, #127, #2544, #2730, #2744 — the other
adversarial) then hardened the dispatch. The reserved-name guard is
case-insensitive, matching the config probe, which goes through a
filesystem that equates `WT.json` with `wt.json` on macOS and Windows;
`command_name()` reads `args_os` — `env::args()` panics on a non-Unicode
argument, and this runs inside `main()` on every invocation (caught by
the tend review) — and returns `None` for a degenerate argv[0] instead
of panicking; the `.exe` suffix is stripped explicitly rather than via
`file_stem`, so a dotted mock name (`python3.11`) resolves identically
on every platform; and `copy_mock_binary` is now private —
`MockConfig::write` writes `<name>.json` before linking and is the only
way to create a mock, so a link cannot exist without its config, and the
dispatch's missing-config fall-through can only mean "wt under a foreign
name" (the argv0-validation tests' `wt;touch`), never a half-configured
mock that silently runs real wt with the mocked tool's arguments. The
`Option<&str>` mock helpers whose `None` arm produced exactly such
configless links lost the arm (every caller passed `Some`), and 25
redundant standalone link calls went with it.

The review also surfaced the one remaining spawn-a-stale-binary path
outside the suite: `wt-perf timeline` resolved a sibling `wt` by path,
checked only existence, and told the user to build it manually — so
`cargo run -p wt-perf -- timeline` after a `src/` edit silently measured
stale code. It now builds `wt` first and takes the artifact path from
cargo's `--message-format=json` report rather than deriving a sibling
location, so target-dir and profile overrides can't divert the build
away from where it's resolved; a release wt-perf builds and measures a
release wt. The build runs before the timeline's wall-clock measurement
starts, cargo's progress streams on stderr, and stdout keeps the
`--chrome` JSON contract.

> _This was written by Claude Code on behalf of max-sixty_

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 22:12:21 -07:00
Maximilian Roos 1eded27943 Simplify forge, shell integration, and removal internals (#3662)
This consolidates cross-cutting models that had accumulated parallel
representations, while preserving the CLI and config interfaces.

## What changed

- Forge identity now flows through one `ForgeKind`, with boundary-safe
network-host classification shared by CI, remote references, and
structured repository metadata. Branded SSH aliases such as
`github-personal` remain outside provider dispatch and receive an
actionable `forge.platform` diagnostic when forge data is requested.
- Worktree removal now uses one owned target type and rechecks uncached
worktree topology immediately before compare-and-swap branch deletion,
retaining branches that gained a live or locked checkout.
- Shell integration no longer implements the retired single-file
directive writer. Stale wrappers receive repair guidance, execution
fails closed, and child processes cannot inherit the retired
sourceable-file capability.
- Zsh and Git-version probes are shared, while dead mocks, redundant
dependency edges, serializer detours, pass-through types, and duplicate
tests are removed.

The removal guard deliberately distinguishes live, stale-prunable, and
locked registrations. The detached cleanup path performs the same
record-aware check before deleting a branch ref.

## Testing

`cargo run -- hook pre-merge --yes` passed after merging current
`origin/main`: 4,489 tests, Clippy, formatting, lockfile checks, docs,
doctests, and snapshot review.

> _This was written by Claude Code on behalf of max_.
2026-07-30 02:10:07 -07:00
Maximilian Roos 4554f50ce6 perf(tests): stop leaking a temp dir per test, and measure where the suite's CPU goes (#3604)
Measured where the test suite's CPU actually goes, fixed what was doing
real extra work, and added `task profile-tests` so the measurement is
repeatable.

## Baseline

`cargo nextest run --features shell-integration-tests` on an 18-core
M-series machine: 4,570 tests, ~95s wall, **988 CPU-seconds (332 user +
655 sys)**.

Two thirds is kernel time, so the cost is process creation and
filesystem churn rather than computation. The integration binary is 86%
of summed self-time: 2,184 tests at ~0.6s each, each spawning `wt` (a 65
MB debug binary, ~11ms CPU per spawn against 2.6ms for a trivial
process) and `git` against a fresh fixture copy. It sits in a broad
middle, not a few outliers: 73% of self-time is in tests taking 0.25 to
2.0s.

## One leaked temp directory per test

`isolated_test_cwd()` held a `TempDir` in a `LazyLock`. Statics don't
run destructors at process exit and nextest runs one process per test,
so every test leaked an empty directory into the system temp root.
Measured with `TMPDIR` pointed at a fresh directory: **704 per
integration-suite run**. This machine had accumulated **454,907 entries,
353,268 of them empty strays older than a day**.

Stale entries are cheap to ignore but expensive to enumerate, and
`git::recover::recover_from_path` reads every ancestor directory of a
deleted CWD:

| temp root | `test_recover_from_path_returns_none_for_unrelated_path` |
|---|---|
| 454k entries | 14.2s (34.4s on a quieter run) |
| empty | 0.27s |

One fixed directory replaces it. Leaks per run: 704 to 0, verified
across full suite runs, and the directory is still empty after ~9,000
test executions.

I also checked whether a crowded temp root slows ordinary temp
operations. It does not: create, populate and delete of a fixture-sized
tree ran at 21ms/iter in a 454k-entry parent against 40 to 60ms in an
empty one. The leak's cost is concentrated entirely in code that
enumerates.

## Fixture temp dirs no longer sit in the shared temp root

The fixtures created their temp directories directly in the system temp
dir, among however many entries the machine had put there.
`test_temp_root()` (`$TMPDIR/wt`) roots them one level in, and
`test_tempdir()` replaces `TempDir::new()` across every `TestRepo`
constructor, the mock-command helper, the `temp_home` fixture and the
recovery tests. That test again: **14.2s to 0.06s**.

Two constraints worth knowing, both found by trying the more aggressive
version first:

- **A cache-dir root fails.** `~/Library/Caches` is under `/Users/`,
which a conditional `includeIf "gitdir:/Users/"` matches, so 16 picker
tests fail their commits under `commit.gpgsign` — they drive git through
`Repository::run_command`, the production API with no isolated config.
`step_promote` above was not a one-off: the suite was hermetic against
host git config only because macOS puts `$TMPDIR` outside `/Users/`.
That is the hole the next section closes — at the layer that covers
in-process git, not by choosing where temp files live.
- **The root's name is load-bearing.** A unix socket path cannot exceed
`sun_path`, 104 bytes on macOS. The canonicalized per-user `$TMPDIR` is
56, and `test_copy_ignored_skips_non_regular_files` binds a listener 89
bytes in. `worktrunk-tests` (16 bytes with its slash) overflowed by two;
`wt` costs 3 of the 14 spare.

The ~240 tests that call `tempfile` directly still use the system temp
dir. They are transient and a clean run leaks nothing, so converting
them is tidiness rather than a fix.

## A test that passed by accident of TMPDIR location

`step_promote::test_promote_bare_repo_with_worktrees` drove git through
bare `Cmd::new("git")` instead of `configure_git_env`, so the host's
config applied. A conditional `includeIf "gitdir:/Users/"` enabling
`commit.gpgsign` fails its commit, but only when `TMPDIR` sits inside
the matched tree. macOS puts `TMPDIR` under `/var/folders`, so it
passed; pointing `TMPDIR` anywhere under the home directory failed it.

## The suite was not actually isolated from the developer's git config

Chasing the temp-root change turned up a real hole. `TestRepo` exposes
the production `Repository` type, and `Repository::run_command` builds a
plain `Cmd::new("git")` with no `GIT_CONFIG_GLOBAL`, so it inherits the
test process's own environment. Separately, a bare `wt_command()` had no
`GIT_CONFIG_GLOBAL` at all and fell through to `~/.gitconfig`. 280
`Repository::at/current/discover` constructions across 31 test files and
156 direct `run_command*` calls in test code sat on that path.

Signing was only the symptom that surfaced. Confirmed leaking from a
real developer config: `commit.gpgsign`, `core.fsmonitor` (spawns a
daemon per fixture repo), `worktree.guessremote` (changes `git worktree
add`, directly under test), `help.autocorrect = prompt` (a mistyped git
command blocks). Structurally: `core.hooksPath`, `credential.helper`,
`filter.*` clean/smudge and `diff.external` all execute arbitrary
programs; `url.*.insteadOf` rewrites remotes; `merge.conflictstyle`,
`diff.context`, `rebase.autostash`, `fetch.prune`, `push.default` all
change what the code under test observes. That set is unbounded, which
is why hardening each fixture's local config was rejected — a denylist
can't cover it, and local config cannot unset an inherited `[include]`
or `credential.helper` at all.

The floor is one constant, `shell_exec::HERMETIC_TEST_GIT_ENV` — the
deny pair pointing `GIT_CONFIG_GLOBAL` and `GIT_CONFIG_SYSTEM` at a path
that does not exist, plus the settings the suite needs through
`GIT_CONFIG_COUNT` / `GIT_CONFIG_KEY_n` / `GIT_CONFIG_VALUE_n` — applied
to every child at its spawn site. There is no git-config file anywhere
in the repo. For the spawn sites the harness owns (`git_test_env`,
`configure_git_cmd`, `isolate_subprocess_env` for `wt` children,
`pty_env_vars` for `env_clear`ed PTY children) that's ordinary per-child
env. For the git that *production* code spawns while a test drives it
in-process, the test never holds the command — so the harness flips an
atomic latch (`shell_exec::enable_hermetic_test_env`, called from the
fixture constructors), and `Cmd`, the choke point every production spawn
passes through, applies the floor to each child while the latch is set.
Setting env on a child is safe; it was setting the test process's *own*
env that wasn't (`set_var` races the other test threads), and the latch
dissolves the need for it — no `unsafe`, no pre-`main` constructor, no
cargo `[env]`. Because the latch lives in the binary, every runner
agrees by construction: `cargo test`, nextest, `cargo llvm-cov`, `cargo
bench`, an IDE, a debugger, a directly executed
`target/debug/deps/integration-*`. `.config/nextest.toml` was tested and
rejected, and is now ruled out standing: nextest 0.9.132 has no `[env]`
key, and a `$NEXTEST_ENV` setup script would miss the three non-nextest
runners CI uses (`cargo llvm-cov`, the Nix `cargo test` derivation,
`cargo bench`). A runner-specific knob doesn't fail loudly when another
runner misses it — it yields a different result, usually in the coverage
job whose numbers gate a merge. `tests/CLAUDE.md` → One Result Per Test,
Whatever Runs It records the rule; `.config/nextest.toml` points at it
from the place someone would be tempted.

Acceptance test, since the suite passed before only by accident of
`$TMPDIR` sitting outside `/Users/`: with `test_temp_root()` temporarily
pointed under `$HOME` so a conditional `includeIf "gitdir:/Users/"`
fires, the picker tests go from **20 of 38 failing to 38 of 38
passing**.

**The cost:** the latch is a test-serving switch compiled into
`shell_exec` — one static, one relaxed load per spawn, marked
`TODO(hermetic-env)` with the structural alternative (threading an
explicit env value through `Repository`). `cargo run -- <cmd>` is
untouched: nothing in production latches it, so a developer's own
invocations keep their aliases, credential helper, and identity. The one
production git spawn that bypasses `Cmd` (the fsmonitor daemon launch)
re-applies the floor by hand. (Two earlier shapes were tried and
replaced: cargo's `[env]`, which taxed every `cargo run` and vanished
whenever a test binary ran outside cargo, and a pre-`main` constructor
crate, which every test target had to link and which put the floor on
developers' `cargo bench`-adjacent runs too.)

## In-process tests were reading the developer's worktrunk config too

`config_path()`'s third priority is the real
`~/.config/worktrunk/config.toml`, and a lib-crate test cannot set the
second for itself — `set_var` is `unsafe` and this crate forbids
`unsafe`. So a test reaching priority 3 got the developer's own config.
A `panic!` build of the guard named the live callers immediately:
`git::repository::tests::prewarm_*` read it on every run, and
`set_skip_shell_integration_prompt` /
`set_skip_commit_generation_prompt` reach the same resolver to
**write**.

Priority 3 is now absent under `#[cfg(test)]`, which is compiled out of
the real binary — so unlike the git floor above, this one costs `cargo
run` nothing. It returns `None` rather than panicking like
`approvals_path()`: that guards a mutation target where silent absence
would let a test believe it saved something, whereas this is a lookup
whose absent state is already handled — `require_config_path()` turns it
into an error, so a write still fails loudly while a best-effort read
preloads nothing.

The guard covers lib-crate tests only; `src/commands/` and `src/output/`
link the lib in non-test mode. Nothing there exercises the fall-through
today (968 bin-crate tests create no config under a scratch `$HOME`), so
that is a requirement on new tests, recorded in `tests/CLAUDE.md`.
`system_config_path()` stays unguarded on purpose — machine-wide file,
and `config::deprecation`'s `PendingDefault` rules need the lookup.

## Merging main's parallel fix

#3620 attacked the same in-process hole from the other side, writing
`LOCAL_TEST_CONFIG` into each fixture's own `.git/config`. The two
compose rather than compete and both are kept: the floor denies the
host's config to every git, and the local config supplies what a
hermetic in-process git still needs — an identity, which the floor
deliberately withholds because it has per-command homes already
(`git_test_env`, `LOCAL_TEST_CONFIG`) and `useConfigOnly` fails loudly
if a path misses both. main's structure (`TestConfigPaths`,
`TestRepo::bare`) is kept as-is.

Two docstrings were true on each side and false together:
`test_gitconfig_path` restated the gitconfig inline, and
`LOCAL_TEST_CONFIG` said in-process git reads the developer's
`~/.gitconfig`, which is what the floor prevents. `test_gitconfig_path`
also held its `TempDir` in a `LazyLock`, the leak this PR removes. Both
are moot now: the function is gone with the files.

## Why there is no gitconfig file

The isolation went through two file-based shapes before this one, and
neither earned its keep. Denial never needed a file, because
`GIT_CONFIG_GLOBAL` and `GIT_CONFIG_SYSTEM` *are* the denial; the file
existed only to *set* things, and `GIT_CONFIG_COUNT` is git's
environment spelling of `-c`. Deleting both files also deletes the
`[include]` that kept them from drifting, the per-process write that
broke `test (windows)` on a shared path, and the config-path argument
threaded through 22 call sites.

The floor that remains is the deny pair plus two settings.
`user.useConfigOnly` is a backstop: denial alone leaves git *guessing*
an identity from the OS username and hostname rather than failing, which
is the one way a hermetic suite could still author a commit as the
developer. Nothing exercises it, and that is the reason to keep it.
`rerere.enabled = false` is *set* rather than left unset, so the suite's
rerere state cannot depend on what a fixture happens to carry.
`commit.gpgsign`, `advice.mergeConflict` and `advice.resolveConflict`
are gone: denial leaves git on its own default for the first, and the
snapshot layer strips the gutter-prefixed `hint:` lines the other two
quieted (they vary across git versions), so nothing depends on
suppressing them at the source. The long-dead
`tests/fixtures/template-repo/` fixture went with them.

Once the latch made denial universal, the redundant copies went too:
`git_test_env` no longer restates the deny pair per command (the floor
is denial's only writer, so its value is uniform across every
transport), `LOCAL_TEST_CONFIG` dropped its `commit.gpgsign` (denial
guarantees the default), the platform-dependent `NULL_DEVICE` constant
is deleted, and the `.env.GIT_CONFIG_GLOBAL` snapshot redaction is gone
— the recorded value is one cross-platform constant, so there is nothing
volatile to redact.

I removed `rerere.enabled` first, on a local measurement that was wrong,
and CI failed on all three platforms. The standard fixture is built once
into `target/debug/wt-test-fixtures/` and copied per test, and that
cached copy held an `rr-cache` directory left by a rebase run while the
floor still enabled rerere. Git turns rerere on by itself whenever
`rr-cache` exists, so every local test kept the behavior the change had
just removed, while CI built the fixture fresh and lost it.
`tests/CLAUDE.md` now records the trap: clear the fixture cache before
trusting a local measurement of a git-config change.

**Why the floor can't live in a fixture:** every other test variable is
set on a *child* — `git_test_env` on a git command,
`configure_cli_command` on a `wt` subprocess. In-process git is not a
child the test configures: `Repository::run_command` is production code
building a plain `Cmd::new("git")`, and the test never holds that
command, so there is no place to set env on it — while setting the test
process's own environment is the one thing a test can't do safely
(`set_var` races the other test threads). The identity did move to the
fixtures and the per-command env this way; the denial reaches
production's children through the latch at `Cmd`, the choke point they
all pass through.

Two things fall out of `-c` semantics, both pinned:

- **It outranks a repository's own config**, where a global file would
yield to it. So `init.defaultBranch` cannot live in the floor:
`default_branch.rs` sets that key in a repo to prove `wt` reads it, and
an entry would silently win. The three harness `git init` calls that
relied on the floor now name their branch, as the other two already did.
- **A PTY child is `env_clear`ed**, so it inherits nothing and used to
get the floor through the file. It now gets the family from
`configure_pty_command`, the choke point every PTY spawn routes through,
and again from `pty_env_vars`, whose vector declares a PTY `wt` child's
complete environment. Each copy is pinned by its own test, because the
settings only quiet advice and refuse a guessed identity, so no PTY
assertion would catch their loss.

Four tests wrote their own gitconfig to get `init.defaultBranch` plus an
identity; the harness supplies both, so those writes are gone too. Net
45 lines lighter, and no snapshot changed.

## Measurement

`task profile-tests` builds first, then runs the suite under bash's
`time` keyword (task's own interpreter, mvdan/sh, parses `time` but
hardcodes `user`/`sys` to zero): CPU totals on the console, every
per-test duration in the default profile's `junit.xml`. It began three
sizes larger — a scratch-`TMPDIR` leak check that dragged a `sun_path`
byte budget into the Taskfile, a `/usr/bin/time` dependency GNU-less
Linux lacks, and a `perf` nextest profile whose console slow-listing
restated what junit already carries — and each piece fell to the same
question, whether the measuring goal needed it. Method and how to read
the numbers: `tests/CLAUDE.md` under Profiling the Suite.

## Found, measured, not changed

**The gate keeps a duplicate cargo artifact set.** `RUSTFLAGS='-D
warnings'` on the insta step is part of cargo's fingerprint, so it forks
all 343 crates into a second artifact set: 261 CPU-seconds to prime plus
a duplicate of `target/debug/deps` (`target/debug` here is 35 GB, in a
52 GB `target/`). I removed it and then put it back: the clippy step
that would cover it runs on ubuntu only, while the cross-platform matrix
runs `wt hook pre-merge --yes insta`, so this RUSTFLAGS is the only
thing denying warnings on macOS and Windows. `[lints.rust] warnings =
"deny"` would keep that coverage everywhere without forking the graph
(verified locally: fails on a planted unused variable, recompiles only
worktrunk and wt-perf, feature-check commands still pass), but it also
makes plain local builds fail on warnings and newly exposes `cargo msrv
verify` and the minimal-versions job. That is a workflow call. The
duplication is a one-time cost per artifact set rather than per-edit, so
it is second-order next to the ~600 CPU-seconds each suite run costs.

**`recover_from_path` enumerates every ancestor up to `/`.** At each
ancestor it reads the directory and stats `.git` in every child.
Bounding the child scan to the first *existing* ancestor would preserve
both documented layouts (sibling and nested) and both of that module's
regression tests, but it would break a custom `worktree-path` layout
where the repo is a child of a higher ancestor, so it needs a decision
about which layouts recovery must support.

**Nothing in the gate is the biggest cost; concurrency is.** The five
`[[pre-merge]]` keys are one table, so they run concurrently, and each
is a cargo command that wants the whole machine. They serialize on
cargo's build-directory lock (`Blocking waiting for file lock on build
directory` appears in every run) while test execution overlaps another
step's build. The `lockfile` comment says it "must be first", which
concurrent execution does not provide. Separately, agent worktrees run
whole gates at once: during this work a second worktree ran its own `wt
hook pre-merge` alongside mine, load average hit **154 on 18 cores**,
and the same suite took 147.7s instead of 92.9s.

## Tried and rejected

`[profile.dev] debug = "line-tables-only"` first measured 14% less CPU,
but per-spawn CPU was unchanged, which did not fit the proposed
mechanism. Re-measuring both configurations on a quiet machine gave
602.6s (baseline) against 606.5s (line-tables). The original delta was
contention from a sibling worktree running its own suite.

macOS Gatekeeper (`syspolicyd`) looked like a candidate at 230% CPU, but
300 spawns of the freshly built `wt` cost it 0s, and 0.2s after a
relink, against 0.4s during a 10s idle baseline.

## Verification

`cargo run -- hook pre-merge --yes` green, 4,611 tests passed, run
against a freshly built fixture cache after the switch to the latch. The
latch was verified to be the only source of the variables: the invoking
shell carries no `GIT_CONFIG_*`, and the meta-tests
(`in_process_git_reads_only_the_hermetic_config` asserting the *origin*
of every resolved setting, `pty_env_vars_carry_the_git_config_floor`,
and the `isolate_subprocess_env` scrub test asserting the floor is
re-set after the scrub) pin each transport. Across earlier runs one hit
a single intermittent PTY failure in `shell_wrapper` (exit 127) that
passes 3/3 in isolation and whose code path never touches `wt_command()`
or the shared cwd; a later run was green under heavier load than the one
that failed.

## Review round

A full review of the branch (three finder lenses, findings adversarially
verified) landed one more commit:

- **The wrapper-suite PTY children never got the floor.**
`configure_pty_command` env-clears, skips the `Cmd` latch, and sets the
real `HOME`, and the shell-wrapper call sites layer only fixture paths
and an identity on top. Every git those ~89 tests ran therefore read the
developer's real `~/.gitconfig` (and lost the gpgsign shield when
`LOCAL_TEST_CONFIG` dropped it). The floor now rides that choke point,
pinned by `configure_pty_command_carries_the_git_config_floor`; every
raw `CommandBuilder` site was checked to route through it.
- **`task profile-tests` could never report CPU.** mvdan/sh's `time`
hardcodes `user`/`sys` to `0m0.000s`, so the numbers the docs said to
track were unproducible; now `bash -c 'time "$@"' bash ...`. Measured
post-fix: user 5m47s / sys 12m8s, a 67.7% kernel share, confirming the
two-thirds claim above; the integration mean measured ~1s and the docs
were corrected from ~0.6s.
- Smaller: two raw `Command::new("git")` asserts in `remove.rs` tests
now go through `configure_git_cmd`; the hermetic meta-test keys on
`--show-scope` scopes rather than git's origin-path spelling; the dead
`template-repo` fixture is deleted; three test `git init`s name `-b
main`; doc corrections (sun_path arithmetic, recover-walk attribution,
dead `[TEST_GIT_CONFIG]` remnants, volatile counts).

Final gate on the finished tree: green, 4,612 tests. Deferred with
rationale: ~985 snapshots carry stale env-block metadata (insta never
compares it; it churns on future re-records), `$TMPDIR/wt` is not
per-user on Linux (a root run poisons it for later users), and
`spawn_detached_exec_*` / `step tether` do not hand-apply the floor (no
in-process test reaches them today).

> _This was written by Claude Code on behalf of Maximilian_

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 21:20:26 -07:00
Maximilian Roos a285ed308d refactor(tests): bring the mock-stub env vars under the WORKTRUNK_TEST_ prefix (#3621)
`MOCK_CONFIG_DIR` and `MOCK_CALL_LOG_DIR` were the only two
worktrunk-invented environment variables without the `WORKTRUNK_`
prefix. That is not just a naming inconsistency:
`isolate_subprocess_env` scrubs the parent environment by prefix —
`GIT_*` and `WORKTRUNK_*` — so an unprefixed name is the one thing a
test child inherits from whoever ran the suite. Renaming them brings
them under that scrub.

- `MOCK_CONFIG_DIR` → `WORKTRUNK_TEST_MOCK_CONFIG_DIR`
- `MOCK_CALL_LOG_DIR` → `WORKTRUNK_TEST_MOCK_CALL_LOG_DIR`

`TEST` rather than a bare `WORKTRUNK_` because both are read only by
`tests/helpers/mock-stub` — they are the protocol between the harness
and its helper binary, and `wt` itself never reads either one. That
matches the ~15 existing `WORKTRUNK_TEST_*` knobs.

## The snapshot half

Mechanical but not a substitution: the `env:` block is byte-sorted by
key, so the renamed entry moves position within it and a `sed` in place
would leave it where the old name sorted. All 971 affected blocks were
rewritten by dropping the old line, inserting the new one, and
re-sorting — the aggregate diff is exactly one removed and one added
line per file:

```
971 files changed, 971 insertions(+), 971 deletions(-)
-    MOCK_CONFIG_DIR: "[MOCK_CONFIG_DIR]"
+    WORKTRUNK_TEST_MOCK_CONFIG_DIR: "[TEST_MOCK_CONFIG]"
```

The redaction placeholder follows its neighbours' convention in
`add_standard_env_redactions` (`WORKTRUNK_TEST_NU_VENDOR_AUTOLOAD_DIR` →
`[TEST_NU_VENDOR_AUTOLOAD]`), so it now reads `[TEST_MOCK_CONFIG]`
rather than repeating the full key. `MOCK_CALL_LOG_DIR` appears in no
snapshot — its two call sites are `.output()` assertion tests — so it
needs no redaction.

## Keeping it from regressing

`tests/CLAUDE.md` gains the rule under "Where a new environment variable
goes": name it `WORKTRUNK_TEST_*`, and the rule covers the
harness↔helper-binary protocol, not just knobs `wt` itself reads.
Without it the next helper-binary variable gets named `MOCK_*` again and
the hermeticity hole reopens.

## Verification

`cargo run -- hook pre-merge --yes` passes (exit 0) on the merged tree:
4607 tests including `--features shell-integration-tests`, `pre-commit
run --all-files`, clippy, doctests. No pending snapshots. A repo-wide
sweep finds no remaining unprefixed spelling.

## Merged main

#3620 landed while this was in flight and regenerated several `for_each`
snapshots that still carried the old key, so main is merged in here. It
resolved with no conflicts, and the result is what you'd want rather
than what git happened to produce: those blocks now carry #3620's new
keys (`GIT_ALLOW_PROTOCOL`, `CLAUDE_CONFIG_DIR`,
`WORKTRUNK_TEST_PARENT_SHELL`) *and* the renamed key, each in sorted
position. The sweep and the gate above both ran after the merge.

#3620 also rewrote the "Where a new environment variable goes" section
this branch adds to — three layers became four. Both edits survived; the
new naming paragraph follows the updated layer list.

The two advisory `affected tests` checks are red for the same reason,
and merging clears them: `cargo affected` errors on `git diff stdout was
not valid UTF-8`, because the diff from this PR's base contains 16
binary files — the `tests/fixtures/standard/` git objects and index
files that #3620 deleted. This branch's own commit contributes none. A
PR based after #3620 won't see them.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

> _This was written by Claude Code on behalf of Maximilian Roos_

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:44:57 -07:00
Maximilian Roos a0c1b662d2 perf(fsmonitor): resolve all daemons in one lsof spawn (#3581)
## Why

`wt remove`'s end-of-command fsmonitor sweep forked one `lsof` per
*machine-wide* `git fsmonitor--daemon` — and with `core.fsmonitor`
enabled globally, a machine accumulates one daemon per repo ever touched
(100+ is routine). So every `wt remove` paid ~100 process spawns. The
cost is superlinear under load, not linear: a fork storm makes macOS
Gatekeeper/XProtect assess each new image under contention, inflating
per-spawn cost for everything else on the box, sweeps included. The
`~50ms each` figure in the old docstring was measured *under that load*
and read as a fixed per-call cost — so the sweep looked merely slow
rather than self-amplifying. Two concurrent test suites driving hundreds
of `wt remove` calls pinned all 18 cores (load average 142) with nothing
hot in `htop`.

This was diagnosed and measured before (#2814 added the sweep; #3401
wrote the cost into the docstring and explicitly changed no behavior)
but never fixed.

## The fix

Resolve every daemon in a single `lsof -a -p <pid1,pid2,…> -U -F pn`
call and split its output on the `p<pid>` record boundary, handing each
PID exactly the output the per-PID call produced — so classification and
the whole data-safety contract in the module docs are unchanged.
Verified end-to-end against a live 108-daemon machine with a PATH shim
counting invocations: **108 spawns → 1**.

Two details the batched path must preserve, each covered by a new test:

- **Don't gate on `lsof`'s exit status.** It exits non-zero when *any*
requested PID vanished mid-scan, while still printing full records for
every survivor. Gating would drop the whole sweep whenever one daemon
happened to exit — the common case on a busy machine.
- **A PID with no record is dropped**, never synthesized as a
socket-less entry. A socket-less entry reads as orphan class 1 and gets
SIGTERM/SIGKILL — on a possibly-recycled PID.

## Also here

- **Test consolidation.** Three integration fixtures built their branch
set by spawning git per loop iteration. A new
`TestRepo::create_branches` creates them in one `git update-ref
--stdin`. The over-threshold completion test drops **10.5s → 1.4s**
(~230 git spawns → 5); `switch_picker` and `statusline` branch loops get
the same treatment. `mock-stub` gains an opt-in `MOCK_CALL_LOG_DIR` call
log so a test can assert *spawn count*, not just response — deliberately
outside the repo under test, since a log written inside the tree would
dirty it mid-`wt merge`.
- **Docs.** Dropped the "disable `core.fsmonitor` globally" workaround
from the troubleshooting docs (canonical `skills/`, mirror regenerated).
It stays enabled by choice, and the daemons were never the actual
fseventsd CPU driver.

- **CI hardening.** pre-commit.ci's `typos` hook maps the token
`pn`→`on` and had auto-rewritten `lsof -F pn` to the broken `lsof -F on`
(offset+name — no `p<pid>` records, so the parser reaps nothing). Pinned
`pn` in `.typos.toml` next to the existing `PN`-from-`PNGs` entry that
documents the same mapping, and added a test covering the empty-PID-set
early return (flagged by `codecov/patch`).

## Testing

New unit tests cover `daemons_from_batched_lsof` against real captured
`lsof -F pn` output, a vanished-PID gap, and empty output; the rewritten
integration test asserts exactly one `lsof` spawn with a comma-joined
PID list. Full integration suite green (1960 passed), clippy and fmt
clean.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-24 17:27:36 -07:00
Maximilian Roos c1c76b150d refactor(trace): decouple human trace.log from machine trace.jsonl (#3297)
At `-vv`, worktrunk's `trace.log` carried machine-parseable `[wt-trace]
ts=… tid=… seq=… cmd="git status" dur_us=12300 ok=true` lines
*alongside* a `$ git status [ctx]` start echo — so every command
appeared twice in two inconsistent renderings, and the human file was
cluttered with `key=value` machine fields. The root cause: `trace.log`
was doing double duty as both the human artifact and the machine-parsed
source, even though `trace.jsonl` (added in #3232) already carries the
same fields losslessly and nothing read it as input.

This decouples the two. `trace.log` + stderr become purely human;
`trace.jsonl` becomes the sole machine format.

```text
# BEFORE — two inconsistent renderings of one command
[h] $ git rev-parse --show-toplevel [worktrunk.rev-parse-dedup]
[h] [wt-trace] ts=11593 tid=8 seq=5 context=worktrunk.rev-parse-dedup cmd="git rev-parse --show-toplevel" dur_us=6101 ok=true

# AFTER — start ($) and finish (✓) pair, one `cmd [ctx]` rendering, no clutter
[h] $ git rev-parse --show-toplevel [worktrunk.rev-parse-dedup]
[h] ✓ git rev-parse --show-toplevel [worktrunk.rev-parse-dedup]  6.1ms
```

Commands render `$ …` (start) → `✓`/`✗ … dur` (finish), in-process spans
`◷ name dur`, milestones `· event` — the leading glyph names the line
type at a glance.

## What a reviewer needs

- **`src/trace/parse.rs`** — rewritten to parse `trace.jsonl` (one JSON
object per line) via a `kind`-dispatch
(`cmd_completed`/`cmd_errored`/`instant`/`span`), skipping non-records
(`{"message":…}` logs, `$ cmd` echoes, non-JSON). Cleaner than inferring
kind from which fields are present.
- **`src/logging.rs`** — `format_wt_trace` now emits the human line;
`style_stderr_line` bolds the command for `$`/`✓`/`✗` lines so a start
and its finish read as a pair. The machine fields (`ts`/`tid`/`seq`)
live only in `trace.jsonl` (via the separate JSON visitor).
- **Consumers repointed at `trace.jsonl`**: `wt config state logs
profile`, the `wt-perf` helper (`timeline` now runs `wt -vv` and reads
the file, located via `git rev-parse --git-common-dir`), and the
`diagnostic.md` profile section (while still inlining the human
`trace.log`).
- **Docs** — help text, the FAQ file inventory (added `trace.jsonl`),
and the `[wt-trace]`-grammar descriptions across
`emit.rs`/`log_files.rs`/`benches/CLAUDE.md` updated.

One behavior note: `wt-perf timeline` now writes trace files to the
repo's `.git/wt/logs/` (a side effect of `-vv`) where the old
`RUST_LOG=debug` path didn't — expected for a `-vv`-based perf helper.

## Testing

Well-covered: parser (14 unit tests), renderer + stderr styling (8),
`logs profile`/`diagnostic`/`switch`/`completion` integration tests
migrated to JSON fixtures, plus a `wt_target_dir` unit test for the
wt-perf `-C` resolution. Verified `logs profile`, `wt-perf timeline`
(text + `--chrome`), and `cache-check` end-to-end on a real `-vv`
capture. An independent review pass found no correctness issues; its
findings (a missed doc, two wt-perf robustness fixes) are folded in.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:59:31 -07:00
Worktrunk Bot 438d33ede6 fix(shell): resolve binary in fish package completions to avoid wrapper recursion (#3241) 2026-06-25 16:51:01 +00:00
Zexin Yuan 22484860e8 feat(completion): mirror wrapped built-in for alias arg completion (#3172)
An alias that forwards `{{ args }}` to a wt command — e.g. `co = "wt
switch {{ args }}"` or `cm = "wt step commit {{ args }}"` — now inherits
that command's argument and flag completion instead of the generic
`--dry-run`/`--yes`/`--var` stub. Alias name completion already worked;
this adds argument and flag completion.

Detection: the single command that forwards `{{ args }}` (via
`template_references_var` — minijinja, not a substring, and scoped per
command rather than the cross-command union), a leading `wt`, then a
tree-walk `find_subcommand` that lands on a leaf subcommand. The leaf
`Command` is cloned and renamed to the alias (clap `Command`/`Arg`/
`Extensions` and `ArgValueCompleter` are all `Clone`; the completer is
`Arc`-backed, so the live git query survives the clone). Bare
dispatchers (`wt step {{ args }}`), multiple `{{ args }}` forwarders,
and aliases that do not forward args fall back to the generic stub.

Mirroring runs at the top level only; `wt step <alias>` keeps the
generic stub.
2026-06-23 09:38:50 -07:00
Maximilian Roos 3d0b99e072 Add WORKTRUNK_VERBOSE env var equivalent to -v/-vv (#3166)
Shell tab-completion runs the `wt` binary as its own subprocess (the
shell sets `COMPLETE=<shell>`), and that path returns from `parse_cli`
before `main` ever reaches `logging::init`. So when a tab-completion is
slow, there's no flag that turns on logging for it — `-v`/`-vv` never
run, and `RUST_LOG` only sets a level, not the `-vv` file sinks. There
was no way to profile a slow completion.

This adds `WORKTRUNK_VERBOSE=0|1|2` as the env-var equivalent of the
`-v`/`-vv` flag count. It's read everywhere — including the completion
path, which no flag can reach — and combined with the flag via `max`, so
the env sets a baseline the flag can raise but never lower. Completion
behaves *identically* to a flagged command at the same level: at level 2
it writes the same `trace.log`/`subprocess.log`/`diagnostic.md` under
`.git/wt/logs/`, so a slow tab-completion can be profiled with:

```console
$ WORKTRUNK_VERBOSE=2 COMPLETE=fish wt -- wt switch ''
```

then reading `trace.log`. (Set it inline like that, or as a one-off,
rather than `export`-ing it — an exported value makes *every* TAB run as
`-vv`, printing the "Writing to…" banner above your prompt and
re-truncating the shared trace files on each keystroke. That's just
normal `-vv` shared-sink behavior, but it's noisy interactively.)

The one place completion deliberately diverges: it strips
`WORKTRUNK_VERBOSE` from the environment of any forwarded `wt-*`
custom-subcommand child, so the child doesn't re-run `logging::init` and
clobber the trace files the parent completion just wrote (its stderr is
discarded anyway).

### Testing

Integration tests cover: `WORKTRUNK_VERBOSE=2` opens the trace files
like `-vv` while `=1` does not; flag `-vv` combined with env `0` still
writes (the `max`); and completion at level 2 writes `[wt-trace]`/`$
git` records to `trace.log` while candidates still go to stdout. A unit
test pins the lossy parse (empty/garbage/out-of-range → `0`, never an
error) so a stray value can't corrupt the completion candidate list.
Docs (faq, config, the env-var table) and help snapshots are synced.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:46:06 -07:00
Ben Drucker ad43df7656 feat(completions): ship dynamic registration for package installs (#3105) 2026-06-18 01:36:59 -07:00
Worktrunk Bot d824d30f3d fix(completion): surface unborn worktree branches (#3097) 2026-06-17 23:46:56 -07:00
Worktrunk Bot ec62580c29 revert(hooks): keep docs on pre-start/post-start; code accepts both (#2857)
Per @max-sixty's [direction in
#2838](https://github.com/max-sixty/worktrunk/issues/2838#issuecomment-4509447593):
revert the docs portion of #2840 and keep the code. Docs continue to
recommend `pre-start`/`post-start`; both names work in code so anyone
who already followed the briefly-changed docs (e.g. @EcksDy) isn't
stranded once a release ships these aliases.

## User-visible — back to `pre-start`/`post-start`

- README, docs site, skill mirrors, `dev/*.example.toml`,
`plugins/worktrunk/README.md`, `flake.nix`, `.config/wt.toml`
- `src/cli/mod.rs` / `src/cli/config.rs` / `src/cli/step.rs` /
`src/help.rs` after_long_help and example snippets — and the auto-synced
`docs/content/` and `skills/worktrunk/reference/` mirrors
- `wt hook --help` canonical subcommand names; completion advertises
`-start` only
- `HookType` Display via strum, serde `rename`, and clap `ValueEnum`
name — all `pre-start`/`post-start`. The Rust variant identifiers stay
`PreCreate`/`PostCreate` (internal; we already paid for that rename in
#2840, and now the eventual flip is a Display-only change)
- `HooksConfig` serde canonical fields

## `*-create` still works (kept code)

- `wt hook pre-create` / `post-create` — CLI alias on the canonical
subcommand
- `pre-create` / `post-create` in config: top-level, `[hooks.*]`, and
per-project, in string, `[table]`, and `[[array-of-tables]]` form.
Mechanism: serde `alias = ...` on the field, plus a silent in-memory
rename in `migrate_content()` so the round-trip in `unknown_tree`
doesn't flag table forms as schema-unknown.
- The pre-0.32.0 `post-create` fatal-load-error machinery stays removed
— the name is reclaimed, and both forms load without error.

## Smaller bits

- `valid_user_config_keys()` / `valid_project_config_keys()` append
`pre-create` / `post-create` so the unknown-field round-trip skips them.
`test_valid_*_keys_all_deserialize` skips both aliases (they can't sit
alongside the canonical without a duplicate-field error).
- `DEPRECATED_SECTION_KEYS` drops the `pre-start`/`post-start` entries
#2840 added — `pre-start`/`post-start` are canonical again.
- `find_pre_start_from_doc` / `find_post_start_from_doc` /
`find_renamed_hook_key` / `is_non_empty_item` /
`migrate_start_hooks_doc` and their tests are removed; the migration
direction flips via a new `migrate_create_hooks_doc` (silent, mirrors
the prior shape).
- Test files `e2e_shell_post_create.rs` and `post_create_commands.rs`
rename back to `_post_start_` (via `git mv`, so the rename shows as a
rename).

## Testing

`cargo run -- hook pre-merge --yes` — 3806 tests pass; the 10 failures
are all `case_4` of `shell_wrapper::unix_tests::*` (nu-shell case; `nu`
isn't installed in this runner; same failures occur on `main`).

Also manually verified that a fresh `wt switch --create` against a
project config with `[post-create]` loads cleanly with no unknown-field
warning and the hook fires as `post-start`.

## Follow-up

Per @max-sixty: in a couple of weeks, once a release with
both-names-work is out and users have had a chance to upgrade, the docs
flip is straightforward (most of it is in `src/cli/mod.rs`'s
`after_long_help` and the doc-sync test propagates).

Re #2838.

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:03:03 +00:00
Maximilian Roos d7e3f88422 feat(hooks): rename worktree-creation hooks to pre-create/post-create (#2840)
Phase 1 of the staged hook rename tracked in #2838: the worktree-creation hooks `pre-start`/`post-start` become `pre-create`/`post-create`. The old names keep working with no deprecation warning yet (Phase 2, months out, adds the warning).

## What changes

- `pre-create`/`post-create` are canonical everywhere: the `HookType` enum, the `HooksConfig` serde fields, the `wt hook` CLI, completion, and all docs.
- Old names keep working: `migrate_content()` rewrites `pre-start`/`post-start` config keys to `-create` before serde, and `parse_hook_type` accepts the old CLI names as silent aliases. `wt config update` rewrites them on disk; `wt config show` shows the migration diff. `wt hook <type>` execution and `wt hook show` both accept the old names; completion and `--help` advertise only the canonical names.
- `detect_deprecations()` flags the old keys so `update`/`show` act on them, but `format_deprecation_warnings()` stays silent (Phase 2 adds the warning). A new empty-warnings guard in `check_and_migrate` keeps a `-start`-only config from emitting a stray hint.
- The dead pre-0.32.0 `post-create` machinery is removed: the fatal `POST_CREATE_REMOVED_MSG` load error, the vestigial `HooksConfig.post_create` merge-fold, and `find_post_create_from_doc`. `post-create` is reclaimed as the canonical background creation hook.

## Semantic flip

Before v0.32.0, the key `post-create` named a *blocking* hook. It now names the *background* one.

Since v0.44.0, a pre-0.32.0 `post-create` config is a fatal load error on the `check_and_migrate` paths: `ProjectConfig::load` and user/system config loading, which fire on essentially every `wt` command. A repo carrying one has been unusable ever since. The one path that skips that check is `project_config_at_ref` (the base-ref read behind `wt switch --create`), which applies only structural migration. A pre-0.32.0 `post-create` surviving solely on a base ref, never checked out into a worktree, would now load as a background hook rather than folding into the blocking `pre-start`. That edge case is accepted: once `post-create` is valid again, reclaiming the name and detecting the dead key are mutually exclusive.

## Reviewing this diff

205 files, but the substance is ~36 files under `src/`. The rest is regenerated snapshots and auto-synced doc mirrors. Start with:

- `src/config/deprecation.rs` — detection (`find_renamed_hook_key`), migration (`rename_hook_key`), removal of the fatal block, the empty-warnings guard, and the `DEPRECATED_SECTION_KEYS` entries that stop unknown-field detection from flagging the migrated keys.
- `src/config/hooks.rs`, `src/git/mod.rs` — the serde field and enum renames.
- `src/config/project.rs` — `ProjectConfig::load` deserializes `check_and_migrate`'s migrated content, so a current-worktree config using the old keys loads into the canonical fields.
- `src/cli/hook.rs`, `src/commands/hook_commands.rs`, `src/completion.rs`, `src/main.rs` — the CLI alias layer; `wt hook show` accepts the old type names as hidden value-parser aliases.
- `src/cli/mod.rs` — the `wt hook` docs, including the soft-deprecation note linking #2838.

The ~93 modified snapshots also pick up deterministic env-block lines (`GIT_*: ""`, `LLVM_PROFILE_FILE`) that pre-existing snapshots already carry. That is stale-snapshot drift surfaced by the regeneration, not a behavior change.

## Testing

Full suite green (3799 tests). New coverage: `snapshot_migrate_start_to_create` (migration preserves value shape and position), `test_deprecated_start_hook_key_runs_silently` and `test_standalone_hook_start_alias_runs_silently` (old config and CLI names run with no warning), `test_config_show_displays_start_hook_migration` (`config show` reveals the diff without an "unknown field" warning), and `test_hook_show_accepts_deprecated_start_hooks` (a current-worktree config using the old keys loads, and `wt hook show` takes both the canonical and the deprecated type arguments).

Part of #2838.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-20 19:31:50 -07:00
Worktrunk Bot 6a6097223b test: delete tautological spacing_test, drop orphan completion comment (#2829) 2026-05-20 07:58:57 +00:00
Maximilian Roos 408f4f5bee Add wt step tether: kill a command's process group when its worktree is removed
`wt step tether -- CMD…` runs CMD in its own process group and tears the whole
group down when CMD exits or its worktree is removed (a 250ms portable poll;
killpg on Unix, taskkill /T /F on Windows). Replaces the leaked-dev-server /
fseventsd-saturation failure mode with a fire-and-forget supervisor needing
only a single post-start hook. No unsafe, no new deps. Shell handling matches
`wt step for-each`. Windows taskkill has a documented self-exit-detach edge.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 11:57:16 -07:00
Maximilian Roos c7b94a7276 feat(config): add wt config alias show and dry-run subcommands (#2291)
## Summary

Third PR in the series simplifying alias-control flags. Introduces `wt
config alias` as the home for alias introspection and removes the
`--dry-run` flag that lived on alias dispatch.

- **`wt config alias show <name>`** — prints the configured template,
source-labeled (user/project). Each step in a pipeline is rendered in a
single gutter block with `# <step-name>` comment lines above named
steps.
- **`wt config alias dry-run <name> [-- args...]`** — parses the
arguments with the same parser `wt <alias>` uses, renders templates
against the execution-time context, and prints without running. Args
after `--` are forwarded verbatim, so `wt config alias dry-run s --
target-branch` previews exactly what `wt s target-branch` would run.
Same layout as `show`; only the header verb differs (`:` vs ` would
run:`).
- **`--dry-run` removed** from `AliasOptions` — both `wt <alias>
--dry-run` and `wt step <alias> --dry-run` now return an actionable
error pointing at the new subcommand. No deprecation period: top-level
alias dispatch is recent, so cutover is acceptable.
- **Tab-completion** completes alias names under `show` / `dry-run`.

## Output format

```
○ Alias deploy (project) would run:
 ┃ # install
 ┃ npm install
 ┃ # build
 ┃ npm run build
```

The `○ Alias <name> (<source>)` header matches the existing
`info_message` style. Bolding the name (what the user typed) and tagging
the source in parens keeps the invocation identifier visually primary.
When both user and project define the same alias, both entries print
back-to-back (user first, matching runtime execution order).

## Design notes

- The new subcommand reuses `AliasOptions::parse` as the source of truth
for invocation parsing, so preview stays aligned with runtime. When user
and project configs both define the alias, `referenced_vars` is unioned
across entries so a flag binds if any template references it.
- Templates referencing `vars.*` are shown unexpanded, mirroring the
lazy execution path — those values are read from git config just before
each step runs, potentially after earlier steps have set them. Syntax
errors still surface up front.
- `wt step <alias>` also breaks since both paths share
`AliasOptions::parse`. That's fine — `wt step <alias>` has no
forward-compat promise beyond the basic call.
- `AliasSource` is promoted to `pub(crate)` with a `label()` helper so
`wt config alias` and the alias runtime code read from one enum.

## Navigating the diff

- `src/cli/config.rs` — adds `ConfigAliasCommand` + the `Alias` variant
on `ConfigCommand`.
- `src/commands/config/alias.rs` — new module. `handle_alias_show` and
`handle_alias_dry_run` share a `format_entry` helper; the `verb:
Option<&str>` parameter is the only difference between show and dry-run
output. `render_preview` replaces the old `render_for_dry_run` in alias
dispatch.
- `src/commands/alias.rs` — the `--dry-run` flag and its rendering
branch are gone. The parser bails on `--dry-run` with a migration
message pointing at the new subcommand; the bail sits after the `--`
literal-mode check, so `wt alias -- --dry-run` still forwards as
positional.
- `src/completion.rs` — `alias_name_completer()` mirrors the existing
`hook_command_name_completer` pattern.
- `tests/integration_tests/step_alias.rs` — existing `--dry-run` tests
migrate to `wt config alias dry-run`, plus new coverage for `show`,
unknown-alias suggestions, and the retired-flag error.
- `docs/content/extending.md` — adds an "Inspecting and previewing"
subsection.

## Test plan

- [x] Full suite via `wt hook pre-merge --yes` (3271 tests, all passing)
- [x] `cargo insta test --accept` — all snapshots up to date
- [x] `cargo test --test integration
test_command_pages_and_skill_files_are_in_sync` — docs synced
- [x] Manual verification: `wt config alias show <name>` and `wt config
alias dry-run <name> [-- args...]` in a repo with mixed user/project
aliases

> _This was written by Claude Code on behalf of Maximilian_

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:00:01 -07:00
Maximilian Roos 64e1b4efb9 feat(cli): move approvals subcommand from hook to config (#2282) 2026-04-18 00:46:27 -07:00
Maximilian Roos 016eb030e7 docs(extending): rename "external subcommand" to "custom subcommand" (#2270)
## Summary

Renames the git-style `wt-<name>` dispatch feature to "custom
subcommand" across user-facing docs and internal code. Two motivations:

- **Avoid overloading "external."** The codebase already uses "external
command" for the `shell_exec` concept (subprocesses like `git` and
`gh`). Using the same word for the `wt-foo` dispatch feature is
ambiguous.
- **Cargo uses "custom command" / "external subcommand"
interchangeably.** kubectl calls theirs "plugins," gh calls theirs
"extensions"; git doesn't have a settled term. "Custom subcommand" reads
naturally in prose and matches cargo's user-facing phrasing.

## Changes

**User-facing docs** — `docs/content/extending.md` (section heading,
description, comparison table) and `docs/content/faq.md` (link +
anchor). Skill references auto-sync.

**Internal code** — `src/commands/external.rs` → `custom.rs`,
`Commands::External` → `Commands::Custom`, `handle_external_command` →
`handle_custom_command`, plus matching renames in `src/completion.rs`
(inject/discover/forward functions) and corresponding tests. Also
renames `tests/integration_tests/external.rs` → `custom.rs`.

**Kept intact** — clap's `#[command(external_subcommand)]` attribute and
`.allow_external_subcommands(true)` are clap's own vocabulary, not ours.

CHANGELOG is historical and left unchanged.

## Test plan

- [x] `cargo build` clean
- [x] `cargo clippy --all-targets --all-features` clean
- [x] `cargo test --lib --bins` — all pass
- [x] `cargo test --test integration` — all pass (1476)
- [x] `pre-commit run --all-files` clean

> _This was written by Claude Code on behalf of Maximilian_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-16 22:06:12 -07:00
Maximilian Roos 37fb27bc97 Deprecate table form for pre-* hooks (#2135)
## Summary

Multi-entry table form for pre-* hooks currently runs serially, while
the same form for post-* hooks runs concurrently. The parser produces
`HookStep::Concurrent` either way, but the foreground executor flattens
steps and runs them serially. To unify the semantics — table form will
run concurrently for all hook types in a future version — this
deprecates the current form for pre-* hooks and auto-migrates it to
pipeline form, which is explicitly serial.

## Implementation

Follows the existing deprecation recipe in `src/config/deprecation.rs`:
- **Detection** and **migration** for top-level hooks (user/project
config) and per-project overrides (`[projects."id".pre-*]`).
- **Warning**: matches the terse `{old} → {new}` pattern of existing
deprecations (`[merge] no-ff → ff`, `post-create → pre-start`, etc.).
- **Auto-migration** at load time: table form rewrites to pipeline of
inline tables so current behavior (serial) is preserved until users run
`wt config update`.

## Docs

Replaces the transitional "concurrent for post-*, sequential for pre-*"
framing with a neutral three-form description (string / table /
pipeline), plus a note recommending pipeline form for pre-* hooks to
avoid the upcoming behavior change.

## Tests

- `snapshot_migrate_pre_hook_table_form` — TOML migration diff
- `test_config_show_displays_pre_hook_table_form_deprecation` — full
user-facing `wt config show` output, covering the "Project config" label
and multi-hook list form
- Unit tests for detection/migration of top-level and per-project
variants
- Existing integration test fixtures migrated to canonical pipeline form

> _This was written by Claude Code on behalf of max-sixty_

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-12 15:55:28 -07:00
Pablo Speciale de9bc631d6 Forward shell completions to external subcommands (wt-*) (#2074) 2026-04-12 08:44:33 -07:00
Maximilian Roos a2482f1da9 refactor: change test git_command() to return Cmd instead of Command (#1718)
The test infrastructure's `git_command()` returned
`std::process::Command`, so test git commands bypassed `Cmd`'s debug
logging (`$ git status [ctx]`) and timing traces (`[wt-trace]`). This
changes it to return `worktrunk::shell_exec::Cmd`.

## Approach

Added `configure_git_env(Cmd, &Path) -> Cmd` alongside the existing
`configure_git_cmd(&mut Command)`. The `Command` version is kept because
`configure_wt_cmd` (which configures wt binary commands) still needs it
— wt commands go through `Command`, not `Cmd`.

Key changes in `tests/common/mod.rs`:
- `TestRepoBase::git_command()` and `TestRepo::git_command()` now return
`Cmd`
- All wrapper methods (`run_git`, `git_output`, `head_sha`, etc.) use
`.run()` instead of `.output()`
- `BareRepoTest::new()` and `NestedBareRepoTest::new()` use
`Cmd::new("git")` for init

247 call sites across 25 files converted: `.output()` → `.run()` and
`.status()` → `.run()` on git command chains. Zero `Command::new("git")`
remaining in the test directory.

Follows #1714 and #1716 which converted raw `Cmd::new("git")` and
`Command::new("git")` in test bodies to `repo.run_command()`.

> _This was written by Claude Code on behalf of maximilian_

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 00:23:23 -07:00
Maximilian Roos efad9312db feat!: rationalize hooks — rename post-create, add post-commit, background post-merge (#1679)
Implements the hook rationalization from #1670, establishing a symmetric
`pre-` (blocking) / `post-` (background) pattern for every lifecycle
event.

## Changes

**Rename `post-create` → `pre-start`**: The old name suggested it ran
*after* creation, but it actually runs *before* `post-start` as a
blocking dependency step. Both names accepted for one release cycle —
config deprecation detection, migration file generation, `wt config
update` support, and CLI alias all in place. `merge_with` folds old-name
hooks into new-name so cross-config combinations don't silently drop
hooks.

**Add `post-commit` hook**: New background hook firing after successful
commits (including squash commits), completing the commit lifecycle
pair. Included in approval batches for `wt step commit`, `wt step
squash`, and `wt merge`.

**Change `post-merge` to background**: Was blocking with `Warn`
strategy, now runs in background like all other `post-` hooks.
`--foreground` flag available for debugging.

The hook table is now a clean symmetric grid:

| Event | `pre-` (blocking) | `post-` (background) |
|-------|-------------------|---------------------|
| start | `pre-start` | `post-start` |
| switch | `pre-switch` | `post-switch` |
| commit | `pre-commit` | `post-commit` |
| merge | `pre-merge` | `post-merge` |
| remove | `pre-remove` | `post-remove` |

## Testing

868 lib + 483 bin + 1227 integration tests pass, all 13 lint checks
pass. The deprecation has 23 dedicated unit tests covering detection at
all three config scopes, migration, empty table filtering, cross-config
merge safety, and integration with `wt config show` / `wt config
update`.

Closes #1670

> _This was written by Claude Code on behalf of max-sixty_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-23 00:33:30 -07:00
Maximilian Roos e43032ff5b feat: add shell completion for step aliases (#1641)
Aliases configured in user/project config now appear as completable
subcommands when tab-completing `wt step <TAB>`. Previously, only
built-in step commands (commit, push, rebase, etc.) appeared since
aliases are dynamic — they're resolved at runtime via the
`external_subcommand` catch-all, which clap's completion engine doesn't
know about.

The fix injects alias names as synthetic clap subcommands in the
completion-only command tree. Each alias subcommand includes
`--dry-run`, `--yes`/`-y`, and `--var` flags matching the real alias
flag parser. Aliases that shadow built-in step commands are skipped.
Shells that support it (fish, zsh) show the alias template as help text.

> _This was written by Claude Code on behalf of @max-sixty_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-21 13:16:10 -07:00
Maximilian Roos e6c5067291 fix: remove prefix filtering from HookCommandCompleter for shell consistency (#1627)
`HookCommandCompleter::complete()` was pre-filtering completions by
prefix, preventing fish/zsh users from doing substring matching on hook
command names. `BranchCompleter` was already fixed for this in #1471,
but `HookCommandCompleter` was missed.

The bash-specific prefix filter in `maybe_handle_env_completion()`
already handles bash correctly — this just removes the redundant (and
harmful) filter from the completer itself, matching `BranchCompleter`'s
approach.

Also inlines `complete_hook_commands()` (single call site) and fixes a
stale docstring (`wt step` → `wt hook`).

> _This was written by Claude Code on behalf of @max-sixty_

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 15:58:13 -07:00
worktrunk-bot 76d8866334 fix: filter completions by prefix for bash shell (#1622)
Bash's programmable completion (`complete -F`) does not filter
`COMPREPLY` by prefix — whatever the completion function puts in the
array is displayed as-is. After #1471 removed prefix filtering to enable
fish/zsh substring matching, bash users saw all branches regardless of
what they'd typed. For users with many branches (the reporter had 80),
`wt switch feat<TAB>` prompts "Display all 80 possibilities?" instead of
showing only the `feature/*` branches.

When the requesting shell is bash (`COMPLETE=bash`), filter completion
candidates by prefix before outputting them. Fish and zsh still receive
all candidates so they can apply their native substring/fuzzy matching.
The filtering runs in `maybe_handle_env_completion()` after
`engine::complete()` returns, so it handles all argument forms correctly
(`--base=m` → `--base=main`, `-b m` → `main`, etc.).

Adds `test_completion_cross_shell_filtering_contract` which verifies the
same branches with the same typed prefix produce different results per
shell — the test that would have caught the #1471 regression.

Thanks to @altruic for reporting in #1621.

Closes #1621

> _This was written by Claude Code on behalf of @max-sixty_

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
2026-03-19 12:07:26 -07:00
Maximilian Roos a7f224d263 feat: add wt step eval for template expression evaluation (#1004)
## Summary

- Add experimental `wt step eval` subcommand that evaluates a minijinja
template in the current worktree context and prints the result to stdout
- Reuses existing hook template infrastructure — all variables
(`branch`, `repo`, `worktree_path`, etc.) and filters (`hash_port`,
`sanitize`, `sanitize_db`) are available
- Output goes to stdout with no decoration or shell escaping, designed
for scripting and shell substitution

```
$ wt step eval '{{ branch | hash_port }}'
16066

$ curl http://localhost:$(wt step eval '{{ branch | hash_port }}')/health
```

This is the first step from the
[IDEAS.md](https://github.com/max-sixty/worktrunk/blob/arbitrary-data/IDEAS.md)
design doc, directly addressing the user request in #947 — "found no way
to run it on a single branch."

## Test plan

- [x] 6 integration tests: basic branch, hash_port, multiple values,
sanitize_db, conditionals, template error
- [x] Help text and completion snapshots updated
- [x] stdout guard allowlisted
- [x] Doc pages auto-synced
- [x] All lints pass
- [x] 2360 tests pass (5 pre-existing shell_wrapper failures excluded)

Ref #947

> _This was written by Claude Code on behalf of @max-sixty_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: worktrunk-bot <w@worktrunk.dev>
2026-03-16 05:55:23 +00:00
worktrunk-bot b527e42a31 fix(completion): skip prefix filtering to enable shell substring matching (#1471)
## Summary

- Stop prefix-filtering branch candidates in
`BranchCompleter::complete()` — return all surviving candidates (after
the >100 remote exclusion from #1442) and let the shell apply its own
matching
- Enables fish substring matching (`auth<TAB>` → `feature/user-auth`)
and zsh fuzzy matching for `wt switch` completions
- Adds dedicated test for the substring-matching use case from #1468

Closes #1468

## Test plan

- [x] All 46 completion tests pass (including updated
`test_complete_with_partial_prefix_returns_all_branches` and new
`test_complete_switch_returns_candidates_for_substring_matching`)
- [x] Unit tests pass
- [x] Lints pass
- [ ] CI green across platforms

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 15:40:05 -07:00
worktrunk-bot 934059a15c fix(completion): exclude remote branches when total exceeds 100 (#1442)
## Summary

- When tab-completing `wt switch`, exclude remote-only branches if the
total branch count exceeds 100
- This prevents the "do you wish to see all N possibilities?" prompt in
bash/zsh for repos with many remote branches
- Threshold of 100 aligns with bash's default `completion-query-items`
- Repos with ≤100 branches are unaffected — remote branches still appear
in completions
- Users can always reach remote branches via the interactive picker (`wt
switch` without args)

Closes #1415

## Test plan

- [ ] Verify compilation and existing tests pass in CI
- [ ] Manual: in a repo with >100 remote branches, `wt switch <TAB>`
should show only local/worktree branches
- [ ] Manual: in a repo with ≤100 branches, `wt switch <TAB>` should
include remote branches as before

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 12:48:42 -07:00
Maximilian Roos fe56eaa335 Remove --no-background, promote --no-progressive, carry foreground through (#1381)
Removes the deprecated `--no-background` flag (replaced by
`--foreground`) from
`remove`, `hook post-start`, `hook post-switch`, and `hook post-remove`.
Removes
all supporting deprecation infrastructure: warning message,
`is_foreground_mode`
helper, `DEPRECATED_ARGS` completion filtering.

Promotes `--no-progressive` from deprecated to hidden flag with inline
mention
in `--progressive`'s help text, using `flag_pair()` for consistent
handling.

Carries `foreground: bool` through the entire call chain instead of
negating to
`background` at call sites. Refactors `print_message` to use
`success_message()`/`progress_message()` helpers instead of manual color
construction, adding `FormattedMessage::append()` for integration
symbols that
need native styling outside the message's color span.

> _This was written by Claude Code on behalf of maximilian_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-08 17:25:56 -07:00
Maximilian Roos cbd5e4da50 feat: add nushell tab completions (#1220)
## Summary

- Add `nu-complete wt` completer function to the nushell init template,
wiring it to the wrapper's rest parameter via `@"nu-complete wt"`
- The completer calls the binary with `COMPLETE=nu` and parses
tab-separated output into nushell's `{value, description}` record format
- Add `"nu"` to multi-shell completion test loops (help, version,
single-dash, deprecated flags) and a dedicated subcommand snapshot test

Thanks to @omerxx for reporting in #1215.

Closes #1215

## Limitations

Nushell's completion engine bypasses custom completers when the current
token starts with `-`, so flag completions (e.g. `wt switch --<TAB>`)
don't appear. Subcommand and value completions work correctly. This is a
nushell engine limitation (nushell/nushell#14504), not something we can
fix in our template.

## Test plan

- [x] Unit tests pass (`cargo test --lib --bins`) — snapshot updated
- [x] Integration tests pass (`cargo test --test integration`) — 1121
tests
- [x] Shell integration test (`cargo test --test integration --features
shell-integration-tests -- test_nushell_completion_subcommands`)
- [x] Lints pass (`pre-commit run --all-files`)
- [x] Manual verification in nushell 0.110.0: `wt <TAB>` shows
subcommands, `wt switch <TAB>` shows branches with descriptions

> _This was written by Claude Code on behalf of @max-sixty_

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 19:12:15 -08:00
Maximilian Roos 35c2172b74 feat: add wt step prune to remove merged worktrees (#1191) 2026-02-26 00:06:34 -08:00
Maximilian Roos bc2ee2dcb6 Add experimental wt step promote command (#789)
## Summary

Adds `wt step promote` — an experimental command to exchange branches
between the main worktree and another worktree, including swapping
gitignored files (build artifacts, `.env`, `node_modules/`, etc.). This
addresses #738 where users want to move a branch from a worktree into
the main worktree.

This is useful when the main worktree has special significance (Docker
Compose configs, IDE settings, heavy build artifacts anchored to project
root) and hooks aren't yet set up to run on arbitrary worktrees.

**Key features:**
- Exchange branches between main worktree and any other worktree
- Swap gitignored files so build artifacts follow their branch
- Auto-restore: `wt step promote` from main worktree (no args) restores
default branch
- Shows mismatch state in `wt list` with ⚑ indicator
- Warns when creating mismatch, with hint to restore
- `.worktreeinclude` filtering respected per worktree

**Swap algorithm (stage → exchange → distribute):**
1. Discover gitignored entries before branch exchange (`.gitignore`
belongs to the branch)
2. Stage files to `.git/wt-promote-staging/` before `git switch`
(prevents tracked file collision)
3. Exchange branches with per-step rollback
4. Cross-distribute staged files to new worktrees
5. Best-effort restore on failure with recovery guidance

**Adversarial testing (4 rounds, 9 issues found and fixed):**
- Stage before switch to prevent tracked file collision
- Per-step rollback in `exchange_branches()`
- Bail on leftover staging (don't destroy previous run's files)
- Recovery guidance on all failure paths
- Dedicated `restore_staged()` for rollback semantics
- `ErrorKind::CrossesDevices` for cross-device fallback
- `Option<PathBuf>` instead of sentinel empty path

## Test plan

- [x] 25 integration tests covering: bidirectional swap, round-trip,
`.worktreeinclude` filtering, nested directories,
same-filename-different-content, tracked-vs-ignored separation, stale
staging detection, bare repo rejection, detached HEAD, dirty worktrees,
no-arg from main/linked, branch argument, already-in-main, mismatch
indicator in `wt list`
- [x] All 2576 tests pass
- [x] All lints clean
- [x] `copy-ignored` tests still pass after
`list_and_filter_ignored_entries` extraction

🤖 Generated with [Claude Code](https://claude.com/claude-code)

> _This was written by Claude Code on behalf of max-sixty_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-21 10:44:38 -08:00
Maximilian Roos d9e1d53bf1 feat: add pre-switch hook (#1094)
## Summary

- Add `pre-switch` hook that runs before `wt switch` does anything —
before branch validation, planning, or worktree creation
- Runs in both the direct switch path and the interactive picker path
- Includes a fetch-if-stale example (check FETCH_HEAD age, fetch if >6
hours)
- Add orienting sentence in hook docs pointing to `post-start` as the
most common starting point

Closes #1085. Thanks to @jdb8 for reporting.

## Test plan

- [x] `test_user_pre_switch_hook_executes` — hook runs in source
worktree before switch
- [x] `test_user_pre_switch_failure_blocks_switch` — failing hook aborts
`--create`
- [x] `test_user_pre_switch_skipped_with_no_verify` — `--no-verify`
skips hook
- [x] All 1075 tests pass, all lints pass

> _This was written by Claude Code on behalf of @max-sixty_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-17 20:13:37 -08:00
Maximilian Roos 87ae623c78 feat: add wt step diff command (#1074)
## Summary

- Add `wt step diff` — shows all changes (committed, staged, unstaged,
untracked) that `wt merge` would include, in a single diff against the
merge base
- Default shows full diff with git pager; `--stat` flag for summary only
- Uses a temporary empty git index with `git add --intent-to-add` to
make untracked files visible without touching the real index

Closes #1043

## Test plan

- [x] 8 integration tests covering: no changes, committed only,
untracked, all change types combined, stat mode, stat with untracked,
explicit target, index safety
- [x] All 1048 integration + 530 unit tests pass
- [x] Pre-commit lints pass
- [x] Help snapshots and doc sync updated

🤖 Generated with [Claude Code](https://claude.com/claude-code)

> _This was written by Claude Code on behalf of @max-sixty_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-17 15:01:40 -08:00
Arnaud Limbourg c7128ce11b Add nushell support (#964) 2026-02-14 08:57:30 -08:00
Maximilian Roos c4b50bb6a8 Add wt step relocate command (#790)
* Add step relocate command

Adds `wt step relocate` to move worktrees to their expected paths based on
the `worktree-path` template. Supports dry-run preview, filtering by branch
name, and auto-committing dirty worktrees. Handles special case of main
worktree by switching to default branch and creating a new worktree.

* Add `--clobber` flag and cycle resolution to `step relocate`

Implement full swap/cycle handling and non-worktree blocker management for
relocate command. Adds `--clobber` flag to automatically backup blocking
paths, dependency graph processing to handle worktree cycles via temporary
locations, and comprehensive spec documentation.

Key changes:
- Add `--clobber` flag to back up non-worktree paths at target locations
- Implement cycle detection and resolution using temp directory
- Build dependency graph to coordinate multi-worktree moves
- Handle main worktree relocation by creating new worktree + switching
- Add test cases for swap, clobber, and cycle scenarios
- Canonicalize temp home paths on macOS for snapshot consistency

* fix: address Codex review issues in step relocate

- Refuse to clobber existing worktrees (would corrupt git metadata)
- Prefer non-main worktree when breaking cycles (git can't move main)
- Sanitize branch names for temp paths (feature/foo -> feature-foo)
- Track original path for shell cd in temp-relocated worktrees
- Show template errors as warnings instead of silently dropping

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add coverage for clobber refusing to overwrite worktrees

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add relocate --commit coverage test

Tests the commit_worktree_changes function by configuring a mock LLM
and verifying that dirty worktrees are auto-committed before relocating.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: simplify step relocate and improve coverage

- Change unreachable defensive code to expect() with explanation
- Simplify multi-line cformat! calls to single-line (improves LLVM coverage)
- Add test_relocate_mixed_success_and_skip to cover "relocated X, skipped Y" path
- Add test_relocate_dirty_with_commit to cover commit_worktree_changes function

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: remove shell cd for temp-to-final moves, simplify summary

- Remove shell directory update for temp-to-final worktree moves
  (rare case where user is inside a worktree during cycle resolution)
- Simplify summary pluralization with inline closure
- Remove unused original_path tracking from temp_relocated

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: simplify remaining cformat! calls in step_relocate

Extract multi-line cformat! macro calls to single-line variables
to improve LLVM coverage instrumentation.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: remove defensive early return in commit_worktree_changes

The early return when no staged changes was unreachable - if is_dirty()
returned true, git add -A must stage something. Removing improves coverage
and follows fail-fast principles.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: simplify step_relocate for better coverage

- Remove redundant exists() check before create_dir_all
- Extract cformat! to single-line variable in commit_worktree_changes

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: revert configure_shell cosmetic changes to reduce diff

Reverts format_path_for_display changes that were unrelated to step relocate
and were reducing coverage percentage since they're in error paths.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: harden step relocate against edge cases

Fixes identified by adversarial testing:

1. Empty default branch validation - fails early with actionable error
   instead of confusing "git checkout ''" errors

2. Same-target conflict handling - `is_target_empty` now returns
   `Option<bool>` to gracefully skip when target is unexpectedly
   blocked (TOCTOU race or same-target conflict)

3. Main worktree rollback - if `git worktree add` fails after
   checkout, restore the original branch

Adds test for same-target scenario that previously panicked.

Co-Authored-By: Claude <noreply@anthropic.com>

* Move step relocate plan to Rust module spec

Moved the comprehensive plan from .claude/plans/step-relocate-spec.md
to a Rust doc comment on the step_relocate function. This makes the
specification more discoverable and keeps it synchronized with the
implementation.

The doc comment now includes:
- Complete flag descriptions
- Detailed algorithm explanation
- Multiple scenario examples (swap, chain, mixed, clobber, main worktree)
- Implementation notes
- Test case list

Co-authored-by: Maximilian Roos <max-sixty@users.noreply.github.com>

* test: add coverage for step relocate edge cases

Add tests for:
- Template expansion error handling (invalid variable)
- Empty default branch validation (non-existent configured branch)

These cover lines 844-847 and 890-895 in step_commands.rs that were
flagged by codecov/patch.

Co-Authored-By: Claude <noreply@anthropic.com>

* Improve step relocate output messages

- Use gutter formatting for template error details (per output guidelines)
- Fix misleading summary when template errors occurred
- Show source path in temp-relocated worktree success messages
- Use bright-black styling for flags in hints (--commit, --clobber)
- Use concrete example in default branch error message

Co-Authored-By: Claude <noreply@anthropic.com>

* Use gutter for dry-run preview in step relocate

Per output guidelines: every user-facing message needs symbol or gutter.
The dry-run preview now uses gutter formatting instead of plain indentation.

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix: use wt command for setting default branch

The error message now suggests 'wt config state default-branch set main'
instead of the raw git config command.

Co-Authored-By: Claude <noreply@anthropic.com>

* Refactor commit pipeline to accept WorkingTree parameter

Extract commit staging and execution into CommitGenerator to eliminate
duplicate commit logic in step_relocate. Add show_progress parameter to
control progress message display for bulk operations. Add repo() accessor
to WorkingTree for repository access.

* refactor: extract step_relocate into modular pipeline with explicit types

Split the 500-line monolithic step_relocate function into a clean pipeline:
- gather_candidates() finds worktrees not at expected paths
- validate_candidates() checks locked/dirty, handles --commit
- RelocationExecutor encapsulates the dependency graph algorithm

New types document each stage: RelocationCandidate, ValidatedCandidate,
GatherResult, ValidationResult. The complex swap/cycle resolution logic
is now isolated in RelocationExecutor with clear methods for each phase.

Co-authored-by: Claude <noreply@anthropic.com>

* fix: update test to use correct commit.generation config format

The test was using the old `args` field which no longer exists. The
command should be a complete shell string that receives the prompt
via stdin.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Maximilian Roos <max-sixty@users.noreply.github.com>
2026-01-25 16:17:31 -08:00
Maximilian Roos 677928d008 Add post-remove hook type (#757)
* Add post-remove hook type

* Add tests for wt hook show filter by hook type

Cover lines 430-432 in hook_commands.rs for post-merge, pre-remove,
and post-remove hook type parsing in handle_hook_show().

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix post-remove hook compilation and improve pre-remove example

- Fix compilation error: replace user_hook!(post_remove) with
  ctx.config.hooks.post_remove.as_ref() to match other hook types
- Replace pre-remove "git stash" example with more practical
  "archive test results" example that demonstrates pre-remove's
  key feature: access to worktree files before deletion
- Update documentation to clarify pre-remove runs "with access
  to worktree files"

Co-authored-by: Maximilian Roos <max-sixty@users.noreply.github.com>

* Trigger CI rebuild

* Add tests for post-remove hook error paths

- Fix merge conflict: replace removed user_hook! macro with direct access
- Add test for invalid template syntax in post-remove hook
- Add test for name filter that doesn't match any hook

Remaining uncovered lines are defensive code for race conditions:
- worktree/hooks.rs:117 - error during wt remove (config validated at startup)
- handlers.rs:556 - config load failure between command start and hook execution

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: post-remove template vars reference removed worktree

Template variables ({{ branch }}, {{ worktree_path }}, {{ worktree_name }})
now correctly reference the removed worktree, not the worktree where
commands execute from. This allows cleanup scripts to identify resources
(containers, servers, etc.) associated with the removed worktree.

Changes:
- spawn_post_remove_commands now accepts removed_worktree_path parameter
- Extra vars override built-in template variables with removed worktree info
- Updated documentation to clarify this behavior
- Added test verifying template vars reference the removed worktree

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: simplify post-remove template variable description

Remove explicit list of variable names; just say "all template variables
reference the removed worktree" which is clearer and less verbose.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add post-remove hook test during wt merge

The main production use case for post-remove hooks is during `wt merge`,
which removes the worktree after merging. This test verifies that
post-remove hooks run correctly in this flow.

Found via adversarial testing of the post-remove hook implementation.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: capture commit before removal for post-remove hooks

All template variables must reference the removed worktree, not the
execution context. The commit SHA was previously computed from the main
worktree (where hooks execute from), but should reflect the removed
worktree's state.

Changes:
- Add `removed_commit` field to RemoveResult::RemovedWorktree
- Capture commit SHA before removal in prepare_worktree_removal and merge
- Pass commit through to spawn_post_remove_commands
- Add commit and short_commit to extra_vars for post-remove hooks
- Update test to verify commit template variable references removed worktree

This prevents a subtle bug where {{ commit }} would unexpectedly contain
main's HEAD instead of the feature branch's HEAD.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: use slash paths for Windows test compatibility

On Windows, hooks run in Git Bash which converts paths to MSYS2 format
(/c/Users/... instead of C:\Users\...). Use to_slash_lossy() for the
expected path comparison so the test passes on Windows.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Maximilian Roos <max-sixty@users.noreply.github.com>
2026-01-20 12:45:17 -08:00
Maximilian Roos 3193efab42 feat: search all remotes for branch existence, matching git's behavior (#744)
* feat: search all remotes for branch existence, matching git's behavior

Previously Branch::exists() and branches_for_completion() only searched
the primary remote. This was more restrictive than git's default checkout
behavior, which searches all remotes.

Now:
- Branch::exists() delegates to remotes() which searches all remotes
- branches_for_completion() lists remote branches from all remotes
- Deduplicates by branch name (most recent commit wins)

Also improves performance by using split_once() instead of
split().collect() for parsing git output.

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: show all remotes in completion when branch exists on multiple remotes

When the same branch exists on multiple remotes (e.g., origin and upstream),
git will error with "matched multiple remote tracking branches" if the user
tries to use it. Now completions show all remotes so users can see the
ambiguity upfront:

  feature    ⇣ 2d origin, upstream

Changes:
- BranchCategory::Remote now holds Vec<String> instead of String
- branches_for_completion() groups by branch name and collects all remotes
- Remotes are sorted alphabetically for deterministic display

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add test for multi-remote branch completion

Covers the `and_modify` closure path in branches_for_completion()
when the same branch exists on multiple remotes.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 11:54:24 -08:00
Rui Chen 9079046127 feat: add completion cmd for completion integration (#701)
* feat: add completion cmd for completion integration

Signed-off-by: Rui Chen <rui@chenrui.dev>

* refactor: move completions command to `wt config shell completions`

- Move from top-level `wt completions` to `wt config shell completions`
- Remove duplicate CompletionShell enum, use existing Shell enum
- Simplify help text to direct users to `wt config shell install`
- Remove Homebrew note from FAQ (no duplication issues found)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: add TODO about dynamic completions in fish

Add documentation note explaining why static completions are used
instead of dynamic completions, and reference the fish example as a
potential improvement for future implementation.

* chore: sync generated docs

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add coverage for static completions command

Tests `wt config shell completions <shell>` for all supported shells
(bash, fish, zsh, powershell) to satisfy codecov/patch coverage.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Signed-off-by: Rui Chen <rui@chenrui.dev>
Co-authored-by: Maximilian Roos <m@maxroos.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-17 22:31:16 -08:00
Maximilian Roos cd8ac3e060 perf(tests): add pre-built fixture to speed up Windows CI (#634)
* perf(tests): add pre-built fixture to speed up Windows CI

Instead of creating repos from scratch for each test, copy a pre-built
fixture with worktrees and remote already configured. This should
significantly reduce test time on Windows where process spawning is slow.

- Add tests/fixtures/standard/ with repo, 3 worktrees, and bare remote
- Update TestRepo to copy fixture instead of running git init/commit
- Add fixture cleanup to doc-generating tests for clean output
- Remove git sample hooks and boilerplate from fixture

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): correctly clean fixture state for isolated tests

- Change `remove_fixture_worktrees()` to take `&mut self` and clear the
  worktrees map so `add_worktree()` can recreate branches
- Add remote removal to select and approval tests to prevent
  `origin/main` from appearing in snapshots
- Update select snapshots for fixture-based commit hashes

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): use relative paths in fixture for portability

The fixture gitdir files contained absolute paths to the local
development machine, causing CI failures. Updated to use relative
paths which work after the _git → .git rename during fixture copy.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): remove invalid remote from bare repo fixture

The bare repository in the fixture had a remote configured with an
absolute path to the local machine, which wouldn't exist on CI.
Bare repositories don't need remotes configured.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): remove origin in approval tests for consistent project_id

Tests manually create approvals using a computed project_id based on
repo path. With the fixture's origin remote, worktrunk computes a
different project_id from the remote URL. Removing origin makes both
use the same fallback (directory path).

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): improve fixture copy robustness for CI

- Add platform-specific copy: `cp -r` on Unix, `robocopy` on Windows
- Add verification that essential paths exist after copy
- Add verification that origin.git is a valid git repository after rename
- Include stderr in error messages for better debugging

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): add .gitkeep to preserve empty git directories in fixture

Git doesn't track empty directories, so objects/info, objects/pack, and
refs/tags were missing on CI after checkout. These directories are
required for a valid git repository.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): ensure LF line endings in fixture git files

Add .gitattributes to force LF line endings for git internal files
(packed-refs, HEAD, config, etc.) to prevent CRLF corruption on Windows.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): handle file copies separately from directories on Windows

robocopy only works with directories, not files. Use Rust's std::fs::copy
for individual files like .gitattributes, and robocopy only for directories.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): enable rerere in test git config for consistent output

Git's rerere feature produces "Recorded preimage" messages during
conflicts. Enable this in test config so output is consistent between
local machines and CI.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): restore accidentally deleted hook_clear tests

The tests test_hook_clear_no_approvals and test_hook_clear_with_approvals
were accidentally removed during the fixture refactoring. This restores them
with the necessary remote removal so project_identifier matches "repo"
(directory name) instead of the fixture's remote URL.

Also adds remote removal to test_hook_show_approval_status for consistency.

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: remove unreferenced ping_pong snapshots

These snapshots used feature_a but the tests now use feature, leaving
these orphaned.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 18:48:39 -08:00
Maximilian Roos c0f48f5b4e fix(completion): exclude deprecated args from tab completions (#509)
Deprecated args like --no-background were appearing in shell completions
when typing `--<TAB>`. This happened because the completion system shows
all hidden args when completing `--` (when all candidates are hidden,
clap_complete shows them all).

Add DEPRECATED_ARGS constant listing args that should never appear in
completions, and filter them from completion output. Unlike other hidden
args (e.g., --squash which is a valid but hidden toggle), deprecated args
should remain hidden even when explicitly requesting option completions.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-09 17:55:43 -08:00
Maximilian Roos bd2a7c16e9 feat(step): add copy-ignored command for syncing .worktreeinclude files (#469)
* feat(step): add copy-ignored command for syncing .worktreeinclude files

Add `wt step copy-ignored` to copy files listed in `.worktreeinclude` that
are also gitignored between worktrees. Useful in post-create hooks to sync
local config files (.env, IDE settings, build caches) to new worktrees.

Key features:
- Uses git ls-files --ignored for accurate gitignore handling (nested
  .gitignore, global excludes, .git/info/exclude)
- Uses ignore crate for .worktreeinclude pattern matching
- COW (reflink) copying for efficient large directory handling
- --directory flag stops at directory boundaries (avoids listing
  thousands of files in target/ or node_modules/)
- Skips existing files for idempotent hook usage
- Skips symlinks and .git entries

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: update hook examples to use wt step copy-ignored

Replace manual CoW bash scripts with the new `wt step copy-ignored`
command in hook documentation. Update Rust and Node.js sections to
show `.worktreeinclude` patterns instead of complex shell commands.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add worktree-only branch completion filter

Add a new `worktree_only_completer()` function to filter branch completions
to only worktree branches, and update the `CopyIgnored` command to use it
instead of `local_branches_completer()` for the `--from` and `--to` arguments.

* Refactor hook documentation on copying files and CoW strategies

Replace the `.worktreeinclude` and `wt step copy-ignored` approach with direct
copy-on-write examples using native OS commands (`cp -c`, `cp --reflink=auto`).
Add comparison table of symlink vs copy vs CoW strategies with their tradeoffs.
Update language-specific sections (Rust, Node.js) to show CoW patterns instead
of `.worktreeinclude` configuration. Improve clarity on disk efficiency and
cross-platform fallback patterns.

* chore: use wt step copy-ignored for this repo

Add .worktreeinclude with target/ and replace the manual bash CoW
script with `wt step copy-ignored` in post-create hook.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: update all manual CoW examples to use wt step copy-ignored

Update cli.rs (source for hook docs) and tips-patterns.md to use the
new copy-ignored command instead of manual bash CoW scripts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: use gutter format for dry-run output, sync skill files

- Refactor copy-ignored --dry-run to show files in a gutter list
  instead of printing each item separately
- Sync skill documentation files with docs (hook.md, step.md,
  tips-patterns.md) after rebase on main

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: simplify error handling in copy-ignored

- Use expect() for strip_prefix since paths always come from git ls-files
- Remove verbose .with_context() wrappers for simple errors - let them bubble up
- Reduces untestable code paths

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add idempotent and nested file tests for copy-ignored

- test_copy_ignored_idempotent: verifies running twice succeeds (AlreadyExists branch)
- test_copy_ignored_nested_file: copies single file in subdirectory

Improves coverage by exercising more code paths.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test: fix copy-ignored tests to hit correct code paths

- test_copy_ignored_no_worktreeinclude: now creates worktree first
  so it hits the .worktreeinclude check instead of "same worktree"
- test_copy_ignored_empty_intersection: same fix
- Add test_copy_ignored_idempotent and test_copy_ignored_nested_file

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add --to flag test for copy-ignored coverage

Adds test for the --to flag which specifies an explicit destination
worktree. This covers lines 437-439 which were previously uncovered.

Note: The .worktreeinclude parse error path (line 465) and copy error
path (line 524) remain untested because:
- The ignore crate is very lenient and doesn't return parse errors
- Filesystem copy errors are hard to test reliably cross-platform

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* test: add .git skip coverage for copy_dir_recursive

Adds a .git file inside the target directory to test that
copy_dir_recursive correctly skips .git entries (line 598).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* ci: trigger CI run

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-07 20:35:34 -08:00
Maximilian Roos 928b3017cd test: remove verbose assertion messages and doc comments (#458)
* Simplify assert messages by removing redundant descriptions

Remove verbose assertion messages that repeat the condition being tested.
Messages like "Should contain indicator" when asserting `contains("●")` are
redundant since the code is self-documenting. This reduces noise in tests
while maintaining clarity through meaningful assertion failures.

* test: remove verbose assertion messages and doc comments

Removed facile test verbosity across three categories:

1. **Doc comments** (~236 lines): Removed `/// Test X` comments that just
   restated test function names. Kept multi-line explanations that add context.

2. **Test names** (9 functions): Shortened overly verbose names by removing
   filler words while maintaining clarity:
   - test_merge_when_primary_not_on_default_but_default_has_worktree →
     test_merge_primary_not_on_default_with_default_worktree
   - test_complete_switch_option_prefix_shows_options_not_branches →
     test_complete_switch_option_prefix_shows_options

3. **Assertion messages** (19 instances): Removed messages that just restate
   the check:
   - assert!(path.exists(), "Path should exist") → assert!(path.exists())
   - assert!(x.is_file(), "Should be a file") → assert!(x.is_file())

Kept messages that add semantic meaning (e.g., "hook should have run") or
include debugging variables.

Total: 279 lines removed across 31 files. All 723 integration + 401 unit tests
passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* test: remove unreferenced snapshot files from renamed tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 16:51:44 -08:00
Maximilian Roos d35abf7ea7 Simplify assert messages by removing redundant descriptions
Remove verbose assertion messages that repeat the condition being tested.
Messages like "Should contain indicator" when asserting `contains("●")` are
redundant since the code is self-documenting. This reduces noise in tests
while maintaining clarity through meaningful assertion failures.
2026-01-06 15:50:56 -08:00
Maximilian Roos 4709409ccb Consolidate test git commands using helper methods (#339)
* Consolidate test git commands using helper methods

Migrate ~180 instances of manual `Command::new("git")` calls to use
TestRepo helper methods for better consistency and reduced boilerplate:

- `repo.run_git(&["..."])` - run in repo root, panic on failure
- `repo.run_git_in(&path, &["..."])` - run in specific dir, panic on failure
- `repo.git_command()` - get configured Command for output capture

Changes:
- Add `run_git_in()` to `TestRepoBase` trait so BareRepoTest and
  NestedBareRepoTest also get this method
- Migrate tests in switch.rs, remove.rs, merge.rs, list.rs, completion.rs,
  shell_wrapper.rs, push.rs, security.rs, list_config.rs, list_column_alignment.rs
- Migrate internal TestRepo methods to use git_command() with chained env vars
- Remove unused `use std::process::Command` imports where no longer needed

Remaining intentional uses of Command::new("git"):
- TestRepoBase::git_command() and TestRepo::git_command() definitions
- BareRepoTest constructors for `git init --bare` (can't use git_command
  because init takes path as argument, not current_dir)
- Standalone bare repo test in remove.rs (to be refactored separately)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Move BareRepoTest to common module and refactor bare repo test

- Move `BareRepoTest` from bare_repository.rs to common/mod.rs for reuse
- Add `wt_command()` helper method to BareRepoTest
- Refactor `test_remove_default_branch_no_tautology` to use shared BareRepoTest
- Remove unused imports (canonicalize, Command, TempDir) from remove.rs

This reduces the standalone bare repo test from 70+ lines to ~15 lines.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Move commit_in method to TestRepoBase trait

Consolidate BareRepoTest::commit_in_worktree into the shared
TestRepoBase trait as commit_in(). This allows all test harnesses
to share this method without duplication.

- Add commit_in() to TestRepoBase trait (tests/common/mod.rs:619)
- Remove duplicate commit_in_worktree from BareRepoTest
- Update 16 callers in bare_repository.rs
- Update 1 caller in remove.rs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-31 15:46:01 -08:00
Maximilian Roos 27b1629d00 Replace --force with --yes in documentation and tests
Update references to the --force flag to use --yes in config
documentation, completion tests, and shell wrapper tests to reflect
the recent flag refactoring.
2025-12-30 14:26:15 -08:00
Maximilian Roos 1a9ce24f36 Separate --force into --yes (prompts) and --force (removal) (#311)
* Add --force flag to wt remove for worktrees with untracked files

The --force flag now also passes --force to git worktree remove,
allowing removal of worktrees containing untracked files like build
artifacts (.vite/, node_modules/, etc).

- Extended existing --force flag semantics (was: skip approval prompts)
- Added -f short form for convenience
- wt merge now always forces worktree removal (build artifacts unneeded post-merge)

Fixes #301

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Separate --force into --yes (prompts) and --force (removal)

The --force flag on wt remove had two meanings: skipping approval prompts
AND forcing worktree removal with untracked files. This was confusing and
potentially dangerous.

Now:
- --yes/-y: Skip approval prompts (all commands with prompts)
- --force/-f: Force worktree removal with untracked files (wt remove only)

This follows CLI conventions (apt, npm, etc.) where --yes skips prompts
and --force overrides safety checks.

Breaking change acceptable per project guidelines (early release mode).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix unit test for NotInteractive error message

Update test to check for --yes instead of --force.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-30 14:00:06 -08:00
Maximilian Roos 7f1ed8577b Add post-switch hook for background execution after every switch (#284)
Add a new `post-switch` hook that runs in the background after every
`wt switch` operation. Unlike post-start (which only runs on creation),
post-switch runs on all switch results: Created, Existing, and AlreadyAt.

Use cases include renaming terminal tabs, updating tmux window names,
and IDE notifications.

Execution order on `wt switch --create`:
1. Create worktree
2. Run post-create hooks (blocking)
3. Show success message
4. Spawn post-switch hooks (background)
5. Spawn post-start hooks (background)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-22 00:14:55 -08:00
Maximilian Roos 87321281d4 Replace --internal flag with WORKTRUNK_DIRECTIVE_FILE environment variable (#269)
* Replace --internal flag with WT_DIRECTIVE_FILE environment variable

* Rename directive file environment variable to WORKTRUNK_DIRECTIVE_FILE

* Update stale comments referencing directive scripts

Codex review identified comments that still referenced "directive scripts"
after migrating to file-based directive passing. Updated to reference
"data output" which is the current terminology.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Address documentation review feedback

- Add WORKTRUNK_DIRECTIVE_FILE to env var tables (cli.rs, config.md)
- Update stale "directive mode" terminology in test comments to use
  "shell integration" or "data output" as appropriate
- Regenerate help snapshot

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Refactor progressive table output to use output module

Remove finalize_non_tty method and simplify non-TTY output path to use
output::table() directly, consistent with buffered mode. Add WORKTRUNK_SHELL
environment variable support for PowerShell-specific path escaping. Update
documentation and templates accordingly.

* Preserve directive script exit code in PowerShell

Add logic to use the directive script's exit code when wt succeeds, and
add redaction for WORKTRUNK_DIRECTIVE_FILE in test snapshots.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-20 21:33:25 -08:00