Commit Graph

7 Commits

Author SHA1 Message Date
Maximilian Roos 14580de79c feat(worktree): accept a worktree path wherever a branch is accepted (#3607)
Follow-up to the [`wt remove <path>` discussion on
#3480](https://github.com/max-sixty/worktrunk/pull/3480#issuecomment-5039137116),
widened from that one command to the whole surface. #3480 has since
landed and is merged in here — its duplicate-checkout warning composes
with this: the warning names the shadowed worktrees, and a path is how
you then address one.

## Audit

Verified against the built binary. wt had three answers to "what does
this token mean?":

| Route | `@` `-` `^` | worktree path | `pr:N` |
|---|---|---|---|
| `wt switch` (`resolve_switch_target`) | yes | only if absolute or ≥2
components, and not `--create` | yes |
| `wt remove` (`resolve_worktree_arg`) | yes | any token | no |
| everything else (raw `worktree_for_branch`) | **no** | **no** | no |

Same token, same cwd, two answers:

```console
$ wt remove inner     # ✓ Removed innerbranch worktree & branch
$ wt switch inner     # ✗ No branch named inner
```

And outside switch/remove the shortcuts didn't work at all — `wt step
diff --branch @` was `✗ Branch @ has no worktree`, while `wt config
state marker set --branch @` silently wrote state under the literal key
`@`. Separately, wt prints paths as `~/…` but wouldn't accept that form
back.

## Change

One canonicalizer in the lib, `Repository::resolve_worktree`, absorbing
the path fallback that lived in the bin crate's `resolve_worktree_arg`
(now deleted). Resolution order is documented once, on that function:
`@`, then `-`/`^`, then a branch with a worktree, then a path naming a
registered worktree, then the branch alone.

**Branch-first, everywhere.** A directory never shadows a branch that
shares its name; a path answers only what a branch cannot — a detached
worktree, or one of two checkouts of the same branch (#3480's case). The
`looks_like_path` shape gate is gone, so a single-component path
resolves like any other.

Two shapes cover what callers need: `require_worktree` for commands that
need a worktree to operate in, `require_selected_branch` for arguments
that key by branch. The merge/rebase target validators fall through to
the same path lookup, so a target can be named by the worktree it's
checked out in.

Routed through it: `switch` (including `--base`), `remove`, `step commit
--branch`, `step diff --branch` and its target, `step copy-ignored
--from`/`--to`, `step promote`, `step relocate`, `config state --branch`
(9 sites), and `merge` / `step rebase` / `step squash` / `step push`
targets.

`resolve_input_path` — already documented as the one resolution point
for user-supplied paths — now expands a leading `~`, so the tilde form
worktrunk prints is a form it reads back. `~user` stays literal; wt
doesn't reimplement that shell feature.

## Documentation

A path is an alias, not a second addressing scheme, so it is stated once
rather than on every argument: one paragraph in `wt switch`'s help and
one sentence on the addressing line in `worktrunk.md`. Argument
descriptions still read as branches. The two exceptions are the
arguments whose descriptions are already catalogues of accepted forms —
`wt switch`'s (`Branch, worktree path, shortcut, or PR/MR URL`) and `wt
remove`'s, which has named the path since before this branch. The
Worktree Model section of `CLAUDE.md` records which way to document it,
so the next argument doesn't grow its own copy.

## Two silent no-ops fixed along the way

- `wt step relocate <unmatched>` matched arguments against branch names
by string equality, so a typo filtered everything out and the empty
result rendered as `○ All worktrees are at expected paths` — a success
message for work that never happened. Every way an argument can fail to
land on a relocatable worktree now errors, including the detached and
prunable cases the new path route makes reachable.
- A selector matching nothing was reported as a branch without a
worktree, hinting `wt switch <token>` — which creates a worktree only
when the branch exists, so for a mistyped path it would just fail again.
`WorktreeSelectorNotFound` now says `No branch or worktree named X`; a
branch that genuinely exists without a checkout keeps the create hint.

## Testing

Full gate green: 4596 tests, lints, docs sync, `--features
shell-integration-tests` clippy. `codecov/patch` is 99.25% of diff hit
against a 97.93% target. New coverage:

- Unit: branch-and-path equivalence, branch-beats-same-named-directory,
detached-by-path (and its `require_selected_branch` refusal), shortcuts
never treated as paths, branch-only fallthrough, and the two distinct
not-found errors. Plus `expand_tilde` round-tripping
`format_path_for_display`.
- Integration: `switch` by relative/single-component/absolute/tilde
path, `--base` by path, `step diff --branch` by path and `@` (asserted
equal to the by-branch output), `config state --branch` set via `@` and
read via the worktree path, and both new relocate errors.

`wt remove`'s resolution is unchanged — it already had this rule; it now
shares the implementation. The 106-test `remove::` suite is untouched
and green.

- Integration: `wt step push <worktree-path>` (the
`require_target_branch` half of the target fallback), and `wt step
relocate` against a prunable worktree.

One diff line is unhit: `expand_tilde`'s fallback when `home_dir()`
returns `None`, which has no deterministic trigger. The `@`-resolution
backstop in `resolve_worktree` is untested for the same reason — no CLI
route reaches it — so it kept its original `match` arm rather than being
re-indented into the diff.

## Left out

`wt config state default-branch set` and `previous-branch set` take a
branch name as a *value to store* rather than a selector, so they still
take it literally.

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

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 09:10:46 -07:00
Worktrunk Bot bcd1ffdfde feat(worktree): warn when a branch is checked out in multiple worktrees (#3480)
## Problem

`git worktree add --force <path> <branch>` bypasses git's "already used
by worktree" guard, so the same branch can be live in two worktrees at
once — breaking worktrunk's branch ⇔ worktree bijection. worktrunk never
creates that state itself, but once it exists
[`worktree_for_branch`](https://github.com/max-sixty/worktrunk/blob/cfd7fe469/src/git/repository/worktrees.rs#L64)
silently resolved to whichever worktree git listed first (roughly
creation order), and that choice flowed into every resolution path — `wt
switch`, `wt push`, `wt step diff`, `wt context`, the picker — with no
warning, error, or way to name the shadowed worktree.

This implements the **detect-and-warn** option from #3392: make the
invisible choice visible without changing resolution semantics.

## Solution

- `worktree_for_branch` now collects **all** worktrees on the branch via
a pure `worktree_paths_for_branch` helper; when more than one exists it
warns once (per branch, deduplicated per process) and still resolves to
the first — semantics are unchanged.
- The warning names every path and points at a concrete fix:

```
▲ Branch feature is checked out in 2 worktrees; wt uses the first:
   ┃ ~/repo.feature
   ┃ ~/repo.feature-dup
↳ To drop a duplicate, run git worktree remove ~/repo.feature-dup
```

Detection is separated from emission so the pure list logic is
unit-testable and the ambiguity check stays cheap — `list_worktrees()`
already holds the full list.

## Testing

- Unit test `test_worktree_paths_for_branch_detects_duplicates` — the
pure helper returns both paths in git's listing order for a duplicated
branch, one path for a unique branch, and none for an absent one.
- Integration test `test_step_diff_duplicate_branch_warns` — creates the
duplicate with `git worktree add --force`, runs `wt step diff
--branch=feature`, and snapshots the rendered warning.

The warning only fires when a duplicate exists (which no prior test sets
up), so existing snapshots are unaffected.

---
Closes #3392 — automated triage

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-07-25 13:47:19 -07:00
Maximilian Roos 0e8887fd94 Reject empty branch-name CLI arguments at the parse boundary (#3179)
`wt step diff --branch=` (an empty `--branch` value) produced a garbled
diagnostic, because the empty string was accepted as a branch name and
flowed downstream into worktree lookup:

```
✗ Branch  has no worktree
↳ To create a worktree, run wt switch ''
```

The same applied to every branch-name argument — `--branch`, `--target`,
`--base`, `wt switch`, `wt merge`, `wt remove`, `wt config state … set`,
and so on — not just `step diff`.

This validates once at the CLI edge: a `non_empty_branch` clap
`value_parser` (matching the existing `parse_vars_assignment` idiom)
rejects empty and whitespace-only values without mutating
otherwise-valid input. It's wired onto all 25 branch-name arguments. `wt
step diff --branch=` now exits 2 with a standard usage error:

```
error: invalid value '' for '--branch <BRANCH>': branch name cannot be empty
```

A real missing branch (`--branch=lonely`) still gets the proper `Branch
lonely has no worktree` diagnostic — that path is unchanged. Out of
scope: the parser rejects empty/blank but not other always-invalid
refnames (`..`, embedded spaces), which still fail later with git's own
message.

Covered by a unit test (`non_empty_branch_rejects_blank`) and an
integration snapshot test (`test_step_diff_branch_empty`); the
branch-argument convention in `src/commands/CLAUDE.md` is updated so
future args follow the pattern.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:51:39 -07:00
Worktrunk Bot b41e210f82 feat(step): add --branch arg to wt step diff (#2995)
## Problem

`wt step diff` could only diff the current worktree. #2994 (part of the
effort to extend `--branch` to more commands) asks for a `--branch` flag
so the diff can target another worktree's branch without leaving the
current one.

## Solution

Add `-b/--branch` to `wt step diff`, mirroring the existing flag on `wt
step commit`. When provided, the repo is rooted at that branch's
worktree (via `worktree_for_branch` + `Repository::at`) so both the diff
and its target/merge-base resolution operate there. The branch must have
a checked-out worktree; otherwise it errors with `no worktree for branch
'<b>'`.

## Testing

Two integration tests in `tests/integration_tests/step_diff.rs`:

- `test_step_diff_branch_arg` — runs `wt step diff --branch feature`
from the main worktree and asserts the output matches
`test_step_diff_committed_changes` (the same diff run from inside the
feature worktree).
- `test_step_diff_branch_no_worktree` — asserts the error path for a
branch without a worktree.

Docs regenerated via `test_docs_are_in_sync`; `cargo clippy
--all-targets` and `cargo fmt --check` are clean. The only failing tests
locally are the `case_4` nushell shell-wrapper tests, which fail because
`nu` isn't installed in this sandbox — unrelated to this change.

---
Closes #2994 — automated triage

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-06 18:50:21 -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
worktrunk-bot 1afa9c230b refactor: rename get_* functions to bare nouns (#1586) 2026-03-17 08:00:16 -07: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