From d87a30bb29f54557799f4998cd30c24067bcf0c4 Mon Sep 17 00:00:00 2001 From: Stephen Way Date: Wed, 13 May 2026 12:48:46 -0700 Subject: [PATCH] docs: CLAUDE.md, CONTRIBUTING.md, CHANGELOG.md, docs/ * CLAUDE.md: project layout, key conventions, where to look first. Captures the non-obvious things a future session needs. * CONTRIBUTING.md: build/test workflow, how to add a subcommand (concrete walkthrough), code style. * CHANGELOG.md: history. 0.1.0 entry covers initial feature set; Unreleased captures stability + optimization batch. * docs/architecture.md: module graph, layering rules, the HTTP funnel, pager + SIGINT, repo resolution, test strategy. * docs/jq.md: --jq syntax cheatsheet (dot paths, brackets, negative indices, pipes, what's not supported). * docs/troubleshooting.md: keychain re-prompts, debug logging, pager opt-out, alias precedence, hook bypass, common 401s. * README.md: links into docs/ and updates binary size to 4 MB. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 63 ++++++++++++++++++ CLAUDE.md | 83 ++++++++++++++++++++++++ CONTRIBUTING.md | 77 ++++++++++++++++++++++ README.md | 12 +++- docs/README.md | 11 ++++ docs/architecture.md | 138 ++++++++++++++++++++++++++++++++++++++++ docs/jq.md | 76 ++++++++++++++++++++++ docs/troubleshooting.md | 132 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 591 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md create mode 100644 CLAUDE.md create mode 100644 CONTRIBUTING.md create mode 100644 docs/README.md create mode 100644 docs/architecture.md create mode 100644 docs/jq.md create mode 100644 docs/troubleshooting.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..705338c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,63 @@ +# Changelog + +All notable changes will be recorded here. The format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- `tokio::signal::ctrl_c()` race in `cli::run` so the pager guard drops + cleanly on SIGINT. +- 9 wiremock-backed HTTP client integration tests covering retry + behavior, header forwarding, pagination, and panic-free error paths. +- `Client::for_base_url` test constructor pointing at an arbitrary URL. + +### Changed + +- Trimmed dependencies (no more `indicatif`, `futures-util`, + `is-terminal`, `textwrap`, `tempfile`). Dropped reqwest features we + don't use (`stream`, `brotli`). Release profile now uses `lto = "fat"` + and `panic = "abort"`. +- HTTP retry loop builds the request once and clones via + `reqwest::Request::try_clone` per attempt (was rebuilding the full + RequestBuilder each time). +- Binary size: 5.94 MB → 4.15 MB stripped (-30%). + +### Fixed + +- Removed the unsafe `std::env::set_var("FJ_NO_PAGER")` from dispatch. + The `--no-pager` flag is now threaded into `pager::maybe_start`. +- Replaced the panicking `.expect("token contains invalid header chars")` + in `auth_headers` with a typed error that tells the user how to + recover. + +## 0.1.0 — 2026-05-13 + +Initial release. Multi-host Forgejo CLI with feature parity to `gh` +across the surface Forgejo exposes. Commands: + +- `auth`: login, status, logout, list, switch, token, refresh, setup-git +- `repo`: list, view, clone, create, fork, sync, edit, rename, archive, + unarchive, delete, branches, topics, mirror, mirror-sync +- `issue`: list, view, create, edit, close, reopen, comment, develop +- `pr`: list, view, create, edit, diff, commits, files, checks, ready, + review, status, checkout, merge, close, reopen +- `release`: list, view, create, edit, delete, upload, download +- `label`, `run`, `secret`, `variable`, `search`, `browse`, `status`, + `org`, `ssh-key`, `gpg-key`, `alias`, `config`, `protect`, `hook`, + `extension`, `gist`, `api`, `completion`, `man` + +Other highlights: + +- Repo auto-detection from `upstream` / `origin` git remote. +- `--web` flag on all list/view subcommands. +- `$EDITOR` integration for body inputs. +- `fj api` with `-H`, `-X`, `-f`, `-F`, `--paginate`, `--include`, + `--silent`, `--jq` (dot-paths, `[N]`/`[-N]`, pipes). +- `--debug` / `FJ_DEBUG` request logging. +- Tokens in the OS keychain. +- Pager via `dup2` redirect to `$FJ_PAGER` / `$PAGER` / `less -FRX`. +- Pre-push hook running fmt, clippy `-D warnings`, tests, and release + build before any push. Live API smoke gated on `FJ_E2E=1`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8cd330f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,83 @@ +# fj — project notes for Claude + +`fj` is a Forgejo CLI, in the spirit of GitHub's `gh`. Multi-host, tokens +in the OS keychain, ~10k LOC Rust with full feature parity to `gh` for +the surface Forgejo exposes. + +## Layout + +``` +src/ + main.rs entry: alias expansion, fj- plugin dispatch + cli/ clap subcommand tree + mod.rs top-level Cli + dispatch + Ctrl+C race + pager + context.rs RepoFlag + resolve_repo (`-R` or git-remote detect) + editor.rs $EDITOR + body resolution helpers + web.rs cross-platform `--web` opener + .rs one file per command group (auth, repo, pr, …) + api/ typed wrappers around /api/v1 + mod.rs split_repo + shared serde helpers + .rs one file per resource (issue, pull, release, …) + client/ HTTP client + mod.rs Client, retry-on-5xx, debug logging, headers + pagination.rs Page + Link header parser + error.rs ApiError + integration_tests wiremock-based tests + config/ hosts.toml shape + load/save + auth/ OS keychain wrapper + git/ git invocation + remote URL parser + output/ tables, colors, relative_time, state_pill, pager +hooks/pre-push local CI gate +scripts/ install-hooks.sh, e2e-smoke.sh +docs/ architecture, jq syntax, troubleshooting +``` + +## Build / test / hook + +- `cargo build --release` produces a ~4 MB binary at `target/release/fj`. +- `cargo test --all` runs 60 tests (51 unit + 9 wiremock integration). +- `cargo clippy --all-targets --all-features -- -D warnings` is the lint + bar. CI (the pre-push hook) treats warnings as errors. +- `./scripts/install-hooks.sh` symlinks `hooks/pre-push` into `.git/hooks`. +- `FJ_E2E=1` enables the live API smoke against `stephen/fj-cli-test`. +- `FJ_SKIP_PREPUSH=1` bypasses the hook for genuine emergencies only. + +## Key conventions + +- Every repo-scoped subcommand accepts `-R/--repo` AND auto-detects from + the git remote (`upstream` then `origin`). The resolver is + `cli::context::resolve_repo`. +- Bodies (`--body`) support: an inline string, `-` for stdin, or omit to + open `$EDITOR`. See `cli::editor`. +- Lists with `--limit > 50` transparently follow `Link: rel=next` via + `Client::get_all`. Per-page cap is the Forgejo default (50). +- The simple jq projector lives in `cli::api::extract_path`. Supports + `.field`, `.0`, `.[0]`, `[-1]`, and `|` pipes. See `docs/jq.md`. +- All commands respect `--host` / `FJ_HOST`, `--debug` / `FJ_DEBUG`, + `--no-pager`, `FJ_PAGER` / `PAGER`. + +## Things to remember + +- Tokens are stored in the OS keychain (service `fj`, key = hostname). + Never serialized to disk. Re-prompts after rebuilds are expected on + macOS because the binary hash changes; click "Always Allow." +- `cargo test` builds a separate test binary with a different hash, so + it gets its own keychain prompt. Tests that need keychain access are + the wiremock-backed ones in `src/client/integration_tests.rs`, which + bypass the keychain via `Client::for_base_url`. +- The repo URL in `Cargo.toml` is `https://rasterhub.com/rasterstate/fj`. +- Commit author is `Stephen Way `. The user has + a CLAUDE.md preference: no em-dashes, terse responses, no preamble. +- MSRV is 1.82 (we use `Option::is_none_or`). + +## Where to look first + +- Adding a new subcommand: copy an existing `cli/.rs` (e.g. `label`). + Wire into `cli/mod.rs` (`Command` enum, `dispatch`) and `main.rs` + (`KNOWN_SUBCOMMANDS`). Typed API wrapper in `api/.rs`. +- Adding a new Forgejo endpoint: `api/.rs` is the right home. +- Touching HTTP behavior: `client/mod.rs::request_with_headers` is the + one spot all requests funnel through. Don't add a parallel code path. +- Test a new API surface: add to `src/client/integration_tests.rs` + rather than `tests/` (this is a binary crate, no `[lib]` target, so + integration tests live inline). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2472cf2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,77 @@ +# Contributing to fj + +## Setup + +```sh +git clone ssh://git@rasterhub.com:2222/rasterstate/fj.git +cd fj +./scripts/install-hooks.sh # symlinks hooks/pre-push into .git/hooks +cargo build --release # 4 MB binary at target/release/fj +``` + +The hook gates every push on fmt, clippy (warnings as errors), the full +test suite, and a release build. Set `FJ_E2E=1` to additionally run the +live API smoke (requires `fj auth login` already done). + +## Workflow + +1. Branch off `main`. +2. Make changes. Keep commits focused. +3. `cargo fmt --all` before committing. +4. The pre-push hook runs the full bar; if it fails, fix the issue + rather than passing `FJ_SKIP_PREPUSH=1` unless you genuinely need + to bypass. + +## Testing + +- 60 tests live in the source tree (51 unit + 9 wiremock integration). +- Unit tests stay close to the code they cover, in inline `#[cfg(test)]` + modules. +- HTTP integration tests live in `src/client/integration_tests.rs` and + use a wiremock server. Construct test clients via + `Client::for_base_url(base, token)`. +- Live API tests live in `scripts/e2e-smoke.sh` and exercise read-only + operations against the configured Forgejo. Toggle with `FJ_E2E=1`. + +## Adding a subcommand + +Concrete example: adding `fj sticker list`. + +1. `src/api/sticker.rs` — typed API wrappers and request/response + structs. Use the `serde_util::deserialize_null_to_default` helper + for fields that Forgejo returns as `null` when empty. +2. `src/cli/sticker.rs` — clap `Args` / `Subcommand` definitions and + the `run` dispatch. +3. `src/api/mod.rs` — `pub mod sticker;` +4. `src/cli/mod.rs` — `pub mod sticker;`, add to `Command`, add the + dispatch arm in `dispatch()`. +5. `src/main.rs` — add `"sticker"` to `KNOWN_SUBCOMMANDS` so plugin + discovery doesn't shadow it. +6. Add inline `#[cfg(test)] mod tests` for any non-trivial pure code. +7. If you touched HTTP behavior, add a wiremock test in + `src/client/integration_tests.rs`. + +## Code style + +- See `CLAUDE.md` for in-tree conventions. +- One small line of comment max per non-obvious thing. Don't narrate + what code does, only why. +- No `.unwrap()` / `.expect()` in non-test code paths. Use `?` and + `anyhow::Error::context` to attach actionable messages. +- Prefer `Result` over panics. The one exception is `unreachable!()` + for genuinely impossible match arms, but flag it in review. + +## Releasing + +```sh +cargo build --release +./target/release/fj completion zsh > ~/.zfunc/_fj +./target/release/fj man -o ./man +``` + +No cargo-dist or Homebrew tap yet. Binary lives at `target/release/fj`; +symlink into a user-level bin dir. + +## License + +MIT. By contributing you agree to the same license. diff --git a/README.md b/README.md index 7d6fcad..8ba0c5a 100644 --- a/README.md +++ b/README.md @@ -112,11 +112,21 @@ fj api /repos/migrate --input '{"clone_addr":"...","repo_name":"x","repo_owner": ## Building ```sh -cargo build --release # 5-6 MB stripped binary at target/release/fj +cargo build --release # ~4 MB stripped binary at target/release/fj ./target/release/fj completion zsh > ~/.zfunc/_fj ./target/release/fj man -o ~/man/man1 ``` +## Documentation + +- [`docs/architecture.md`](docs/architecture.md) — module graph, HTTP + funnel, pager + SIGINT, repo resolution +- [`docs/jq.md`](docs/jq.md) — `fj api --jq` projection syntax +- [`docs/troubleshooting.md`](docs/troubleshooting.md) — keychain + prompts, 401s, hook bypass, pager opt-out, alias precedence +- [`CONTRIBUTING.md`](CONTRIBUTING.md) — build / test / release workflow +- [`CHANGELOG.md`](CHANGELOG.md) — release notes + ## License MIT diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..07708b0 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,11 @@ +# `fj` documentation + +- [`architecture.md`](architecture.md) — module graph, layering rules, + the HTTP funnel, pager + SIGINT, repo resolution, test strategy. +- [`jq.md`](jq.md) — the `fj api --jq` syntax. Dot paths, brackets, + negative indices, pipes. +- [`troubleshooting.md`](troubleshooting.md) — keychain prompts, hangs, + 401 errors, `--debug`, pager opt-out, alias precedence, hook bypass. + +For build/test workflow see [`../CONTRIBUTING.md`](../CONTRIBUTING.md). +For project conventions see [`../CLAUDE.md`](../CLAUDE.md). diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..38e7f26 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,138 @@ +# Architecture + +`fj` is a thin Rust CLI over Forgejo's `/api/v1`. The whole binary is +~10k LOC and shapes itself like `gh`: clap-parsed subcommands, one +typed wrapper per API resource, one HTTP client funnel for everything. + +## Module graph + +``` + ┌─────────────┐ + │ main.rs │ alias expansion + fj- plugin dispatch + └──────┬──────┘ + │ + ┌──────▼──────┐ + │ cli::run │ Ctrl+C race + pager guard + └──────┬──────┘ + │ + ┌─────────────┼────────────────────┐ + │ │ │ + ┌────▼─────┐ ┌─────▼─────┐ ┌──────▼──────┐ + │ cli::auth│ │ cli::repo │ ... │ cli::api │ raw escape hatch + └────┬─────┘ └─────┬─────┘ └──────┬──────┘ + │ │ │ + │ ┌─────▼─────┐ │ + │ │ context:: │ -R or git-remote autodetect + │ │resolve_repo│ + │ └─────┬─────┘ │ + │ │ │ + │ ┌─────▼─────┐ │ + └──────►│ api::* │◄─────────────┘ + │ typed │ + │ wrappers │ + └─────┬─────┘ + │ + ┌─────▼─────┐ + │ client │ one funnel: auth headers, retry, pagination, + │ │ debug logging, custom headers + └─────┬─────┘ + │ + ┌─────▼─────┐ + │ reqwest │ rustls-tls, JSON, multipart + └───────────┘ +``` + +## Layering rules + +- `cli::*` handles flags, argument validation, output formatting. It + never makes HTTP calls directly: it calls `api::*`. +- `api::*` is one file per Forgejo resource. Each function takes a + `&Client` and returns typed `Result`. +- `client::*` is the only place that touches `reqwest`. All HTTP goes + through `Client::request_with_headers`, which handles auth headers, + retries, debug logging, and custom header merging. There is no + parallel HTTP path (other than the multipart asset upload in + `api::release::upload_asset`, which goes through `client.http()` but + still uses `client.token()` for auth). +- `config::*` is the on-disk shape of `hosts.toml`. Tokens are NOT + stored here. They live in the OS keychain via `auth::*`. +- `git::*` shells out to the `git` binary. The only place we call git. + +## Repo resolution + +Every command that operates on a specific repo calls +`cli::context::resolve_repo(explicit_repo, explicit_host)`. The +algorithm: + +1. If `-R/--repo` was given, parse it as `owner/name`. +2. Otherwise call `git remote -v` and pick the first remote that parses + as a Forgejo URL. Preference order is `upstream`, then `origin`, + then any other. +3. The host comes from `--host`, then the detected remote's host, then + the configured default host in `hosts.toml`. + +This is the magic that lets `fj pr list` work without any flags inside +a clone. + +## HTTP retry + +In `client::request_with_headers`: + +- The request is built once and stored as `reqwest::Request`. +- Idempotent methods (GET, HEAD, OPTIONS, PUT, DELETE) get 3 attempts. +- POST and PATCH get 1 attempt — they're never retried. +- Backoff is `200ms * 2^attempt` between retries. +- On 5xx or transport error, we retry. On any other status, we return. + +## Auto-pagination + +`Client::get_all(path, query, total_limit)` follows `Link: rel=next` +until either `total_limit` items have accumulated or there are no more +pages. List commands switch to this path automatically when the user +asks for `--limit > 50` (Forgejo's per-page cap). + +## Pager + +When `cli::run` dispatches a command whose output can run long (list, +view, diff, search, status, api), it calls +`output::pager::maybe_start(force_disabled)`. That function: + +1. Bails if stdout isn't a TTY or `--no-pager` / `FJ_NO_PAGER` is set. +2. Spawns `$FJ_PAGER` / `$PAGER` / `less -FRX`. +3. `dup`s our stdout fd (saving it for restore) then `dup2`s the pager + child's stdin fd onto stdout (fd 1). +4. Returns a `PagerGuard` whose `Drop` impl flushes stdout, restores + the saved fd, closes the saved fd, and waits on the child. + +This means the rest of `fj` keeps using `println!`/`print!` unmodified +even though it's now writing to the pager. + +## SIGINT + +`cli::run` `tokio::select!`s the command future against +`tokio::signal::ctrl_c()`. On SIGINT the command future is dropped, +which propagates to the `_pager` binding's `Drop` and tears down the +pager cleanly. + +## Aliases and extensions + +`main.rs` runs two passes before clap: + +1. **Alias expansion** via `cli::alias::expand_argv`. Reads + `aliases.toml`. If `argv[1]` matches a defined alias, replaces it + with the tokenized expansion. +2. **Plugin dispatch** via `main::detect_plugin`. If `argv[1]` isn't a + known subcommand AND `fj-` is on PATH, exec it. + +Only if neither pass triggers do we hand off to clap. + +## Test strategy + +- **Unit tests** stay inline next to the code they cover. Pure + functions only (no I/O). +- **Integration tests** for HTTP behavior live in + `src/client/integration_tests.rs` and use a wiremock server. + Construct test clients via `Client::for_base_url`. +- **Live E2E** lives in `scripts/e2e-smoke.sh`, gated on `FJ_E2E=1` + in the pre-push hook. It hits the real Forgejo at `rasterhub.com` + against `stephen/fj-cli-test`. diff --git a/docs/jq.md b/docs/jq.md new file mode 100644 index 0000000..3928afb --- /dev/null +++ b/docs/jq.md @@ -0,0 +1,76 @@ +# `fj api --jq` syntax + +`fj api` ships a small jq-flavored JSON projector. It's not a full jq +implementation; it covers the 95% of cases gh users use `--jq` for. + +If you need anything more complex, pipe the raw JSON through real `jq`: + +```sh +fj api /repos/foo/bar | jq '.' +``` + +## Stages + +Expressions are split on `|` into stages. Each stage runs against the +output of the previous stage. + +``` +fj api /repos/foo/bar | head +# vs +fj api /repos/foo/bar -q '.owner | .login' +``` + +## Path segments + +Within a stage, dot-separated segments traverse the JSON tree. Each +segment is one of: + +| Segment | Meaning | +| ------------ | --------------------------------------------- | +| `.field` | Object field lookup | +| `.0` | Array index (positional) | +| `.[0]` | Same as `.0`, bracket form | +| `.[-1]` | Last element of an array | +| `.[-2]` | Second-to-last element | + +Bracket form and dot form are interchangeable: `.data.0.name` and +`.data[0].name` mean the same thing. + +## Examples + +Given: + +```json +{ + "data": [ + {"id": 1, "owner": {"login": "alice"}}, + {"id": 2, "owner": {"login": "bob"}} + ], + "total": 2 +} +``` + +| Query | Output | +| ------------------------------ | ------------------ | +| `.total` | `2` | +| `.data.0.id` | `1` | +| `.data[0].owner.login` | `"alice"` | +| `.data[-1].owner.login` | `"bob"` | +| `.data | .[0]` | first element | +| `.data[0] | .owner | .login` | `"alice"` | + +## Errors + +- A missing field returns a hard error with the field name. +- An out-of-range index returns a hard error with the index. +- Negative indices that overshoot (e.g. `[-5]` on a 3-element array) + also error. + +## Not supported + +The following are intentionally NOT implemented; reach for real jq: + +- Filters like `.[] | select(.state == "open")` +- Functions: `map`, `length`, `keys`, etc. +- String interpolation +- Multiple outputs from a single expression diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..046b6cc --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,132 @@ +# Troubleshooting + +## macOS keeps prompting for keychain access + +After every `cargo build --release` the binary's hash changes, so the +macOS keychain treats it as a new application and re-prompts. Click +"Always Allow" once and it'll stick until the next rebuild. + +For automated test environments, see +`src/client/integration_tests.rs` — those tests use +`Client::for_base_url` to bypass the keychain entirely. + +## `fj` hangs on the first command after a rebuild + +Same root cause as above. macOS is prompting for keychain access in a +modal dialog you can't see (the binary doesn't have a TTY). Bring the +Keychain Access prompt to the foreground and approve it, or run +`fj auth status` from a regular terminal first to surface the prompt. + +## `error: HTTP 401 ... authentication failed` + +The stored token is invalid or revoked. Re-authenticate: + +```sh +fj auth refresh # re-verify the existing token +fj auth refresh --token NEW # replace the stored token +fj auth login --host # full re-login (overwrites) +``` + +## I want to see the raw HTTP requests fj is making + +```sh +fj --debug pr list -R foo/bar +# or +FJ_DEBUG=1 fj pr list -R foo/bar +``` + +You'll get `→ METHOD URL?query` and `← STATUS url` for every request, +plus a 200-character preview of the request body if one was sent. + +## A command is hanging in my CI pipeline + +CI runners often don't have an `$EDITOR`. `fj issue create` and friends +will hang waiting for `$EDITOR` to return if you omit `--body`. + +- Pass `--body "text"` explicitly. +- Or pass `--body -` to read from stdin. +- Set `$VISUAL` or `$EDITOR` to a non-interactive program. + +## The pager doesn't exit / output gets paged when I don't want it + +```sh +fj --no-pager repo list # opt out per-command +FJ_NO_PAGER=1 fj repo list # opt out in your shell +``` + +The default pager is `less -FRX`, which exits when output fits one +screen. Override with `$FJ_PAGER` or `$PAGER`: + +```sh +FJ_PAGER='less -R' fj repo list # don't auto-quit +FJ_PAGER=cat fj repo list # disable paging +``` + +## `error: unexpected argument '-R' found` + +Some `fj repo` subcommands take a positional `repo` arg AND accept `-R` +(for symmetry with the rest of the CLI). If you see this on an older +build, update to a build after commit `eb716ee`. All commands now accept +both forms. + +## `-R` works in some shells, not in others (zsh globbing) + +If `fj api /repos/foo` returns a "no matches found" error before fj +even runs, you've hit shell globbing on `/`. Quote the path: + +```sh +fj api '/repos/foo/bar' +``` + +## Aliases aren't expanding + +Aliases are looked up before clap parses, but only the FIRST positional +is matched. So: + +```sh +fj alias set co "pr checkout" +fj co 42 # works: co → pr checkout +fj --host other.host co 42 # works: --host is parsed by clap after expansion +fj help co # does NOT expand co; "help" is a builtin +``` + +If your alias name collides with a real subcommand, the real one wins. +Aliases never shadow built-in commands. + +## Extension `fj-foo` isn't being found + +`fj` looks for `fj-` on PATH only when `` isn't a known +subcommand. The list of known subcommands lives in +`src/main.rs::KNOWN_SUBCOMMANDS`. If your plugin name collides with a +built-in, rename it. + +## `fj pr ready` returns "pull does not allow editing draft state" + +Forgejo only lets you flip `draft: false` via PATCH if the API user is +the PR author or has write permissions. If you're not the author and +not a maintainer, this fails. There's no workaround on the client side. + +## The hook hangs on `cargo test` + +Likely a keychain re-prompt for the test binary's new hash. Either +approve the prompt or rebuild outside the hook context first, then push: + +```sh +cargo test --all # accept any keychain prompts +git push # hook reruns; tests fast-cache hit +``` + +If genuinely stuck: + +```sh +FJ_SKIP_PREPUSH=1 git push # bypass once, only when necessary +``` + +## My commit was rejected because clippy complains about new code + +The hook runs `cargo clippy --all-targets --all-features -- -D warnings`. +Every warning is an error. Either fix it or, very rarely and with a +comment explaining why, add a targeted `#[allow(clippy::lint_name)]`. + +Don't add `#[allow(dead_code)]` to silence "never used" warnings on +real code — that's a signal you have unused code paths to remove.