gwm-cli 1.0.0

git worktree manager — TUI + CLI, native libgit2, per-repo bootstrap
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# Contributing to gwm

Thanks for your interest in `gwm` — a Rust CLI / TUI for managing git worktrees across projects. This file describes the conventions used here. They mirror the ones used in [`fiches-pedagogiques-api-rest`](https://github.com/FlippadTeam/fiches-pedagogiques-api-rest/blob/dev/CONTRIBUTING.md) so the muscle memory is the same.

## Table of contents

- [About this repository]#about-this-repository
- [Project layout]#project-layout
- [Development]#development
- [Testing]#testing
- [Branches]#branches
- [Commits]#commits
- [Labels]#labels
- [Pull Requests]#pull-requests
- [Merge strategy]#merge-strategy
- [Releases]#releases

## About this repository

`gwm` is a single-binary Rust crate (`bin` + reusable `lib`):

- **bin** `gwm` — entry point: dispatches to subcommands (CLI) or opens the TUI.
- **lib** `gwm` — modules (`aliases`, `bootstrap`, `clean`, `cli`, `command_log`, `config`, `config_cli`, `daemon`, `doctor`, `error`, `exec`, `github`, `gitmoji`, `history`, `hooks`, `issue_templates`, `json_api`, `labels`, `launcher`, `lifecycle`, `milestones`, `multiplexer`, `naming`, `pr_templates`, `presets`, `review`, `statusline`, `sync`, `templating`, `trust`, `tui`, `workspace`, `worktree`) exposed publicly so integration tests in `tests/` can drive them directly.

It uses [`git2`](https://docs.rs/git2) (vendored libgit2) for worktree operations and [`ratatui`](https://docs.rs/ratatui) for the TUI.

## Project layout

```
gwm-cli/
├── Cargo.toml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── docs/                 # full documentation tree (README delegates here)
├── examples/
│   ├── gwm.toml.example
│   └── presets/          # embedded `gwm init --preset` bodies
├── src/
│   ├── lib.rs            # public re-exports
│   ├── main.rs           # bin entry point
│   ├── error.rs
│   ├── config.rs         # .gwm.toml parsing
│   ├── config_cli.rs     # `gwm config get/set/...` plumbing
│   ├── naming.rs         # branch / path conventions
│   ├── worktree.rs       # libgit2 worktree ops
│   ├── bootstrap.rs      # copies / guards / shell hooks
│   ├── lifecycle.rs      # [hooks.*] lifecycle phases
│   ├── hooks.rs          # git hook install (commit-msg auto-prefix)
│   ├── trust.rs          # TOFU ledger gating bootstrap (issue #95)
│   ├── doctor.rs         # 8 health checks for `gwm doctor`
│   ├── github.rs         # gh shell-out + issue / PR linking
│   ├── labels.rs         # declarative GitHub label set (issue #81)
│   ├── milestones.rs     # declarative GitHub milestone set (issue #82)
│   ├── launcher.rs       # `l` / `r` TUI launcher resolution (issue #75)
│   ├── multiplexer.rs    # tmux / zellij window+split helpers
│   ├── presets.rs        # `gwm init --preset` stack registry (issue #37)
│   ├── review.rs         # `gwm review <PR#>` (issue #308)
│   ├── sync.rs           # `gwm sync` fetch + rebase / merge
│   ├── exec.rs           # `gwm exec ... -- <cmd>` fleet runner (issue #313)
│   ├── clean.rs          # `gwm clean` artifact reclaim (issue #313)
│   ├── workspace.rs      # multi-repo workspace mode (issue #36)
│   ├── daemon.rs         # JSON-RPC unix-socket daemon (issue #38)
│   ├── json_api.rs       # `--format=json` rendering
│   ├── statusline.rs     # `gwm statusline` daemon consumer (issue #309)
│   ├── history.rs        # destructive-op journal + `gwm undo`
│   ├── command_log.rs    # TUI command log buffer
│   ├── aliases.rs        # [aliases] argv expansion
│   ├── gitmoji.rs        # branch-type → :shortcode: map
│   ├── templating.rs     # placeholder substitution
│   ├── issue_templates.rs# `gwm new` issue templating
│   ├── pr_templates.rs   # `gwm pr` body rendering
│   ├── cli.rs            # clap subcommands
│   └── tui/
│       ├── mod.rs        # event loop
│       ├── app.rs        # state
│       ├── ui.rs         # rendering
│       ├── keymap.rs     # global keymap (rebindable)
│       ├── modal_keymap.rs # per-context modal keys
│       ├── palette.rs    # command palette
│       ├── theme.rs      # theme presets / roles
│       ├── wt_tree.rs    # working-tree file tree
│       ├── commit_graph.rs # recent-commits view
│       └── state/        # per-view state machines
│           ├── async_task.rs
│           ├── command_logs.rs
│           ├── config_panel.rs
│           ├── confirm.rs
│           ├── create_form.rs
│           ├── filter.rs
│           ├── github_fetch.rs
│           ├── link_prompt.rs
│           ├── pty_overlay.rs
│           ├── sidebar.rs
│           └── spinner.rs
└── tests/                # one `*_tests.rs` / `*_integration.rs` per module
    ├── common/           # shared helpers (init_repo, paths_equal)
    ├── config_tests.rs
    ├── naming_tests.rs
    ├── bootstrap_tests.rs
    ├── trust_tests.rs    # ledger load/save/lookup/record/revoke (issue #95)
    ├── worktree_integration.rs
    ├── tui_app_tests.rs
    └── cli_binary.rs     # assert_cmd end-to-end
```

All tests live under `tests/` — no inline `#[cfg(test)] mod tests` blocks inside `src/`.

## Development

### Prerequisites

- Rust toolchain (stable channel, 1.86+ — the MSRV declared in `Cargo.toml`, raised by `tui-term` / `portable-pty` when the PTY overlay landed).
- A C compiler (libgit2 is vendored and built from source on first `cargo build`).

### Build & run

```bash
git clone https://github.com/kbrdn1/gwm-cli.git
cd gwm-cli

cargo build              # builds bin + lib
cargo run -- list        # smoke test the CLI
cargo run                # opens the TUI in the current repo
cargo install --path .   # install gwm into ~/.cargo/bin
```

### Code style

- **Indentation**: 2 spaces (matches `fiches-pedagogiques` convention).
- **Formatter**: `cargo fmt` (project uses `rustfmt` defaults except indent).
- **Linter**: `cargo clippy -- -D warnings`.
- Run `cargo fmt && cargo clippy` before opening a PR.

### Local hooks (recommended, opt-in)

A POSIX `pre-commit` script lives under [`.githooks/`](.githooks/). It is **not installed automatically** — opt in with:

```bash
git config core.hooksPath .githooks
```

Once enabled, two gates run on every `git commit`:

1. **Env-dependent test pre-validation.** If staged `tests/*.rs` hunks reference ambient state (`assert_cmd`, `std::env::var`, `which::which`, `dirs::`, `Command::cargo_bin`), the hook re-runs the suite under a stripped PATH:

   ```bash
   PATH="$(dirname "$(command -v cargo)"):/usr/bin:/bin" cargo test
   ```

   This catches tests that pass in your rich dev shell but fail on a minimal CI runner — the lesson from PR #43 (three CI round-trips before the suite went green).

2. **Local `gwm doctor`.** If staged paths touch `.gwm.toml`, `src/bootstrap.rs`, `src/doctor.rs`, `examples/gwm.toml.example`, or `tests/{bootstrap,doctor}*`, the hook runs `gwm doctor`. Exit codes follow the doctor contract:

   | Exit | Meaning  | Commit behaviour          |
   |:-----|:---------|:--------------------------|
   | `0`  | Clean    | proceeds silently         |
   | `1`  | Warnings | proceeds with advisory    |
   | `2`  | Errors   | **blocked** until resolved |

   If `gwm` is not on `PATH`, the gate prints a skip notice and the commit proceeds — the CI `doctor` job is the safety net.

Both gates short-circuit in O(1) when no staged paths match — contributors who never touch tests or config pay nothing per commit.

**Bypass** for a single commit you know is safe:

```bash
git commit --no-verify
```

CI runs `shellcheck` against the hook and a smoke test on every PR — see the `hook-smoke` job in [`ci.yml`](.github/workflows/ci.yml) — so a broken hook is caught before it reaches you.

## Testing

```bash
cargo test                              # run everything
cargo test --test config_tests          # one file
cargo test --test worktree_integration  # libgit2 integration
cargo test -- --nocapture               # see println from tests
```

### 🔴 TDD is mandatory — non-negotiable

**Test-Driven Development is the primary contribution rule of this repo.** No production code lands without a failing test that pinned the behaviour down first. This is not a guideline, it is a hard merge requirement. PRs that add or change behaviour without tests are sent back, full stop.

The loop is **red → green → refactor**:

1. **Red** — write a failing test capturing the new behaviour (or the bug you are fixing). Run it. It MUST fail for the right reason (assertion mismatch, not a compile error in unrelated code).
2. **Green** — write the minimum production code that turns the test green. No speculative abstractions.
3. **Refactor** — clean up under green tests. Re-run the suite after each refactor step.

Where the test lives:

- **unit logic** (config parsing, naming, kebab, guard regex) → tests in the matching `tests/*_tests.rs` file.
- **disk side effects** (file copy, symlink removal, command exec) → use `tempfile::TempDir`.
- **git operations** → use `tests/common::init_repo()` which gives you a fresh repo on `main` with one commit.
- **public CLI surface** → end-to-end test in `tests/cli_binary.rs` via `assert_cmd`.
- **bootstrap stages** (copy, guard, no-symlink, command) → `tests/bootstrap_tests.rs`.
- **TUI state transitions** → ratatui-free state-machine tests in `tests/tui_app_tests.rs`.

#### Exceptions (must be argued in the PR description)

The bar to skip a test is "observably untestable from the public surface":

- Pure formatting / typo fixes in incidental strings (not asserted anywhere).
- Dependency bumps with no behaviour change (CI green is the test).
- Comment-only changes.

Everything else needs a test. "I tested it manually" is not an exception — codify it as an integration test.

#### Enforcement

- Reviewers run `git log --stat <branch>..HEAD -- tests/`. If the touched module has no companion test diff and the change isn't one of the exceptions above, the PR is blocked.
- The `## Tests` checklist in the PR template is binding. Do not tick `cargo test` unless it actually ran green locally.
- `tests/cli_binary.rs::help_prints_subcommands` is the canary — update it whenever a new CLI subcommand is added.

## Branches

Main branches:

- `main` — what ships. Direct commits allowed for trivial maintenance (typos, docs, dep bumps). Anything user-visible goes through a PR.
- Feature branches: `<type>/#<issue-number>-<short-description>`.

Examples: `feat/#12-tui-search`, `fix/#45-locked-worktree-detection`, `docs/#3-update-readme`.

`gwm` itself uses this exact convention via `gwm create feat 12 tui-search`.

## Commits

Format: `<emoji> <type>(<scope>)<!>: <subject>` (Gitmoji + Conventional Commits).

### Types

| Type       | When                                                |
|:-----------|:----------------------------------------------------|
| `feat`     | new feature                                         |
| `fix`      | bug fix                                             |
| `hotfix`   | critical production bug fix                         |
| `refactor` | code restructuring, no behaviour change             |
| `docs`     | documentation only                                  |
| `test`     | adding / fixing tests                               |
| `perf`     | performance improvement                             |
| `chore`    | repo maintenance (deps, config, scripts)            |
| `ci`       | CI / GitHub Actions changes                         |
| `build`    | build system, Cargo manifest                        |

### Emojis (Gitmoji)

| Emoji | Type       |
|:------|:-----------|
|| feat       |
| 🐛    | fix        |
| 🚑️   | hotfix     |
| 📝    | docs       |
| ♻️    | refactor   |
|| perf       |
|| test       |
| 🔧    | chore      |
| 🏗️    | build      |
| 👷    | ci         |
| 🔥    | chore (remove) |
| ⬆️    | chore (bump deps) |
| 🔒    | security   |

### Scopes (optional, used in this repo)

`config`, `naming`, `worktree`, `bootstrap`, `cli`, `tui`, `tests`, `docs`, `ci`, `structure`.

### Examples

- `✨ feat(tui): add fuzzy search on worktree list`
- `🐛 fix(worktree): handle is_prunable error gracefully`
- `🔧 chore(deps): bump ratatui to 0.29`
- `♻️ refactor(bootstrap): extract guard-matching into pure fn`
- `✅ test(naming): cover unicode descriptions`

### Breaking changes

Suffix the type with `!` and add a `BREAKING CHANGE:` footer:

```
✨ feat(config)!: replace `[[bootstrap.copy]]` with `[[steps]]`

BREAKING CHANGE: configs using the old keys must migrate to the new schema.
```

## Labels

See [`.github/LABELS.md`](.github/LABELS.md) for the full matrix. Quick reference:

- **type**: `feature`, `fix`, `hotfix`, `docs`, `test`, `refactor`, `chore`, `perf`, `ci`, `build`
- **status**: `duplicate`, `invalid`, `wontfix`
- **domain**: `cli`, `tui`, `config`, `worktree`, `bootstrap`, `security`, `dependencies`

## Pull Requests

Before opening a PR:

- [ ] `cargo fmt`
- [ ] `cargo clippy -- -D warnings`
- [ ] `cargo test` (all green)
- [ ] CHANGELOG.md updated under `## [Unreleased]`
- [ ] If the public CLI changed: the `docs/3.cli` section updated (the README is a landing page that delegates to `docs/`)
- [ ] If the config schema changed: `examples/gwm.toml.example` and the `docs/4.configuration` section updated

Use the PR template (`.github/PULL_REQUEST_TEMPLATE.md`).

## Merge strategy

- **Never squash**. Use a regular merge commit so the atomic commit history (with its `feat` / `fix` / `refactor` labels) is preserved on `main`.
- **Never delete the source branch** after merge. Keeps traceability and lets us cherry-pick / revert.

```bash
gh pr merge <num> --merge   # NOT --squash, NOT --delete-branch
```

## Releases

Versioning is SemVer (`MAJOR.MINOR.PATCH`), with `-rc.N` / `-alpha.N` / `-beta.N` suffixes for pre-releases cut from `dev`.

- `MAJOR` → breaking change
- `MINOR` → new feature
- `PATCH` → bug fix
- `-rc.N` / `-alpha.N` / `-beta.N` → release candidate / alpha / beta cut from `dev` before promotion to `main`

What a "breaking change" actually covers — the published 1.0 compatibility
contract (which surfaces are covered by this SemVer promise, which are free to
change in a minor/patch, the MSRV policy, and the deprecation process) — lives
in [Stability & compatibility](docs/6.development/3.stability.md).

### Step 0 — Reconcile open PRs (applies to every tag)

Before any RC or stable cut, run:

```bash
gh pr list --state open
```

Every open PR must be in exactly one of these buckets:

- **In the changeset** — merged into the source branch (`dev` for RCs / stables, `main` for hotfixes) before tagging.
- **Intentionally deferred** — won't make this release, will land in a later one. Note why in the release notes if it was a known candidate.
- **Closed as stale** — superseded, obsolete, or duplicate. Close with a one-line comment pointing at the supersession.

Skipping this step caused the v0.3.0 cut to ship without three queued feature PRs (#51, #52, #53). Recovery required an immediate v0.4.0 promotion 38 minutes later. **Two minutes upfront beats a follow-up release.**

### Pre-release (from `dev`)

When `dev` is ready to be exercised by early adopters before promotion:

1. **Step 0 first** — see above.
2. Stay on `dev` (do not merge to `main` yet).
3. Write per-RC notes in a new file `changelogs/pre-releases/<version>-rc.N.md` — heading `# [<version>-rc.N] - YYYY-MM-DD`, body describing only the **delta** against the previous RC (or against the previous stable, for `rc.1`). One file per RC, not a running log. (See [`changelogs/pre-releases/0.3.0-rc.2.md`]changelogs/pre-releases/0.3.0-rc.2.md for the expected layout.)
4. Add the entry to `CHANGELOG.md`'s `## Past releases > ### Pre-releases` index.
5. Tag: `git tag -a v0.x.y-rc.N -m "v0.x.y-rc.N" && git push --tags`.
6. GitHub Actions (`pre-release.yml`) builds binaries and publishes a **prerelease** (5 targets — Linux x86_64 + aarch64, macOS Intel + Apple Silicon, Windows x86_64). The release body is populated from the per-RC file via `--notes-file changelogs/pre-releases/<version>-rc.N.md` (run `gh release edit <tag> --notes-file <path>` after the workflow if you need to refresh it).
7. Iterate: subsequent candidates are `v0.x.y-rc.2`, `v0.x.y-rc.3`, …

### Stable release (from `main`)

Once the rc is validated and promoted to `main`:

1. **Step 0 first** — see above.
2. Update `Cargo.toml` `version`.
3. Move the `## [Unreleased]` section out of `CHANGELOG.md` into a new file `changelogs/<version>.md` (e.g. `changelogs/0.3.0.md`), rename its heading to `# [<version>] - YYYY-MM-DD`, and add a one-line entry at the bottom of `CHANGELOG.md`'s `## Past releases` index pointing to the new file. `CHANGELOG.md` at the root then only carries the next `## [Unreleased]` section. (See [`changelogs/0.2.0.md`]changelogs/0.2.0.md for the expected layout.)
4. Merge `dev``main` (regular merge, never squash; see [Merge strategy]#merge-strategy).
5. Tag: `git tag -a v0.x.y -m "v0.x.y" && git push --tags`.
6. GitHub Actions (`release.yml`) builds binaries and publishes the stable release. The release body is populated from `changelogs/<version>.md` via `--notes-file` (run `gh release edit v0.x.y --notes-file changelogs/<version>.md` after the workflow if needed).

Triggering matrix:

| Tag pattern              | Workflow         | `prerelease` flag |
|:-------------------------|:-----------------|:------------------|
| `v0.x.y`                 | `release.yml`    | `false`           |
| `v0.x.y-rc.N`            | `pre-release.yml`| `true`            |
| `v0.x.y-alpha.N`         | `pre-release.yml`| `true`            |
| `v0.x.y-beta.N`          | `pre-release.yml`| `true`            |

### Homebrew tap (`brew install kbrdn1/tap/gwm`)

Stable releases automatically refresh [`kbrdn1/homebrew-tap`](https://github.com/kbrdn1/homebrew-tap) (`Formula/gwm.rb`) via the `homebrew-tap-update` job in [`release.yml`](.github/workflows/release.yml). Pre-releases (`-rc.N` / `-alpha.N` / `-beta.N`) are filtered out so `brew install gwm` always tracks the latest stable.

The canonical formula source lives at [`packaging/homebrew/gwm.rb.template`](packaging/homebrew/gwm.rb.template). Edits to the template (new shell completion call, license bump, extra `test do` block) flow to the tap on the next stable release — no manual sync needed.

#### One-time bootstrap (maintainer)

The job needs a fine-grained personal access token (PAT) with `contents: write` scoped to the tap repo. Create it once:

1. Generate a PAT at <https://github.com/settings/personal-access-tokens/new>:
   - **Resource owner**: your user (or the org owning `homebrew-tap`).
   - **Repository access**: select `kbrdn1/homebrew-tap` only.
   - **Permissions**: Contents → **Read and write**. Nothing else.
   - **Expiration**: ≥ 1 year (set a calendar reminder to rotate).
2. Add it as a secret on the `gwm-cli` repo:
   - <https://github.com/kbrdn1/gwm-cli/settings/secrets/actions/new>
   - Name: `HOMEBREW_TAP_TOKEN`. Value: the PAT.
3. Flip `continue-on-error: true` to `false` on the `homebrew-tap-update` job in [`release.yml`].github/workflows/release.yml after the first successful sync — failures should then block the workflow loudly.

#### Re-running after a failed sync

If the job failed (typically: PAT missing or expired) after the GitHub release already shipped, re-drive the tap refresh without re-tagging:

```bash
gh workflow run release.yml --ref <tag>   # e.g. v0.5.0
```

The `workflow_dispatch` path is gated to the same stable-only condition; rc/alpha/beta will skip the tap step automatically.

---

By contributing, you agree your changes are licensed under the MIT License (see `LICENSE.md`).