mirror of
https://github.com/max-sixty/worktrunk.git
synced 2026-09-14 20:00:38 +08:00
codex/remove-codex-cloud-specific-tests
37 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2203c414f5 |
fix(docs): convert a config-example link whose text holds a bracketed span (#3731)
`wt config create --project` writes a comment into the user's `.config/wt.toml` — and `wt config create --help` prints the same text — carrying a raw, unresolvable Zola link: ``` # When many repositories share one self-hosted host, name it once in user config with a [pattern-keyed `[projects]` entry](@/config.md#user-project-specific-settings) instead of repeating this block in each repo. ``` Every other cross-reference in that file is a plain URL (`… see \`wt hook\` (https://worktrunk.dev/hook/) …`), because `transform_config_source_to_toml` converts the `after_long_help` markdown to plain text on the way into `dev/wt.example.toml`. This one link isn't converted: `convert_markdown_links_for_config` matched link text with `[^\]]+`, which stops at the first `]` — here the one closing the nested `` `[projects]` `` span — so the regex failed to match and the markdown survived verbatim. The line arrived with #3701; it's the only link in either generated example file with a bracketed span in its text. ## The fix **One rule for `]` in link text.** `ZOLA_LINK_PATTERN`, earlier in the same file, already solves this problem for the skill mirrors — it alternates a backticked code span with any non-`]`-non-backtick char, which is why `skills/worktrunk/reference/config.md` renders this very sentence with a resolved URL while the TOML example didn't. `convert_markdown_links_for_config` now uses that same class rather than a second, weaker one. Brackets in these link texts always sit inside a code span, so the class fits the shape exactly, and it covers `[[…]]` array-of-tables names as well — these sections already document `[[projects."…".post-start]]` pipelines, so a link naming one is the next form to arrive. Regenerating produces the intended form: ``` # When many repositories share one self-hosted host, name it once in user config with a pattern-keyed `[projects]` entry (https://worktrunk.dev/config/#user-project-specific-settings) instead of repeating this block in each repo. ``` **A shape the regex declines now fails loudly.** Widening the class fixes the shapes we know about; it can't fix the next one. `finalize_skill_content` already handled that risk with a guardrail — after the rewrite it scans for a stray `](@/…md` and panics with the offending line, precisely because "the regex declined on an unexpected character in the link text" is the expected failure mode. `transform_config_source_to_toml` had no equivalent, which is why this one reached `dev/wt.example.toml` and the `--help` output. That check is now extracted into `assert_no_untransformed_zola_links` and called from both surfaces, so the next unsupported shape is a test failure naming the line rather than a raw `@/config.md` target in a user's config file. ## Why nothing caught it `test_project_config_source_generates_example_toml` compares `dev/wt.example.toml` against the output of this same transform, so an unconverted link is "in sync" by construction — the sync test can't see the difference between a link that converted and one the regex declined to match. Two tests close that gap: - `test_config_markdown_links_convert_to_plain_text` asserts the transform's output directly. It fails on `main`'s regex with exactly the reported symptom, and pins the forms already working (Zola page, Zola page + anchor, absolute URL, two links on one line, the `[[…]]` array-of-tables name) plus the case that must *not* convert — a bare `` `[forge]` `` span is not a link and has to survive verbatim. - `test_untransformed_zola_link_fails_the_config_transform` covers the backstop itself: an unbalanced backtick in link text makes the rewrite decline, and the assertion turns that into a panic naming the line. ## Files - `tests/integration_tests/readme_sync.rs` — the shared link-text class, the guardrail extraction and its second call site, and both tests. - `dev/wt.example.toml` — regenerated by the sync test (one line). - `tests/snapshots/…help_config_create.snap` — the same line, as `wt config create --help` renders it. Ran locally on the final state: `readme_sync::` (15), `test_help` (47), `cargo clippy --tests --all-features`, and `cargo fmt --check`. All green; the generated files are byte-identical under the new class, so the sync tests pass without regenerating. The full gate runs in CI. --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |
||
|
|
02a12c7f59 |
feat(config): match [projects."…"] keys by pattern, and carry forge there (#3701)
## Problem Forge platform is readable only from project config (`[forge].platform`) or a brand substring in the remote hostname. A self-hosted host carrying none of `github`/`gitlab`/`gitea` — a GitLab at `git.company.example`, a company git server — needs the same `[forge]` block in every repository's `.config/wt.toml`. Closes #3678. The user-level `[projects."…"]` table is where per-repository settings already live without touching each repo, but its keys are exact, so covering a host means one entry per repository. ## Solution **Pattern keys.** A `[projects]` key containing `*` matches any run of characters, `/` included, so one entry covers every repository on a host, nested groups and all. `*` is the only metacharacter. ```toml [projects."git.company.example/*"] forge.platform = "gitlab" [projects."git.company.example/platform/*"] worktree-path = ".worktrees/{{ branch | sanitize }}" ``` Every matching entry applies, least- to most-specific, so a narrower key wins where two set the same field and leaves the rest alone. A literal key is the most specific of all; specificity is the count of non-`*` characters. Rules and rationale: the `project_match` module docstring. **`forge` on `[projects]`.** Same shape as the repository's own block, carrying `platform` and `hostname`. Both describe the host rather than the repository — which is why an SSH alias resolved through `~/.ssh/config`, a name local to one machine, belongs in user config rather than a repository's committed one. A repository's own `[forge]` still wins field by field, being the more specific of the two: a repository that sets only `platform` still takes a matching entry's `hostname`. **One resolver.** `wt list`, its statusline, `wt switch pr:`, and CI-platform detection each read project config separately, so a configured platform could resolve in one command and read `unknown` in the next. They now share `Repository::configured_forge_platform` (and `forge_hostname` for the API host). ## Approvals `approved-commands` matches by the same rules, so a pattern entry approves its commands for every repository it covers. That widening is the user's to opt into — only a hand-written key is ever a pattern: - `wt config approvals add` and the interactive prompt record under the exact project identifier, so approving in one repository never reaches another. An identifier that itself contains `*` (a starred remote URL or no-remote path fallback) is refused outright — persisting it verbatim would create an entry reads treat as a pattern; the interactive flow degrades to a warning plus a per-run approval. - `wt config approvals clear` empties only the exact entry, leaving a pattern other repositories share intact — and both its outcomes end with a hint naming any pattern entries still approving commands for the project, so a surviving approval is traceable to the hand-written entry supplying it. - `--stale` judges only the exact entry, so one repository's config can't revoke approvals the others rely on. ## Tests `project_match` unit tests cover `*` spanning `/`, `.` staying literal, specificity ordering, and the lexicographic tie-break. Config tests cover a host-wide entry applying to nested groups, exact-over-pattern precedence, field-by-field layering, hooks appending across both entries, and forge platform/hostname. Forge resolution tests cover the unbranded host, nested groups, a narrower entry winning, project config overriding, falling through to inference, and an invalid value leaving the host unresolved. Approvals tests cover pattern lookup plus the two exactness guarantees above. ## Docs `src/cli/mod.rs` (the primary source) gains "Matching several repositories with one entry" and "Forge platform and hostname" under user project-specific settings, plus a pointer from the project-config forge section. Generated mirrors and `--help` snapshots regenerated. ## Review hardening An adversarial review pass surfaced eight findings, all fixed: - **Approval widening (moderate)**: the starred-identifier refusal above. Previously such an approval persisted verbatim and silently approved its commands for every repository the star matched. - **Literal-key tie (moderate)**: a pattern whose stars all match empty (`github.com/owner/repo*`) ties the exact key on literal count and sorted after it, so its values won the fold. Literal keys now outrank any pattern outright. - **Docs vs behavior (moderate)**: the layering paragraph claimed "most specific wins" for everything; hooks and aliases actually append across matching entries (all run, least-specific first). Docs now say so, and state the forge field-by-field precedence. - **Minor**: `matches()` is a two-pointer byte glob (was a per-call regex compile, ~0.7 ms per pattern key, a few hundred calls per `wt list`), pinned by an exhaustive differential test against a reference matcher; the invalid-platform diagnostics name their two possible config homes; a root `[forge]` in user config now points at `[projects."<id>"].forge`; docs note a host-wide key should end in `/*`; `approve_command` delegates to `approve_commands`, unifying their dedup predicates. ## Relationship to #3681 This is an alternative to #3681, which adds a bespoke `[forge-hosts]` section for the same issue. Both can't land — they'd be two ways to write one sentence. This one puts the setting in the table that already carries per-repository user config, and the pattern keys are reusable for the workspace-scoped ask in #3654 where repositories share a host or namespace. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
0f2d562541 |
fix(forge): classify a forge by the brand in the hostname, not by DNS label (#3673)
Reverts the branded half of the exact-label classifier and deletes the diagnostic built to explain it. `github-enterprise.acme.com`, `mygithub.com`, `gitlab-internal.company.com`, and the `github-personal` SSH alias resolve to their forge again, so CI status, `wt switch --prs`, and `repo.provider` work with no config. ## Why the label boundary goes It looked like an ownership check and wasn't one. An attacker controls their own DNS, so `github.attacker.example` has the exact label `github` and classified fine; `gitlab.evil.co.uk` likewise. What the rule actually excluded was the self-hoster who put the brand in a hyphenated name. It failed open for the adversary and closed for the customer. The residual case for it doesn't survive either. The hostname comes out of the user's own `.git/config`, and whoever can put a host there can put code there too — the trust decision happens at clone time, and by the time worktrunk reads the remote the user is already building from it. All the classification decides is which forge CLI (`gh`, `glab`, `tea`, `az`) runs against it. So the rule is recall-first: any host carrying `github`, `gitlab`, or `gitea` matches, first match winning. The cost is a host that merely sounds like a forge getting a forge CLI run at it, which surfaces as that CLI's error rather than as silence — the better of the two failures, and `forge.platform` overrides it. ## What stays Azure DevOps keeps suffix matching on its two service domains, and for a reason unrelated to security: those are service domains rather than a brand in the host, so every real hosted instance already matches, and the on-prem edition carries neither string. `dev.azure.com.attacker.example` and `evil-visualstudio.com` are outside the domains and carry no brand to fall back on, so they stay unclassified. Userinfo still resolves to the network host, so `https://github.com@attacker.example/…` is `attacker.example`. ## What goes `LegacyForgeAlias`, `Repository::legacy_forge_alias`, `legacy_forge_alias_diagnostic`, and its three emit sites in `wt list`, `wt switch --prs`, and `wt config show --full`. Every host the diagnostic fired on now classifies, so it could only ever return `None`. A host with no brand at all still reaches the existing generic hint, which is the right message there — there is no platform to infer. The end-to-end warning-dedup test goes with it, since no warning is raised from both the collect and `--prs` threads any more; `stash_warning_preserves_order` keeps the mechanism covered. ## Docs `## Forge platform` in `src/cli/mod.rs` described the override as being for SSH aliases and self-hosted instances, which now detect on their own. It states the rule and scopes the override to hosts carrying no brand — a Forgejo instance at `forge.example.com`. Mirrors, `dev/wt.example.toml`, and the two `config` help snapshots regenerate from it. --- The branch's history has a false start — the first commit widened the diagnostic, the second deletes it in favour of relaxing classification — plus a merge of `main` after v0.71.0 shipped. The net diff is the second approach; it all squashes on merge. Also supersedes [#3672](https://github.com/max-sixty/worktrunk/pull/3672), the triage bot's PR for the same issue — it widens the diagnostic rather than removing the need for one, so it should be closed too. Closes #3671 > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
568b6de85f |
docs: consolidate duplicated explanations and trim slop (#3494)
Sweep of the docs for repetition and filler, from an audit of the hand-authored pages, the command pages' source in `src/cli/mod.rs`, and the plugin skill. Net −574 lines; every cut either had a surviving canonical home or restated an adjacent sentence. **One home per mechanism** (other mentions now link to it): - `template-append` — the LLM commits guide owns the rendering mechanism; the user- and project-config sections keep the key, an example, and what is unique to them (the project approval gate and the only-`template-append`-from-project scoping). Was explained in full in three places. - `-vv` diagnostic files — `wt config state logs` owns the four file descriptions; the FAQ summarizes in one sentence and links. - fsmonitor/trash cleanup — the FAQ's two overlapping `wt remove` bullets merge into one that distinguishes own-daemon teardown from the orphan sweep; troubleshooting.md's restatement compresses to a pointer, keeping its unique wedged-daemon-on-live-worktree guidance. - copy-ignored built-in excludes — the `wt step copy-ignored` page owns the directory list (previously enumerated 4×); the config sections state the rule and link. - hook-types table — `wt hook` owns it; the extending guide replaces its verbatim copy with a sentence. - LLM tool commands — unchanged, deliberately: the apparent hand-synced duplication between llm-commits.md and the config example is already machine-pinned by `test_llm_docs_commands_match_config_example`, so it cannot drift. **Cut-over debt**: the deprecated `wt config state ci-status` section shrinks to a deprecation pointer — its status table, fetch order, and caching notes all duplicated the `wt list` CI-status section. **Structure**: `wt step copy-ignored`'s "Features" list dissolves into the sections that owned its facts (excludes → "What gets copied", reflink → "Performance"); the four trailing "Note: This command is experimental…" lines go (the `[experimental]` badge already appears twice per section); shell-integration.md described the directive-file mechanism twice and now describes it once. **SKILL.md** drops from 339 to 181 lines: the permission-model section duplicated the config-types bullets, the hook-type mapping appeared twice, and the "Determining Which Config to Use" / "Validation Before Adding Commands" scaffolding enumerated judgments an agent makes on its own. The approvals-escalation and agent-handoff sections are untouched. **Deliberately not addressed**: `wt list`'s schema 1 JSON tables (still the default schema; the wholesale deletion lands when the default flips) and corpus-wide em-dash density (a house-style decision, not a per-page fix). Generated mirrors (docs command pages, skill references, plugins mirror, `dev/*.example.toml`, help snapshots) regenerated via `test_docs_are_in_sync` and `cargo insta`. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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) |
||
|
|
f522357e11 |
feat(commit): experimental project-level commit-message guidance (#2774)
## Summary
- Adds `[commit.generation] template-append` to **both** user config and
project config (`.config/wt.toml`). It *adds to* the commit/squash
prompt rather than replacing it (`template` still replaces). One field
covers both commit and squash.
- Each fragment is rendered as its own minijinja template with the same
variable context as the main template, then emitted in a
provenance-labeled block: `<user-guidance>` (the developer's own config
— no approval) followed by `<project-guidance>` (repo-supplied — gated).
Default templates render each block conditionally; `{{ user_guidance }}`
/ `{{ project_guidance }}` are exposed for custom user templates.
- The project fragment keeps the first-time approval gate — same
one-shot gate as project hooks. `wt merge` bundles it into the existing
hook-approval batch so the user sees one combined prompt. Declining is
non-fatal; the LLM runs with just the user fragment (if any). Skipped
automatically when no LLM command is configured.
- Marked experimental in CLI help, docs, and the user/project config
docstrings.
Discussion: #2758 (comment
[4454619614](https://github.com/max-sixty/worktrunk/issues/2758#issuecomment-4454619614))
## Test plan
- [x] Unit tests for `commit_template_append()` and the user-side
accessor (trim / empty / unset)
- [x] `Merge` test for user `template-append`
- [x] Snapshot tests confirming the default commit and squash templates
render the `<project-guidance>` block
- [x] Unit tests for user-side append: renders into `<user-guidance>`,
ordered before `<project-guidance>`, blank-is-unset, minijinja variable
expansion
- [x] PTY integration tests for the approval flow (accept / decline /
merge bundling), asserting `<project-guidance>`
- [x] `cargo run -- hook pre-merge --yes` — 3708 tests passing, clippy +
pre-commit clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
00c4508671 |
refactor(ci): move CiPlatform to the lib crate, cache on RepoCache, drop the "override" framing (#2692)
Follows up on #2686 (the original "dedup the warning" fix), which parked the configured-platform state in a process-wide `OnceLock` because `CiPlatform` lived in the binary crate and `RepoCache` in the library crate. **Move `CiPlatform` to the lib crate.** It's now `worktrunk::git::CiPlatform` (a new `src/git/ci_platform.rs` module), with `Repository::ci_platform(remote_hint)` replacing the free `platform_for_repo` and a private `Repository::configured_ci_platform()` reading `forge.platform` / `ci.platform`. The configured value caches on a `OnceCell` field of `RepoCache` instead of a process global — same once-per-`wt list` warning dedup, but the repo-scoped data no longer lives in a process static. `src/commands/list/ci_status/platform.rs` is now pure forge dispatch (`gh` / `glab` / `az`) over a `CiPlatform`; the inherent `detect_*` methods became free functions so the enum could move without dragging the bin-crate backends along. **Drop the "override" framing.** `forge.platform` / `ci.platform` is now described everywhere as "the platform, falling back to URL detection when unset" rather than "overriding detection" — config docs (`## Forge platform`), the `ProjectForgeConfig` / `ProjectCiConfig` struct and accessor docs, `wt config state ci-status` help, and a few test names (`test_list_full_with_configured_platform_github`, `test_list_full_with_invalid_configured_platform`, `test_switch_pr_gitea_forge_platform`). Genuine "override" uses elsewhere (config-path/env-var overrides, `wt config state default-branch set`) are untouched. **Fix a pre-existing bug.** `forge.platform = "gitea"` is a valid value — the `wt switch pr:` shortcut uses it to pick `tea` — but worktrunk fetches CI status only from GitHub/GitLab, so `wt list` used to print `Invalid CI platform in config: 'gitea'. Expected 'github' or 'gitlab'.` for it. `configured_ci_platform()` now recognizes `gitea` as a known forge it just doesn't show CI for: no warning, blank CI column. Added `test_list_full_with_gitea_forge_platform` (asserts blank CI and empty stderr). Branch is merged with `main`, so the dispatch and detection handle the new `AzureDevOps` variant alongside GitHub and GitLab. `cargo run -- hook pre-merge --yes` passes. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1ca73ab52c |
feat: experimental Azure DevOps support (#1256)
## Summary
Adds **experimental Azure DevOps support** alongside the existing GitHub and GitLab integrations:
- `wt switch pr:<N>` resolves Azure DevOps PRs via the `az` CLI (auto-detected from `dev.azure.com` / `ssh.dev.azure.com` / `*.visualstudio.com` remotes, or pinned via `[forge] platform = "azure-devops"`).
- `wt list --full` surfaces Azure DevOps PR and pipeline CI status.
- `wt config show --full` reports `az` install/auth state when Azure DevOps is the detected platform.
GitHub still wins in mixed-remote setups; `forge.platform` is the override. Requires the `azure-devops` CLI extension (`az extension add --name azure-devops`).
## Context
Originally proposed by @mikeyroush; reimplemented against current `main` to pick up the `[forge]` config section, `url.insteadOf` fallback, the `handle_switch` consolidation, and the new Gitea provider that all landed after the original branch was opened. The dispatch path (`choose_pr_provider`) is shared with GitHub/Gitea — Azure is just a fourth provider in the same priority chain.
Implementation notes worth a reviewer's attention:
- Azure DevOps URLs don't fit the standard `host/owner/repo` shape (`dev.azure.com/{org}/{project}/_git/{repo}`), so `Repository::find_remote_for_azure` matches on `org` + `project` + `repo` instead of the owner-based path used for the other forges.
- Pipeline/PR web URLs are constructed from `org/project/build-id`, not the API's `url` field (which is a REST endpoint).
- `*.visualstudio.com` legacy hosts encode the org in the hostname; the URL helpers handle both shapes.
Fixes #1144
## Test plan
- `cargo run -- hook pre-merge --yes` — 3631 tests pass, clippy + fmt clean
- Unit tests cover the host-aware URL helpers, `find_remote_for_azure` (all URL shapes + the same-org/different-project collision case), and `choose_pr_provider` dispatch
- Integration tests bring Azure to parity with the other forges (see Coverage):
- 13 `test_switch_pr_azure_*` tests mirroring the Gitea suite — same-repo, fork, `*.visualstudio.com` host, create/base conflicts, not-found, az-not-installed, `forge.platform` override, invalid JSON, generic server error, auth error, missing `azure-devops` extension, undeterminable org/host
- 9 `test_list_full_with_azure_*` tests covering `detect_azure_pr` (conflicts, queued, stale, retriable error) and `detect_azure_pipeline` (passed/failed/running, stale, no runs, retriable error)
- Manual validation: `wt switch pr:<N>` and `wt list --full` against an Azure DevOps repo
## Coverage
The `az`-shelling code (`fetch_pr_info`, `detect_azure_pr`, `detect_azure_pipeline`) is now exercised by integration tests via new `setup_mock_az*` helpers (modeled on `setup_mock_gh` / `setup_mock_glab`) — covering the happy paths plus the not-found / auth / extension-missing / generic-error / retriable-error branches. The non-`az` parts (URL parsing, provider dispatch, remote matching) remain unit-tested.
|
||
|
|
7fca05441e |
feat(switch): experimental Gitea PR support via pr: shortcut (#1320)
Add experimental Gitea PR support to `wt switch pr:<number>`.
The `pr:` syntax already resolved GitHub PRs; this teaches it to also
resolve Gitea PRs via the `tea` CLI. GitLab continues to use `mr:`.
## Dispatch
`pr:N` now goes through `choose_pr_provider`:
1. `[forge] platform` in `.config/wt.toml` if set (`github` / `gitea` /
`gitlab`)
2. Primary remote URL detection (host contains `github` / `gitea` /
`gitlab`)
3. CLI auth lookup: if `tea` is configured for this host (per
`~/.config/tea/config.yml`) but `gh` is not (per `gh auth token
--hostname <host>`), pick Gitea
4. Default to GitHub
There is no longer an "ambiguous" fallback that tries both providers and
wraps both errors — users on self-hosted Gitea instances either run `tea
login add <host>` (auto-detected) or set `[forge] platform = "gitea"`.
## New code
- `src/git/remote_ref/gitea.rs` — `GiteaProvider` implementing
`RemoteRefProvider` via `tea api repos/<owner>/<repo>/pulls/<n>`; reuses
the shared `cli_api_error` / `run_cli_api` helpers.
- `src/git/remote_ref/info.rs` — `PlatformData::Gitea { host,
head_owner, head_repo, base_owner, base_repo }`, wired into
`source_ref()`, `prefixed_local_branch_name()`, and `find_remote()`.
- `src/git/url.rs` — `GitRemoteUrl::is_gitea()`.
Shared helpers introduced in this PR:
- `mod.rs::extract_host_from_html_url()` (used by github + gitea;
identical 7-line chains collapsed).
- `github::is_authed_for()` (wraps `gh auth token --hostname`).
- `gitea::is_authed_for()` (reads tea's config.yml; never invokes `tea`
to avoid OAuth refresh on lookup).
## Docs
User-facing copy says "GitHub PR" by default; one paragraph in `wt
switch --help` mentions Gitea support, marked experimental.
## Tests
- 13 new integration tests covering Gitea same-repo, fork, error
responses (401/403/404/5xx/malformed JSON/deleted fork/no source
branch), `tea` not installed, `forge.platform` overrides,
GitLab-remote-with-`pr:` bail, self-hosted defaults-to-GitHub, and
self-hosted-with-`tea`-login routes-to-Gitea.
- Unit tests for `extract_source_branch` edge cases and the tea config
parser.
## Compatibility
No CLI flag or config file changes. The `tea` CLI is only required for
Gitea PRs; GitHub-only users see no change.
---------
Co-authored-by: worktrunk-bot <noreply@anthropic.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
|
||
|
|
f418729e21 | fix(step/copy-ignored): drop .pi/ from built-in excludes (#2527) | ||
|
|
86f4b37e48 |
docs(help): rename '[Aliases]' link to 'Extending Worktrunk guide' (#2330)
The bare `[Aliases](@/extending.md#aliases)` link text rendered in terminal help as "See Aliases for ..." — a self-reference when sitting inside a section already titled "Aliases" (`wt config user/project --help`), and a generic label elsewhere (`wt config alias --help`). Renamed to `[Extending Worktrunk guide]` in all three call sites. Also adds a short guideline under "Help text authoring" in CLAUDE.md: link text must stand alone when the URL is stripped (since terminal help strips URLs and keeps only the text). > _This was written by Claude Code on behalf of Maximilian_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
a1be5645c4 |
feat(alias): dispatch aliases from top-level wt <name> (#2266)
`wt deploy` now resolves `deploy` against configured aliases before falling through to a `wt-deploy` PATH binary. Built-ins still win (clap matches before alias dispatch ever runs), and `wt step <name>` keeps working at runtime — only the docs cut over to the new form. ## Why `wt deploy` reads better than `wt step deploy`, and aliases as first-class commands lower friction for using them as everyday shortcuts. ## Precedence built-in (clap) → alias (user/project config, merged) → `wt-<name>` PATH binary → "unrecognized subcommand" error. User config wins over PATH binaries because aliases are how users customize wt — same model as git, where `[alias]` entries shadow `git-foo` externals. ## Navigating the diff - `src/commands/alias.rs` — refactored `step_alias` to share `run_alias` with the new `try_alias(name, rest) -> Result<Option<()>>`. Returns `Ok(None)` when the name isn't a configured alias or when not in a git repo; propagates config-load errors so a broken `wt.toml` fails loudly instead of silently turning into "unrecognized subcommand". Argument parsing is gated on alias-membership, so unrelated args meant for an external binary don't surface as alias parse errors. New `alias_names_for_suggestions()` mixes alias names into "did you mean" hints. `HelpContext` enum lets the help splice annotate "(shadowed by built-in)" against the right level (top-level builtins for `wt --help`, step builtins for `wt step --help`). The user-facing "shadow warning" was removed entirely — under the new model an alias named `commit` runs fine via `wt commit`, only `wt step commit` is shadowed. - `src/commands/external.rs` — `handle_external_command` calls `try_alias` first, then PATH lookup, then unrecognized-subcommand error. Suggestions include alias names. Non-UTF-8 args bypass alias dispatch (alias parser requires UTF-8; binary subcommands get raw `OsStr`). - `src/help.rs` + `src/main.rs` — early-parse pass returns `Option<HelpContext>`; help splice fires for both `wt --help` and `wt step --help`. - `src/completion.rs` — aliases injected at the top level in addition to `step`. - `src/cli/mod.rs` — long Aliases section moved out of `Step::after_long_help` into hand-authored `docs/content/extending.md`. New sync test `test_top_level_builtins_match_clap` keeps the `TOP_LEVEL_BUILTINS` constant aligned with the `Cli` enum. ## Tests 3221 tests pass, lints clean. New integration tests: `test_top_level_alias_dispatch`, `test_top_level_alias_with_step_builtin_name`, `test_top_level_alias_did_you_mean`. Removed `test_step_alias_shadows_builtin_plural` (warning gone). Reframed `test_step_alias_shadows_builtin` to verify shadow filtering of typo suggestions instead. Completion tests now isolate user config via `WORKTRUNK_CONFIG_PATH=/dev/null` — project config isolation is a noted gap (commented inline). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b6ddf736d7 | docs(config): improve project config intro and template variable heading (#2032) | ||
|
|
36aec6b090 |
Document hooks in user config, fix config doc comment style (#1861)
Adds a `## Hooks` section to user config docs — the config supported hooks but didn't document them. Also fixes comment style issues and improves cross-references. - Added `## Hooks` section to user config with the three formats (string, named table, pipeline) and user-vs-project differentiation - Used plain text descriptions before code blocks instead of TOML comments inside them, avoiding `# #` double-commenting in `dev/config.example.toml` - Replaced the duplicate TOML block in project config hooks with a minimal example and cross-reference to `wt hook` docs - Both hooks sections now link to `wt hook` docs as the canonical reference - Added CLAUDE.md guidance on avoiding TOML comments inside code blocks in config docs > _This was written by Claude Code on behalf of @max-sixty_ Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
09332057f1 |
fix: add hook examples back to project config docs (#1853)
## Problem
CI failed on main after #1845 (
|
||
|
|
491108a782 |
Document hooks in user config (#1845)
The user config (`~/.config/worktrunk/config.toml`) supports hooks via `OverridableConfig.hooks`, but the user config documentation didn't mention them. The project config docs had a hooks section with the full TOML format reference — this adds a comparable section to user config and deduplicates the project config side. - Added `### Hooks` section to user config docs with the three formats (string, named table, pipeline) and user-vs-project differentiation - Replaced the duplicate TOML block in project config hooks with a cross-reference to the user config section > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
ca5980aee8 |
Remove double-commenting from project config example (#1836)
Same treatment as #1832: move prose descriptions out of TOML code blocks so they don't produce `# #` lines in the generated `wt.example.toml`. Also uncomments `hostname` field with an "Example:" note. > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
75dd894284 |
Auto-generate wt.example.toml from mod.rs source (#1835)
The project config example file (`dev/wt.example.toml`) was hand-maintained with no sync tests, while the user config example had a robust auto-generation pipeline. This brings both to parity — `mod.rs` is the single source of truth, sync tests generate both example files, and CI catches drift. Hooks documentation (~70 lines of formats, template variables, and per-type examples) replaced with a pointer to `wt hook --help` plus a quick-reference showing all three hook formats (string, named table, pipeline). The example file now focuses on project-specific settings: `list.url`, `forge`, `step.copy-ignored`, and aliases. The sync test infrastructure is refactored from a user-config-specific function into shared `extract_config_section` and `assert_config_example_in_sync` helpers that both tests call. > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
b33cc8e9d6 |
Simplify forge detection with [forge] config section (#1826)
SSH host aliases (`git@work-ssh:owner/repo`) corrupt the hostname in
remote URLs but never the path. The previous forge detection code tried
to work around this by searching all remotes, auto-detecting GHE
hostnames, and maintaining multiple resolution strategies. This PR
replaces all of that with a simpler design: derive owner/repo from URL
paths, and let users set `forge.platform` / `forge.hostname` for corner
cases.
## Key changes
**New `[forge]` config section** with `platform` and `hostname` fields.
`ci.platform` is deprecated with migration support. Example configs and
inline help examples updated.
**`fetch_pr_info`** now builds literal API paths from the primary
remote's raw URL instead of relying on `gh`'s placeholder resolution.
`--hostname` only passed when explicitly configured.
**`platform_for_repo`** checks config > branch's remote > primary
remote. No longer searches all remotes. Loads config internally instead
of taking a `platform_override` parameter.
**`find_remote`** matches by owner/repo only (no host required),
handling SSH aliases where the local hostname differs from the API
hostname.
**`CiBranchName::from_branch_ref`** uses `split_once('/')` instead of
iterating `all_remote_urls()`, removing a git call per branch during `wt
list`.
## Deleted
- `resolve_gh_hostname` (GHE auto-detection heuristic)
- `find_forge_remote` (search all remotes by predicate)
- `all_remote_urls()` loop in branch name parsing
- `platform_override` parameter threading through callers
The spec in `src/git/repository/remotes.rs` documents the full design.
> _This was written by Claude Code on behalf of max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
5c1672932f |
docs: remove deprecated post-create from documentation (#1776)
Replace all `post-create` references with `pre-start` across documentation, skills, example config, and test names. The Rust deprecation handling code (migration, alias, config parsing) remains intact for users with existing configs. Also removes `post-create` from the `wt hook show` value_parser — it was listed as a valid hook type for display even though it's been deprecated. > _This was written by Claude Code on behalf of @max-sixty_ Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
ce9ea1a6b6 |
refactor: consolidate .git/wt-* paths under .git/wt/ and stage removed worktrees in trash (#1583)
## Summary - Moves the rename-based staging directory from a visible sibling path (`project.wt-removing-<timestamp>`) into `.git/wt/trash/`, hiding it from the user's workspace - Consolidates all worktrunk-managed `.git/wt-*` directories under a single `.git/wt/` parent - Adds `Repository::wt_dir()` accessor as the single root for all worktrunk state - Renames `wt-relocate-tmp` to `wt/relocate-staging` for consistency with `wt/promote-staging` - Falls back to legacy `git worktree remove` if the trash directory can't be created - The `.git/` directory is always on the same filesystem as worktrees, so the instant rename guarantee is preserved ## Path migration | Before | After | |--------|-------| | `.git/wt-logs/` | `.git/wt/logs/` | | `.git/wt-cache/summaries/` | `.git/wt/cache/summaries/` | | `.git/wt-cache/ci-status/` | `.git/wt/cache/ci-status/` | | `.git/wt-promote-staging/` | `.git/wt/promote-staging/` | | `.git/wt-relocate-tmp/` | `.git/wt/relocate-staging/` | | (new) `.git/wt/trash/` | Staging for background removal | ## Context Users reported confusion when seeing `.wt-removing-*` directories in their workspace after `wt remove` (#1572). By staging in `.git/wt/trash/` instead, the directory is completely hidden — even if the background `rm -rf` is slow or gets interrupted. The `.git/wt-*` sibling directories were also consolidated into `.git/wt/` for tidiness per review feedback. ## Test plan - [x] Unit tests for `generate_removing_path` and `build_remove_command_staged` updated and passing - [x] All 105 remove-related integration tests passing - [x] `test_remove_background_path_gone_immediately` — verifies instant removal still works - [x] `test_remove_background_fallback_on_rename_failure` — verifies fallback when staging path is blocked - [x] `test_remove_stale_staging_dir_from_crashed_removal` — verifies stale dirs land inside `.git/` - [x] All 2496 tests pass (lib + bin + integration) - [x] Help snapshots, doc sync, and lint checks all pass Closes #1572 🤖 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> Co-authored-by: Maximilian Roos <5635139+max-sixty@users.noreply.github.com> |
||
|
|
55bc9188b4 |
feat(step): add alias command for user-defined command templates (#1348)
## Summary
- Adds `wt step <alias-name>` for running user-defined command templates
configured in `[aliases]` sections of user or project config
- Aliases support the same template variables as hooks (`{{ branch }}`,
`{{ worktree }}`, etc.) plus custom `--var KEY=VALUE` variables
- Project-config aliases require the same command approval flow as
project hooks; user-config aliases are trusted. `--dry-run` skips
approval since it's a read-only preview
- Alias names matching built-in step commands are filtered from the
"available" list in error messages (shadowed by the built-in)
- Introduces `Phase` enum (`Hook(HookType)` | `Alias`) replacing the old
`hook_type` + `phase_override` pattern in the approval system
- Renames `HookCommand` to `ApprovableCommand` since it now covers both
hooks and aliases
## Test plan
- [x] Unit tests for `AliasOptions::parse` (name-only, --dry-run, --yes,
--var, empty key rejection, positional arg rejection)
- [x] Integration tests for alias execution, dry-run (without --yes),
exit code propagation
- [x] Integration tests for shadowing (filtered from available list)
- [x] Integration tests for user/project config merging
- [x] Integration tests for approval flow (project prompts, user skips,
override skips, already-approved, --yes bypass, decline)
- [x] Unit tests for `merge_alias_maps` coverage
- [x] Sync test: `BUILTIN_STEP_COMMANDS` matches actual `StepCommand`
clap variants
> _This was written by Claude Code on behalf of max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
a9ed5344ed |
refactor: rename main_worktree_path template var to primary_worktree_path (#613)
* refactor: rename main_worktree_path template var to primary_worktree_path The old name was confusing because "main worktree" has a specific meaning in git (the original clone directory), but the template variable actually provided the "primary" worktree — where established files live. - Rename template var: main_worktree_path → primary_worktree_path - Remove default_branch_worktree() method, inline into primary_worktree() - Add deprecation mapping so existing configs continue to work - Update all documentation and examples Co-Authored-By: Claude <noreply@anthropic.com> * docs: improve primary_worktree_path description in examples Update the template variable description from "Where established files live" to "Main worktree (or default branch worktree for bare repos)" for clarity. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
6a4aa35e7e |
docs: consolidate template variables reference into hook.md (#544)
- Remove deprecated variable mentions from all docs - Replace duplicate table in step.md with link to hook.md#template-variables - Add URL to canonical reference in project config template - All filters (sanitize, sanitize_db, hash_port) documented in hook.md Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
962b548bbf |
feat: add sanitize_db filter for database-safe identifiers (#498)
* feat: add sanitize_db filter for database-safe identifiers Add a new template filter `sanitize_db` that transforms strings into database-safe identifiers compatible with PostgreSQL, MySQL, and SQL Server. Transformation rules: - Convert to lowercase - Replace non-alphanumeric characters with underscore - Collapse consecutive underscores - Prefix with underscore if starts with digit - Truncate to 63 characters (PostgreSQL limit) Examples: - feature/auth-oauth2 → feature_auth_oauth2 - 123-bug-fix → _123_bug_fix - UPPERCASE.Branch → uppercase_branch Closes #474 Co-Authored-By: Claude <noreply@anthropic.com> * docs: add sanitize_db mention to database per worktree section Add example showing how to use sanitize_db for branch-based database names in the tips-patterns.md guide. Co-Authored-By: Claude <noreply@anthropic.com> * docs: add sanitize_db to example configs and tips-patterns - Add sanitize_db to filter list in wt.example.toml - Add sanitize_db to variables section in config.example.toml - Add usage example in tips-patterns.md database section Co-Authored-By: Claude <noreply@anthropic.com> * docs: simplify URL template description Remove filter list from URL template description - the example already shows hash_port usage and other filters aren't relevant in this context. Co-Authored-By: Claude <noreply@anthropic.com> * feat(sanitize_db): add 3-char hash suffix for collision/keyword safety Appends a deterministic 3-character base36 hash suffix to sanitize_db output. This ensures: - SQL reserved words are avoided (e.g., `user` → `user_abc`) - Different inputs don't collide (e.g., `a-b` and `a_b` get different suffixes since hash is computed from original input) The suffix uses 46,656 unique values (36^3), providing good uniqueness while keeping identifiers readable. Updated all documentation examples to use sanitize_db for database names (POSTGRES_DB, DATABASE_URL) instead of repo name. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
eba1553446 |
feat(config): [ci] platform override and documentation improvements (#465)
* feat(config): add [ci] platform override for custom CI domains For GitHub Enterprise or self-hosted GitLab with custom domains where URL-based detection fails, users can now explicitly specify their CI platform: ```toml [ci] platform = "github" # or "gitlab" ``` - Add ProjectCiConfig struct with platform field - Modify get_platform_for_repo to accept platform_override parameter - Add ProjectConfig::ci_platform() helper to reduce duplication - Update all call sites (config.rs, list/mod.rs, ci_status.rs) - Document in wt config --help and example config 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: document underdocumented features Add documentation for features that existed but weren't discoverable: - `--show-prompt` and `--stage` flags for `wt step commit/squash` - `skip-shell-integration-prompt` config option for CI environments - `[select]` pager configuration for preview panel - Expanded JSON query examples for `wt list --format=json` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
bd865a466f |
Improve default config examples (#432)
* Standardize template variable documentation examples
Use consistent canonical values throughout all documentation:
- Repo: myproject (not my-project)
- Branch: feature/auth (not feature/foo, feature/new-stuff)
- Worktree: myproject.feature-auth
- Description: "Repository directory name" (not "Repository name")
Also:
- Add missing {{ main_worktree_path }} to Claude plugin skill docs
- Expand dev/wt.example.toml with all available variables
- Add Documentation Examples section to cli-output-formatting.md
- Sync auto-generated docs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Simplify project config example template
- Show both hook formats (single/table) once at top
- Remove redundant format examples from each hook section
- Remove separate Examples section at bottom
- Keep language-specific commands as examples (npm, docker)
- Trim template variables to most useful subset
Result: 91 lines (down from 162)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
feff9d22ca |
Rename template variables for clarity with deprecation warnings (#369)
* Rename template variables for clarity with deprecation warnings Renames hook template variables for clearer semantics: - `repo_root` → `repo_path` (absolute path to repository root) - `worktree` → `worktree_path` (absolute path to current worktree) - `main_worktree` → `repo` (repository directory name) Adds new `main_worktree_path` variable for the actual path to the default branch worktree, useful for sharing dependencies across worktrees. Deprecated variables continue to work but emit warnings with migration assistance: - Detects deprecated variables using minijinja's undeclared_variables() - Creates a `.new` migration file with replacements - Shows inline warning: "User config uses deprecated template variables: repo_root → repo_path, worktree → worktree_path" - Shows hint: "Wrote migrated config.toml.new; to apply: mv ..." Project config warnings only appear when in main worktree (where changes can be committed). Warnings are deduplicated per path per process. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix issues identified in Codex review - Fix malformed template bug: check for closing `}}` BEFORE emitting replacement to prevent corrupted output - Add shell escaping to mv command paths using shell_escape::escape - Add `--` separator to mv command to prevent paths starting with `-` from being interpreted as flags - Handle migration file write errors gracefully with hint message instead of propagating error that would block config loading - Use distinct [TEST_CONFIG_NEW] placeholder for .new files in snapshots for better readability - Add test for malformed template preservation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Simplify deprecated variable replacement with regex Replace manual character-by-character parsing (~55 lines) with regex-based approach (~13 lines) that: - Handles both {{ }} and {% %} Jinja blocks - Supports multiline blocks with (?s) dotall mode - Preserves original formatting instead of normalizing whitespace - Uses word boundaries to avoid false matches Trade-off: regex moves from dev to prod dependency. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix snapshot filter for Windows shell-escaped paths Windows paths with backslashes get quoted by shell_escape, so the filter needs to handle: 'C:\Users\...\test-config.toml' Update regex to match both forward/backward slashes and optional quotes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Simplify replacement by reusing TOML string extraction Instead of regex-matching Jinja blocks, reuse extract_template_strings() to get TOML string values, then do word-boundary replacement within them. This naturally excludes TOML keys like `worktree-path` since we only process string values, not keys. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Update test snapshots after merge: main_worktree → repo 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
3471fee63b |
Update docs and help text to clarify default branch references (#275)
* Update docs and help text to clarify default branch references * Replace "main" with "default branch" in docs and help text Update references throughout documentation and help snapshots to use "default branch" instead of hardcoded "main" for clarity. Changes include: - Help text descriptions for list, merge, switch, and remove commands - Configuration file documentation and examples - JSON output field descriptions - Status symbol explanations - Example commands and shortcuts Also update environment variable from GIT_EDITOR to GIT_CONFIG_GLOBAL in test snapshots and add missing RUST_LOG variable. |
||
|
|
5f32e30bd2 |
Add hash_port filter for deterministic port assignment (#266)
Add a `hash_port` filter that hashes any string to a port number in the
range 10000-19999. This enables running dev servers on unique ports per
worktree without port collisions.
Example usage in hooks:
```toml
[post-start]
dev = "npm run dev --port {{ branch | hash_port }}"
```
The filter uses Rust's DefaultHasher for simple deterministic hashing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
7e7b49999e |
Add {{ branch | sanitize }} filter for explicit branch sanitization (#265)
Changes {{ branch }} to provide raw branch names (e.g., feature/auth) and
adds a sanitize filter for filesystem-safe paths. Users now explicitly use
{{ branch | sanitize }} to replace / and \ with -.
This makes sanitization visible in templates rather than implicit.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
b2048db58a |
feat: Add user-level hooks that run for all repositories, add wt hook show (#118)
* feat: Add user-level hooks that run for all repositories
Implement a new user hooks system that allows developers to define
personal hooks in `~/.config/worktrunk/config.toml` that execute for
all repositories. User hooks complement project hooks with these key
differences:
- **Scope**: Run for all repositories (vs. single repository)
- **Location**: User config file (vs. `.config/wt.toml`)
- **Approval**: Not required (user implicitly approves via config)
- **Execution order**: Run before project hooks
- **Skip behavior**: Skipped together with project hooks via `--no-verify`
### Implementation Details
**Configuration syntax** (same as project hooks):
```toml
[post-create]
setup = "echo 'Setting up worktree...'"
[pre-merge]
notify = "notify-send 'Merging {{ branch }}'"
```
**Hook types supported**: post-create, post-start, pre-commit, pre-merge,
post-merge, pre-remove
**Template variables**: Support all project hook variables plus `remote_url`
for repository-specific conditional logic
**New data structures**:
- `HookSource` enum: Distinguishes user vs. project hooks
- `prepare_user_commands()`: Expands user hook templates without approval
**Updated hook pipeline**:
- `HookPipeline::run_sequential()`: Accepts `HookSource` instead of
`phase` and `label_prefix`
- `HookPipeline::spawn_background()`: Renamed from `spawn_detached()` for
consistency
### Documentation Updates
- Updated help text and documentation to clarify `--no-verify` skips all
hooks (both user and project)
- Added "User hooks" section to config documentation with examples
- Added comparison table showing key differences from project hooks
- Added use cases and filtering examples
### Tests
Added comprehensive integration test suite (`tests/integration_tests/user_hooks.rs`)
covering:
- Basic execution of all hook types
- Execution order (user before project)
- No approval required for user hooks
- `--no-verify` skips all hooks
- Failure handling and behavior
- Template variable expansion
- Background hook execution
- Combined user and project hook scenarios
* Refine hooks: document remote_url, consolidate check
- Document remote_url template variable in cli.rs (syncs to docs)
- Move check_any_hook_configured into run_hook_with_filter
- Simplify standalone hook match arms
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update snapshots for symmetric project hook labeling
Project hooks now show "project" prefix (e.g., "Running project pre-merge")
for consistency with user hook labeling.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update docs to reflect user hooks in all hook references
- wt hook intro: Mention both user and project hooks
- --no-verify: Change "skip project hooks" to "skip all hooks"
- User config comment: Clarify verify skips all hooks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add `wt hook show` command to display configured hooks with approval status
Adds a new `show` subcommand to `wt hook` that displays all configured hooks
from both user and project config files with their approval status. Project
hooks show a ❓ indicator when they require approval.
Features:
- Lists hooks organized by type (post-create, post-start, pre-commit, etc.)
- Shows both user-level hooks (run for all repos) and project-specific hooks
- Displays approval status for project hooks (❓ = needs approval)
- Optional `--expanded` flag to show template variables substituted with current context
- Optional hook type filter to show only specific hook types
- Output is displayed through a pager (like git help) when available
Also reorders hook subcommands to put `show` first, making hook inspection
the primary entry point before hook execution commands.
* feat: Add user-level hooks that run for all repositories
Personal hooks in ~/.config/worktrunk/config.toml now run automatically
before project hooks for all worktree operations. User hooks don't require
approval and use the same syntax and template variables as project hooks.
Changes:
- Add user hook support for all hook types (post-create, post-start,
pre-commit, pre-merge, post-merge, pre-remove)
- User hooks run before project hooks in execution order
- Skip all hooks with --no-verify (applies to both user and project)
- Add remote_url template variable for repository-aware filtering
- Update documentation and help text to reflect user hooks
- Refine hook labeling to distinguish "user" vs "project" hooks in output
- Add comprehensive integration tests for user hook functionality
* feat: Add user-level hooks and improve hook infrastructure
User hooks enable personal, repository-agnostic automations while maintaining
team standards via project hooks. Key improvements:
- User hooks defined in ~/.config/worktrunk/config.toml run before project hooks
- User hooks don't require approval (user implicitly approves by defining them)
- Post-start logs now include source prefix to avoid collisions (user/project)
- remote_url template variable added for conditional hook logic
- Hook output labels distinguish user vs project hooks for clarity
- All hooks skipped together via --no-verify flag
- --no-verify help text simplified from "Skip project hooks" to "Skip hooks"
Implementation:
- HookSource enum tracks whether hooks are user or project
- prepare_user_commands() handles user hooks (no approval needed)
- Hook pipeline shows source in output: "Running user pre-merge" vs "Running project pre-merge"
- Background operation names include source to prevent log file collisions
* feat: support SSH URLs and improve branch name escaping
Add support for ssh:// URL format in git remote parsing to handle
SSH URLs alongside existing git@ and https:// formats.
Improve branch name escaping for git config keys by using hex encoding
(-XX format) instead of percent-encoding, ensuring all non-alphanumeric
characters (except . and -) are properly escaped. This handles UTF-8
multi-byte sequences and enables reliable round-trip encoding/decoding
of branch names containing /, _, and other special characters.
* feat: Add user-level hooks and improve hook infrastructure
User hooks enable personal, repository-agnostic automations while maintaining
team standards via project hooks. Key improvements:
- User hooks defined in ~/.config/worktrunk/config.toml run before project hooks
- User hooks don't require approval (user implicitly approves by defining them)
- Post-start logs now include source prefix to avoid collisions (user/project)
- remote_url template variable added for conditional hook logic
- Hook output labels distinguish user vs project hooks for clarity
- All hooks skipped together via --no-verify flag
- --no-verify help text simplified from "Skip project hooks" to "Skip hooks"
Implementation:
- HookSource enum tracks whether hooks are user or project
- prepare_user_commands() handles user hooks (no approval needed)
- Hook pipeline shows source in output: "Running user pre-merge" vs "Running project pre-merge"
- Background operation names include source to prevent log file collisions
* Fix pre-remove hook approval with "Approve at the Gate" pattern
The pre-remove hook execution was incorrectly using auto_trust=true,
bypassing approval prompts. This commit introduces a design pattern
where approval happens exactly once at command entry points:
- Add collect_and_approve_hooks() helper for upfront approval
- wt remove now approves pre-remove hooks before any execution
- Add --force flag to skip approval prompts
- Thread auto_trust parameter through handle_remove_output
The pattern ensures approval happens at the "gate" (command entry),
eliminating error-prone threading of auto_trust through execution layers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Remove pending snapshot file from statusline tests
The .statusline.rs.pending-snap file is no longer needed and should not be committed to version control.
* Implement "Approve at the Gate" pattern for all project hooks
Moves approval prompts to command entry points instead of hook execution.
Commands are now approved once in a batch before any hooks run, with template
expansion so users see actual values (branch names, paths) instead of
placeholders. This eliminates redundant prompts and simplifies the execution
layer by removing the `auto_trust` parameter threading.
Key changes:
- Rename `collect_and_approve_hooks()` to `collect_and_approve_hooks_with_context()`
and add `expand_commands_for_approval()` for showing expanded templates in prompts
- Remove `auto_trust` parameter from hook execution functions
- Simplify `HookSource::Project` enum variant (no longer carries approval state)
- Add gate approval at `wt switch --create`, `wt remove`, `wt step commit/squash`
- Update merge command collection to expand templates before approval batch
- Remove binary `rust_out` artifact and test snapshot file
* Clarify hook command runs on demand for testing and CI
Update documentation and help text to better explain that `wt hook`
runs hooks independently of normal worktree operations. Changes emphasize
the use cases (testing, CI, re-running after failure) rather than
describing it as "manually" running hooks.
Also simplify phrasing around user hooks and remove unnecessary markdown
formatting for consistency.
* Rename approval and hook functions to simpler names
Update function names to be more concise and consistent:
- `collect_and_approve_hooks_with_context()` → `approve_hooks()`
- `handle_standalone_run_hook()` → `run_hook()`
- `handle_standalone_commit()` → `step_commit()`
- `handle_standalone_add_approvals()` → `add_approvals()`
- `handle_standalone_clear_approvals()` → `clear_approvals()`
Also update documentation examples to reference the new names.
* Handle declined command approvals by skipping hooks while continuing operations
When users decline command approval, hooks are now skipped but the operation
proceeds. This applies consistently across all approval gates: merge, commit,
squash, remove, and hook execution.
Previously, declined approvals would still attempt hook execution. Now, the
verify flag is shadowed based on approval result - if declined, verify becomes
false to gate all subsequent hook execution. For explicit hook runs via
`wt hook`, a declined approval returns early since the entire purpose is hook
execution.
This ensures users have clear control: approving runs hooks, declining skips
them but continues the workflow.
* Fix wt hook approval: filter by name and use consistent target branch
Two fixes for `wt hook` command:
1. Pass name_filter to approval so `wt hook pre-merge --name foo` only
prompts for approval of the "foo" hook, not all hooks of that type.
Added `approve_hooks_filtered()` function that accepts optional name
filter parameter.
2. Use current branch as target for pre-merge/post-merge approval prompts
to match execution. Previously approval used default_branch while
execution used current branch, showing misleading command expansions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix wt hook show --expanded to expand {{ target }} for merge/commit hooks
The expand_command_template function now passes hook-specific extra vars:
- PreCommit: target = default branch (for comparison context)
- PreMerge/PostMerge: target = current branch (matches run_hook behavior)
Previously, --expanded would show raw {{ target }} placeholders for these
hook types because extra_vars was always empty.
Also added TODO for pre-remove approval context issue: when removing
another worktree, the approval preview uses current worktree context
instead of the target worktree context.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
4d97ac1f53 | Fix docs for approvals and post-merge timing (#119) | ||
|
|
31b2eb2473 |
Document post-start hook for large asset downloads
Update example config and hooks documentation to clarify that post-start hooks support downloading large assets (images, ML models, binaries) that are too large for git. Add concrete example of asset fetching script in post-start configuration. |
||
|
|
cdf0bc94a5 |
Remove array format for hook commands, standardize on named table
Hook commands now support only two formats: single string or named table. Array format is removed to reduce cognitive load and encourage descriptive command names that appear in output. Migration is straightforward: convert `["cmd1", "cmd2"]` to named table with descriptive keys like `[post-create]` then `install = "cmd1"` and `build = "cmd2"`. Documentation, examples, tests, and serialization logic updated throughout. |
||
|
|
b5ff056368 |
docs: Update config example paths and options
Moves example config files to `dev/` directory for clarity. Updates references to `wt.example.toml` and `config.example.toml` in documentation. Refines `worktree-path` and `approved-commands` examples in generated config. Clarifies `commit` option in `merge` configuration to include squash and rebase. Removes "Shared directory" pattern from `worktree-path` options. |