|
1 | 1 | # Worktrunk Development Guidelines |
2 | 2 |
|
3 | | -> **Note**: This CLAUDE.md is just getting started. More guidelines will be added as patterns emerge. |
| 3 | +## Quick Start |
| 4 | + |
| 5 | +```bash |
| 6 | +cargo run -- hook pre-merge --yes # run all tests + lints (do this before committing) |
| 7 | +``` |
| 8 | + |
| 9 | +For Claude Code web environments, run `task setup-web` first. See [Testing](#testing) for more commands. |
4 | 10 |
|
5 | 11 | ## Project Status |
6 | 12 |
|
@@ -50,40 +56,33 @@ Key skills: |
50 | 56 | ### Running Tests |
51 | 57 |
|
52 | 58 | ```bash |
53 | | -# Run all tests + lints (recommended before committing) |
| 59 | +# All tests + lints (recommended before committing) |
54 | 60 | cargo run -- hook pre-merge --yes |
| 61 | + |
| 62 | +# Tests with coverage report → target/llvm-cov/html/index.html |
| 63 | +task coverage |
55 | 64 | ``` |
56 | 65 |
|
57 | 66 | **For faster iteration:** |
58 | 67 |
|
59 | 68 | ```bash |
60 | | -# Lints only |
61 | | -pre-commit run --all-files |
62 | | - |
63 | | -# Unit tests only |
64 | | -cargo test --lib --bins |
65 | | - |
66 | | -# Integration tests (no shell tests) |
67 | | -cargo test --test integration |
68 | | - |
69 | | -# Integration tests with shell tests (requires bash/zsh/fish) |
70 | | -cargo test --test integration --features shell-integration-tests |
| 69 | +pre-commit run --all-files # lints only |
| 70 | +cargo test --lib --bins # unit tests only |
| 71 | +cargo test --test integration # integration tests (no shell tests) |
| 72 | +cargo test --test integration --features shell-integration-tests # with shell tests |
71 | 73 | ``` |
72 | 74 |
|
73 | 75 | ### Claude Code Web Environment |
74 | 76 |
|
75 | | -When working in Claude Code web, install the task runner and run setup: |
| 77 | +Run `task setup-web` to install required shells (zsh, fish), `gh`, and other dev tools. Install `task` first if needed: |
76 | 78 |
|
77 | 79 | ```bash |
78 | | -# Install task (go-task) - https://taskfile.dev |
79 | 80 | sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/bin |
80 | 81 | export PATH="$HOME/bin:$PATH" |
81 | | - |
82 | | -# Run setup |
83 | 82 | task setup-web |
84 | 83 | ``` |
85 | 84 |
|
86 | | -This installs required shells (zsh, fish) for shell integration tests and builds the project. Also installs `gh` and other dev tools—run this if any command is not found. The permission tests (`test_permission_error_prevents_save`, `test_approval_prompt_permission_error`) automatically skip when running as root, which is common in containerized environments. |
| 85 | +The permission tests (`test_permission_error_prevents_save`, `test_approval_prompt_permission_error`) skip automatically when running as root. |
87 | 86 |
|
88 | 87 | ### Shell/PTY Integration Tests |
89 | 88 |
|
@@ -254,59 +253,27 @@ Examples: `feature-user-post-start-npm.log`, `feature-project-post-start-build.l |
254 | 253 |
|
255 | 254 | The `codecov/patch` CI check enforces coverage on changed lines — respond to failures by writing tests, not by ignoring them. If code is unused, remove it. This includes specialized error handlers for rare cases when falling through to a more general handler is sufficient. |
256 | 255 |
|
257 | | -### Running Coverage Locally |
258 | | - |
259 | | -```bash |
260 | | -task coverage # includes --features shell-integration-tests |
261 | | -# Report: target/llvm-cov/html/index.html |
262 | | -``` |
263 | | - |
264 | | -Install once: `cargo install cargo-llvm-cov` |
265 | | - |
266 | 256 | ### Investigating codecov/patch Failures |
267 | 257 |
|
268 | | -When CI shows a codecov/patch failure, investigate before declaring "ready to merge" — even if the check is marked "not required": |
| 258 | +When CI shows a codecov/patch failure, investigate before declaring "ready to merge": |
269 | 259 |
|
270 | | -1. Identify uncovered lines in your changes: |
271 | | - ```bash |
272 | | - task coverage # run tests, generate coverage |
273 | | - cargo llvm-cov report --show-missing-lines | grep <file> # query the report |
274 | | - git diff main...HEAD -- path/to/file.rs |
275 | | - ``` |
276 | | - |
277 | | -2. For each uncovered function/method you added, either: |
278 | | - - Write a test that exercises it, or |
279 | | - - Document why it's intentionally untested (e.g., error paths requiring external system mocks) |
280 | | - |
281 | | -### How Coverage Works with Integration Tests |
282 | | - |
283 | | -Coverage is collected via `cargo llvm-cov` which instruments the binary. **Subprocess execution IS captured** — when tests spawn `wt` via `assert_cmd_snapshot!`, the instrumented binary writes coverage data to profile files that get merged into the report. |
284 | | - |
285 | | -When investigating uncovered lines: |
286 | | - |
287 | | -1. Run `task coverage` first to see actual coverage % (~92% is normal) |
288 | | -2. Use `cargo llvm-cov report --show-missing-lines | grep <file>` to find specific uncovered lines |
289 | | -3. **Check if tests already exist** for that functionality before writing new ones |
290 | | -4. Remaining uncovered lines are typically: |
291 | | - - Error handling paths requiring mocked git failures |
292 | | - - Edge cases in shell integration states (e.g., running as `git wt`) |
293 | | - - Test assertion code (only executes when tests fail) |
| 260 | +```bash |
| 261 | +task coverage # run tests, generate coverage |
| 262 | +cargo llvm-cov report --show-missing-lines | grep <file> # find uncovered lines |
| 263 | +``` |
294 | 264 |
|
295 | | -Code that only runs on test failure (assertion messages, custom panic handlers) shows as uncovered since tests pass. Keep this code minimal — useful for debugging but a rarely-traveled path. |
| 265 | +For each uncovered function/method, either write a test or document why it's intentionally untested. Integration tests (via `assert_cmd_snapshot!`) do capture subprocess coverage. |
296 | 266 |
|
297 | 267 | ## Benchmarks |
298 | 268 |
|
299 | | -See `benches/CLAUDE.md` for details. |
| 269 | +Benchmarks measure `wt list` performance across worktree counts and repository sizes. |
300 | 270 |
|
301 | 271 | ```bash |
302 | | -# Fast synthetic benchmarks (skip slow ones) |
303 | | -cargo bench --bench list -- --skip cold --skip real |
304 | | - |
305 | | -# Specific benchmark |
306 | | -cargo bench --bench list bench_list_by_worktree_count |
| 272 | +cargo bench --bench list -- --skip cold --skip real # fast synthetic benchmarks |
| 273 | +cargo bench --bench list bench_list_by_worktree_count # specific benchmark |
307 | 274 | ``` |
308 | 275 |
|
309 | | -Real repo benchmarks clone rust-lang/rust (~2-5 min first run, cached thereafter). Skip with `--skip real`. |
| 276 | +Real repo benchmarks clone rust-lang/rust (~2-5 min first run, cached thereafter). Skip with `--skip real`. See `benches/CLAUDE.md` for methodology and adding new benchmarks. |
310 | 277 |
|
311 | 278 | ## JSON Output Format |
312 | 279 |
|
@@ -343,6 +310,44 @@ fn validate_config() { ... } |
343 | 310 |
|
344 | 311 | Never use `#[cfg(test)]` to add test-only convenience methods to library code. Tests should call the real API directly. If tests need helpers, define them in the test module. |
345 | 312 |
|
| 313 | +## Error Handling |
| 314 | + |
| 315 | +Use `anyhow` for error propagation with context: |
| 316 | + |
| 317 | +```rust |
| 318 | +use anyhow::{bail, Context, Result}; |
| 319 | + |
| 320 | +// Prefer .context() for adding helpful error messages |
| 321 | +let data = std::fs::read_to_string(path) |
| 322 | + .context("Failed to read config file")?; |
| 323 | + |
| 324 | +// Use bail! for early returns with formatted errors |
| 325 | +if worktree.is_dirty() { |
| 326 | + bail!("worktree has uncommitted changes"); |
| 327 | +} |
| 328 | +``` |
| 329 | + |
| 330 | +**Patterns:** |
| 331 | + |
| 332 | +- **Use `bail!`** for business logic errors (dirty worktree, missing branch, invalid state) |
| 333 | +- **Use `.context()`** for wrapping I/O and external command failures |
| 334 | +- **Don't `logger.error` before raising** — include context in the error message itself |
| 335 | +- **Let errors propagate** — don't catch and re-raise without adding information |
| 336 | + |
| 337 | +## Adding CLI Commands |
| 338 | + |
| 339 | +CLI commands live in `src/cli/` with implementations in `src/commands/`. |
| 340 | + |
| 341 | +1. **Add subcommand** to `Cli` enum in `src/cli/mod.rs` |
| 342 | +2. **Create command module** in `src/commands/` (e.g., `src/commands/mycommand.rs`) |
| 343 | +3. **Add `after_long_help`** attribute for extended help that syncs to docs |
| 344 | +4. **Run doc sync** after adding help text: |
| 345 | + ```bash |
| 346 | + cargo test --test integration test_command_pages_and_skill_files_are_in_sync |
| 347 | + ``` |
| 348 | + |
| 349 | +Help text in `after_long_help` is the source of truth for `docs/content/{command}.md`. |
| 350 | + |
346 | 351 | ## Accessor Function Naming Conventions |
347 | 352 |
|
348 | 353 | Function prefixes signal return behavior and side effects. |
@@ -370,69 +375,16 @@ Function prefixes signal return behavior and side effects. |
370 | 375 | - Don't use `load_*` for computed values (use bare nouns) |
371 | 376 | - Don't use `get_*` prefix — use bare nouns instead (Rust convention) |
372 | 377 |
|
373 | | -## Repository Caching Strategy |
374 | | - |
375 | | -Most data is stable for the duration of a command. The only things worktrunk modifies are: |
| 378 | +## Repository Caching |
376 | 379 |
|
377 | | -- **Worktree list** — `wt switch --create`, `wt remove` create/remove worktrees |
378 | | -- **Working tree state** — `wt merge` commits, stages files |
379 | | -- **Git config** — `wt config` modifies settings |
380 | | - |
381 | | -Everything else (remote URLs, project config, branch metadata) is read-only. |
382 | | - |
383 | | -### Caching Implementation |
384 | | - |
385 | | -`Repository` holds its cache directly via `Arc<RepoCache>`. Cloning a Repository shares the cache — all clones see the same cached values. |
386 | | - |
387 | | -**Key patterns:** |
388 | | - |
389 | | -- **Command entry points** create Repository via `Repository::current()` or `Repository::at(path)` |
390 | | -- **Parallel tasks** (e.g., `wt list`) clone the Repository, sharing the cache |
391 | | -- **Tests** naturally get isolation since each test creates its own Repository |
392 | | - |
393 | | -**Currently cached:** |
394 | | - |
395 | | -- `git_common_dir` — computed at construction, stored on struct |
396 | | -- `worktree_root()` — per-worktree, keyed by path |
397 | | -- `repo_path()` — derived from git_common_dir and is_bare |
398 | | -- `is_bare()` — git config, doesn't change |
399 | | -- `current_branch()` — per-worktree, keyed by path |
400 | | -- `project_identifier()` — derived from remote URL |
401 | | -- `primary_remote()` — git config, doesn't change |
402 | | -- `primary_remote_url()` — derived from primary_remote, doesn't change |
403 | | -- `default_branch()` — from git config or detection, doesn't change |
404 | | -- `integration_target()` — effective target for integration checks (local default or upstream if ahead) |
405 | | -- `merge_base()` — keyed by (commit1, commit2) pair |
406 | | -- `ahead_behind` — keyed by (base_ref, branch_name), populated by `batch_ahead_behind()` |
407 | | -- `project_config` — loaded from .config/wt.toml |
408 | | - |
409 | | -**Not cached (intentionally):** |
| 380 | +Most data is stable for the duration of a command. `Repository` caches read-only values (remote URLs, config, branch metadata) via `Arc<RepoCache>` — cloning a Repository shares the cache. |
410 | 381 |
|
| 382 | +**Not cached (changes during command execution):** |
411 | 383 | - `is_dirty()` — changes as we stage/commit |
412 | 384 | - `list_worktrees()` — changes as we create/remove worktrees |
413 | 385 |
|
414 | | -### Adding New Cached Methods |
415 | | - |
416 | | -1. Add field to `RepoCache` struct: `field_name: OnceCell<T>` |
417 | | -2. Access via `self.cache.field_name` |
418 | | -3. Return owned values (String, PathBuf, bool) |
| 386 | +When adding new cached methods, see `RepoCache` in `src/git/repository/mod.rs` for patterns (repo-wide via `OnceCell`, per-worktree via `DashMap`). |
419 | 387 |
|
420 | | -```rust |
421 | | -// For repo-wide values (same for all clones) |
422 | | -pub fn cached_value(&self) -> anyhow::Result<String> { |
423 | | - self.cache |
424 | | - .field_name |
425 | | - .get_or_init(|| { /* compute value */ }) |
426 | | - .clone() |
427 | | -} |
| 388 | +## Releases |
428 | 389 |
|
429 | | -// For per-worktree values (different per worktree path) |
430 | | -// Use DashMap for concurrent access |
431 | | -pub fn cached_per_worktree(&self, path: &Path) -> String { |
432 | | - self.cache |
433 | | - .field_name |
434 | | - .entry(path.to_path_buf()) |
435 | | - .or_insert_with(|| { /* compute value */ }) |
436 | | - .clone() |
437 | | -} |
438 | | -``` |
| 390 | +Use the `release` skill for cutting releases. It handles version bumping, changelog generation, crates.io publishing, and GitHub releases. |
0 commit comments