v0.7.0
Stable promotion of the full 0.7.0 release train. This consolidates every pre-release delta from rc.1, rc.2, and rc.3 into the stable notes.
Upgrade notes
Section titled “Upgrade notes”Install with cargo install gwm --version 0.7.0 or from the release archives.
MSRV is 1.82. No .gwm.toml migration is required.
Stable-only delta after rc.3
Section titled “Stable-only delta after rc.3”Performance
Section titled “Performance”- ⚡ TUI sidebar commit graph pipes now carry
git2::Oidvalues instead of heap-allocated hash strings (#108 / #142). This cuts the 300-row graph render benchmark by about 47% and keepsPipeallocation-free. - ⚡ Recent Commits in the TUI sidebar now use a libgit2 revwalk cached by
(worktree path, head OID, limit)instead of shelling out togit logon repeated sidebar rebuilds (#107 / #143). This cuts the 300-row sidebar benchmark by about 99%.
Historical delta: [0.7.0-rc.1] - 2026-05-22
Section titled “Historical delta: [0.7.0-rc.1] - 2026-05-22”Delta against v0.6.0 stable. Merged PRs: #90, #91, #92, #109, #110, #111, #113, #115, #116, #118.
This RC bundles three configurability features (declarative labels / milestones / branch_types), three security hardening passes against attacker-controlled .gwm.toml (TOFU trust ledger, symlink-safe bootstrap, path-traversal rejection), a TUI regression fix on R: review, and end-to-end test coverage on the mutating subcommands.
- Declarative GitHub labels (#81 / #90). New
[[labels]]table in.gwm.tomldeclares the desired GitHub label set (name + optional description / color), plus a new subcommand:gwm labels list- print the resolved set and the diff against theoriginremote (+ create,~ update,= match,- extra-on-remote).gwm labels push- apply the diff viagh label create --force.--dry-runshows the plan without mutating the remote (it still reads remote labels viagh label listto compute the diff; only create / update / delete calls are skipped);--pruneopt-in deletes labels on the remote that aren’t declared in config (destructive, off by default);--random-colorspicks a random pastel for labels with nocolorfield instead of the default deterministic-hash colour.- Colour resolution: when
coloris omitted, gwm derives a deterministic pastel from an FNV-1a hash of the name, so the same label gets the same colour across repos. Hex normalisation accepts#D73A4Aand round-trips tod73a4a. - Without a
[[labels]]block in.gwm.toml, both subcommands are no-ops (0 labels declared, nothing to push) and never shell out togh- safe to run in repos that haven’t opted in. - Requires
ghon$PATH(already a soft dependency ofgwm status).
- ✨ Configurable branch types (#80 / #91). New
[[branch_types]]block in.gwm.tomloverrides the built-in allowed branch types. Absent or empty block ⇒ historical defaults (feat,fix,hotfix,docs,test,refactor,chore,perf,ci,build); present ⇒ only the listed types are accepted bygwm create, the TUI create picker andBranchSpec::validate().gwm typesprints the resolved list with a(source: built-in defaults | .gwm.toml)footer and aligns columns on the longest name. Invalid-type errors now enumerate the repo-local allowed list verbatim instead of leaking the hardcoded set. Entries are validated at load time:namemust be non-empty, match^[a-z]+$, and be unique. - Declarative GitHub milestones (#82 / #92). New
[[milestones]]table in.gwm.tomldeclares the desired GitHub milestone set (title + optional description /due_on/state), plus a new subcommand mirroringgwm labels:gwm milestones list- print the resolved set and the diff against theoriginremote (+ create,~ update,= match,- extra-on-remote).gwm milestones push- apply the diff viagh api repos/:owner/:repo/milestones(POST for new entries, PATCH for updates). No nativegh milestonesubcommand exists, so we shell out to the REST API directly.--dry-runshows the plan without mutating the remote;--pruneopt-in deletes milestones on the remote that aren’t declared in config (destructive, off by default).due_onaccepts bothYYYY-MM-DD(materialised as end-of-day UTC, common-sense “due Friday” semantic) and full RFC3339 (2026-07-15T17:00:00Z, canonicalised to UTCZso non-UTC offsets don’t flip-flop the diff);statedefaults to"open", opt in to"closed"for archived sprints.- Without a
[[milestones]]block in.gwm.toml, both subcommands are no-ops (0 milestones declared, nothing to push) and never shell out togh- same safe-by-default contract as labels. - Requires
ghon$PATH.
Security
Section titled “Security”- 🔒 TOFU trust ledger on
.gwm.toml+--allow-bootstrap/--deny-bootstrapflags (#95 / #113). Closes the arbitrary-RCE primitive againstgwm create/gwm bootstrapon hostile clones: until this lands, runninggwmagainst a freshly cloned (or PR-fork) repo was equivalent tocurl … | shagainst the repo author, with no trust boundary between “I cloned this remote” and “this remote can execute commands as me”.- Ledger at
~/.config/gwm/trust.toml(overridable via$GWM_TRUST_LEDGER) records(origin URL, sha256 of .gwm.toml, trusted_at, trusted_by)tuples. First run on a repo with a.gwm.tomlprints a summary of the bootstrap surface (copies, guards, no-symlinks, command lines) and promptsTrust this .gwm.toml? [y/N/show]. Subsequent runs against the same(origin, hash)pass silently. Any byte change to.gwm.tomlre-prompts (whitespace included -rm -rf /tmp/andrm -rf /tmp /differ by one byte). - New subcommand
gwm trustwith three actions:list(audit the ledger),revoke <origin>(drop entries for an origin URL - verbatim match against the recorded form, SSH and HTTPS are distinct),show(print the active ledger path and contents). - Bypass flags:
--allow-bootstrap(global, alsoGWM_ALLOW_BOOTSTRAP=1) skips the prompt without recording - for CI runners and scripted workflows where there is no human to answer.--deny-bootstraprefuses to run bootstrap even if the ledger says trusted - for forensic inspection of an unfamiliar repo. - Non-interactive safety: when stdin is not a tty and no
--allow-bootstrapis in effect,gwmaborts with a clear message rather than hanging on a read that will never see input. The default-deny prevents a CI pipeline from silently running attacker code because the prompt got swallowed. - No schema change to
.gwm.toml- purely an additive layer above the existing bootstrap pipeline. Existing repos get a single one-shot prompt after upgrade. - Full docs page:
docs/4.configuration/5.trust-ledger.md(shipped in PR #115 - closes #114).
- Ledger at
- 🔒
bootstraprefuses to copy through symlinks at the destination (#93 / #110). Three changes that together close a write-anywhere primitive triggered bygwm bootstrapon an attacker-controlled checkout:- Reorder:
run_no_symlinksnow runs beforerun_copiesinbootstrap::run. The previous ordering let a target declared in both[[bootstrap.no_symlink]]and[[bootstrap.copy]]be touched by the copy pass first - either silently skipping through a live symlink or writing through a dangling one before the no-symlink pass got a chance to strip it. - Defence in depth in
run_copies: a newsymlink_metadatacheck at the top of the loop refuses any step whose destination is a symlink (broken or live), regardless of whether the user declared[[bootstrap.no_symlink]]for it. Closes two failure modes the reorder alone doesn’t: symlinks not declared in[[no_symlink]](planted by manual migration, editor plugins, or an attacker), and the macroscopic TOCTOU window between the no-symlink pass and the copy loop. O_NOFOLLOW-based write primitives (bootstrap::copy_no_follow/bootstrap::write_no_follow) replace everyfs::copy/fs::writesite underrun_copies,resolve_missing(inline fallback), andhandle_guard_match(seed-from-example). The destination file is opened withO_NOFOLLOW | O_CREAT | O_EXCLon unix, so even if a symlink materialises in the microseconds between the stat and the open, the syscall returnsELOOP(symlink) orEEXIST(other entry) instead of writing through. Source permissions are preserved on unix to match the priorfs::copybehaviour.- Observable contract change: a copy step whose destination is a symlink now reports
Failedwith a message naming the path and referencing #93. Pre-fix, the live-symlink case was a silentSkippedand the broken-symlink case was aFailedfromfs::copyerrno. No CLI surface or.gwm.tomlschema change.
- Reorder:
- 🔒
bootstraprejects path traversal in copy / guard / fallback fields (#94 / #111). Closes the write-anywhere / read-anywhere primitive flagged onstep.to,Guard.example_file, andFallbackContent.target- values like"../../etc/passwd"or"/etc/shadow"previously slipped throughPath::joinand landed outside the worktree (or read outside the main repo). Two layers:- Load-time validation (
Config::load_for_repo→validate_bootstrap_paths): rejects empty strings, absolute paths, and..traversal segments in the three fields, surfacing the violation with the exact TOML key (e.g.bootstrap.copy[0].to) so the user can fix the config without grepping.bootstrap.copy[].fromis intentionally not validated - it’s joined ontoctx.main_repo(the repo root that ships the config), same trust boundary as the file itself. - Runtime defence-in-depth (
bootstrap::ensure_within): canonicalize-then-prefix-check on every resolved path inrun_copies(againstctx.worktree) and onexample_fileinhandle_guard_match(againstctx.main_repo). The deepest existing ancestor is canonicalized so the check catches..traversal, absolute paths, AND symlinks in intermediate components - closes a third attack vector the load-time validator alone misses (an attacker who can plant a symlink at adstparent component redirects the resolved path even when the TOML string is benign). - Behavior on rejection: the step reports
Failedwith a detail that names the offending path and references issue #94. No CLI surface or.gwm.tomlschema change for benign configurations.
- Load-time validation (
- 🔒
configrejects invalid[[bootstrap.guard]].deny_patternsat load time (#96 / #116). A guard whose regex failed to compile previously surfaced the error mid-bootstrap on the first matching copy - leaving an inconsistent partial state on disk. Validation now runs atConfig::load_for_repo, naming the offendingbootstrap.guard[<N>].deny_patterns[<M>]index plus theregexcrate’s diagnostic. The bootstrap-time evaluation path also flipped from “log and continue” to fail-closed: a regex that somehow slips past the load-time check (e.g. a config edited between load and use) makes the affected copy step reportFailedinstead of proceeding without the guard.
- 🐛
R: reviewno longer silently no-ops on push-tracked PR branches (#117 / #118).resolve_review_basenow ignoresbranch.<n>.mergewhen it points at the branch itself. After the canonicalgwm create <type> <#> <slug> && git push -u origin <branch>flow, git recordsmerge = refs/heads/<same-branch>- the local branch tracks its own remote-side copy. The previous priority-1 chain step returned that name verbatim, makinggit diff <branch>..<branch>empty so the launcher’s defaultskip_when_no_changes = truesilently swallowed theRkeystroke. The fix falls through to the next chain step (branch.<n>.gwm-base→[review].default_base→dev/main) when the upstream is self-referential, so the launcher handsgit diffa ref that actually diverges from HEAD. The self-equality compare runs afterread_branch_mergestrips therefs/heads/prefix, so both the canonical refspec form and the bare short-name form are caught.
- ✅ E2E coverage for the mutating subcommands (#101 / #109).
tests/cli_binary.rsnow exercisesgwm init(default body shape, idempotency refusal on existing.gwm.toml, repo-bound contract),gwm create(worktree dir + branch creation at HEAD,branch.<name>.gwm-baserecorded for the launcher fallback chain,[[bootstrap.copy]]runs by default but is skipped under--no-bootstrap, validation rejects unknown branch types and non-digit issue numbers), andgwm remove(deletes the worktree dir,--delete-branchdrops the local branch, unknown patterns fail loudly). All worktree-creating tests pin[worktree].baseto atempfile::TempDirso the CI runner never writes under~/cc-worktree/.... - ✅ Characterization tests for #98 / #99.
tests/worktree_integration.rs::add_silently_attaches_to_pre_existing_stale_branchpins the current (buggy) reuse behaviour ofworktree::addwhen the target branch already exists - a fix for #99 will turn this test red and force the reviewer to confirm the contract change.remove_prunes_admin_files_on_happy_pathpins the post-condition that.git/worktrees/<name>is gone after a successfulworktree::remove- the post-condition that #98’s reorder fix must preserve.
Dependencies
Section titled “Dependencies”- Add
sha2 = "0.10"for.gwm.tomlcontent hashing in the TOFU trust ledger (#95).
Historical delta: [0.7.0-rc.2] - 2026-05-23
Section titled “Historical delta: [0.7.0-rc.2] - 2026-05-23”Delta against v0.7.0-rc.1. Merged PRs: #119, #120, #121, #122, #129, #130, #131, #132, #133, #134, #135, #136, #137.
This RC bundles a TUI render-loop perf cleanup, three targeted bug fixes (#98, #99, #100), an MSRV bump to 1.82, two error/dedup refactors (#105 / #106), and the complete decomposition of the tui::app::App god struct into six sub-state slices under tui/state/ (#102, parts 1/6 through 6/6 - #123, #124, #125, #126, #127, #128).
Performance
Section titled “Performance”- ⚡ TUI no longer opens
git2::Repositoryper worktree row per frame (#103 / #129).branch_age_for(w)used to callgit2::Repository::open(&w.path)+ a full revwalk on every render pass; on a 30-worktree repo at 60 FPS that’s 1800 opens + 1800 revwalks per second, measurable as a sidebarj/kstutter. The fix pre-computesageonce atworktree::list()time and caches it asWorktreeInfo.age: Option<Duration>- the TUI render path becomes pure read-only struct field access. The list pass already opens the worktree’s repo to read HEAD + status, so piggybacking the revwalk costs nothing extra; the per-frame work disappears entirely. Unblocks thetui::app::Appdecomposition (#102) by removing the libgit2 leak from the render tree.
Security
Section titled “Security”- 🔒 Argv-injection guards on
gh label …andgit diff/git rev-list(#100 / #121). Closes two vectors flagged by the multi-agent review:- Label names that confuse
gh’s flag splitter.gh label create <name>takes the name positionally, so a[[labels]] name = "-h"was parsed by gh as the help flag -ghprinted its banner and exited 0, whilegwm labels pushhappily reported✓ createdfor an entry that never existed.name = "--repo"retargeted to a different repository entirely.validate_label_name(insrc/labels.rs, invoked both atConfig::load_for_repotime and fromresolve_labels) rejects empty names, leading-, embedded,(GitHub’s label-list separator), and ASCII control characters. Spaces and unicode pass through verbatim - GitHub permits them and real-world label sets rely on them. --end-of-optionsbefore user-derived git refs.materialise_diffandcount_commits_aheadshelled out togit diff <base>..<head>andgit rev-list --count <base>..<head>withbaseandheadtaken verbatim from the review base-resolution chain (branch.<n>.merge,branch.<n>.gwm-base,[review].default_base). A[review] default_base = "--upload-pack=/tmp/x"is the textbook CVE-2017-1000117 shape: on susceptible git versions the leading--made git re-parse the value as an option and (historically) execute arbitrary code. Modern git is patched, but the defensive--end-of-optionsseparator is still mandated. New helpersgit_diff_argv/git_rev_list_count_argvcentralise the argv construction with the separator interposed; the two shell-out sites consume them.- Covered by
tests/labels_tests.rs::{validate_label_name_*,resolve_labels_propagates_invalid_name_from_config},tests/config_tests.rs::labels_load_rejects_leading_dash_name_at_load_time, andtests/launcher_tests.rs::{git_diff_argv_*,git_rev_list_count_argv_*,count_commits_ahead_treats_dash_prefixed_base_as_ref_not_flag}. No CLI surface or.gwm.tomlschema change for benign configurations.
- Label names that confuse
Changed
Section titled “Changed”- ♻️ TUI:
tui::app::Appgod-struct fully decomposed into sixtui/state/sub-structs (#102, parts 1/6 through 6/6). The 1300-lineApppreviously held 30+pubfields mixing repo handle, domain state, UI state, input state, modal state, and async fetch state - every change rippled across unrelated fields. This RC lands all six atomic extractions:CreateForm(#123 / #133, 1/6) - the 4 create-overlay fields (create_field,create_type_index,create_issue,create_desc) + 7 inline methods become a pureCreateFormsub-struct atsrc/tui/state/create_form.rs.Fieldenum re-exported fromtui::so the public surface is unchanged. Newtests/tui_state_create_form_tests.rs(11 unit tests).FilterState+ memoisedfiltered_indices(#124 / #134, 2/6, closes #104) - extracts the fuzzy-filter buffer + active flag, AND introduces the memoised cache that closes #104.App::filtered_indices()used to recompute 3–5× per render frame (table draw +clamp_selection_to_filter+ navigation methods); now the expensive nucleo pass runs once per query/list change. Cache invalidated by every buffer mutation; worktrees-length mismatch auto-invalidates as defence-in-depth. 17 new unit tests.ConfirmModal(#125 / #131, 3/6) - the destructive-action safety countdown (issue #30) becomes a pure sub-struct parameterised on(now: Instant, total: Duration).ConfirmKeyAction/CountdownTickOutcomeenums move alongside. 10 new unit tests.LinkPrompt(#126 / #135, 4/6) - the two-stage issue/PR link prompt (issue #67) becomes a pure sub-struct exposingtoggle_target/commit_target/ stage-awarepush_char/pop_char.LinkPromptStagere-exported. 8 new unit tests.SidebarState+ navigation-triple dedupe (#127 / #136, 5/6) - extracts the sidebar fields + collapses the 4-occurrencesidebar_scroll = 0; invalidate_sidebar_cache(); refresh_link();triple into oneApp::on_navigation()entry point. Future navigation motions can’t accidentally regrow the duplication. 13 new unit tests + 1 App-level integration test.GitHubFetch+ inflight dedupe (#128 / #137, 6/6) - extracts the 4 GitHub-linking fields (link,link_slug,issue_state,pr_state) + theGitHubFetchState<T>enum. New explicit inflight-dedupe layer: every spawn goes throughGitHubFetch::request(FetchKey)returningHitCache/AlreadyInflight/Spawn. Concurrent visit events to the same target no longer trigger redundantghshell-outs. 11 new unit tests.
- ♻️
naming.rsregexes are now module-levelLazyLock<Regex>statics (#97 / #122). The three patterns poweringBranchSpec::validate_againstandparse_branch(^\d+$for issue numbers,^[a-z0-9][a-z0-9-]*$for kebab-shaped descriptions,^([a-z]+)/#(\d+)-([a-z0-9-]+)$for the full branch shape) used to recompile per call viaRegex::new(...).unwrap()/.ok()?. The TUI sidebar refresh andgwm doctorboth callparse_branchonce per worktree - O(N) wasted compilations on a busy repo. The static-lift drops the per-call cost from ~5µs to ~50ns. Compile-time literals can takeexpect("static <NAME> compiles"): a regex-compile failure on a hard-coded pattern is a developer bug caught by the newnaming_regexes_compile_at_first_usetest, not a user-facing error. CLAUDE.md “nounwrapon user-facing paths” rule respected - three.unwrap()call sites removed. - ♻️ Split
GwmError::Other(String)catch-all into typed variants (#105 / #130). The four highest-recurrence patterns flagged bygrep GwmError::Other src/now have their own variants so callers (and downstream pattern-matchers) can introspect without string-sniffing:UnbornHead { reason }-cli.rs::current_branchwhenrepo.head()fails (unborn / detached) or the shorthand can’t be resolved.GhJsonParse { kind, source }- everyserde_json::from_strsite that consumes aghCLI payload (parse_issue_json,parse_pr_json,find_pr_for_branch’s PR list,parse_labels_json,parse_milestones_json).kindis a&'static strso a grep-friendly hint lands inDisplay;sourceis the underlyingserde_json::Errorfor#[source]chaining.LinkMissing { kind: LinkKind, branch }-cmd_openwhen a branch has no recorded issue or PR link.LinkKind(a new publicenum Issue | Pr) replaces the prior twoOther(format!("no issue/PR linked …"))sites with a typed dispatch.- The 6
Other(format!("git log/status …"))sites inworktree.rs::git_log_with_author/git_log_oneline/git_status_shortnow use the existingCommandFailed(String)variant, aligning them with the rest of thegwm-shells-out callers (issue, PR fetches, multiplexer spawn). - Remaining
Other(String)call sites (config-key parse failures, unknownghenum states, ambiguous fuzzy patterns, trust-prompt refusals, TUI input validation, missingoriginremote) stay asOtherfor now - they’re heterogeneous enough that grouping them under one new variant would be a regression of the same shape.
- ♻️
StepResultconstructors,StepStatus::sigilhelper, sharedLinkTarget, and generic diff-summary helpers (#106 / #132). Four real duplication patterns flagged by the multi-agent review collapse here:bootstrap::StepResultnow exposesok/ok_with_detail/skipped/warning/failedassociated constructors. The 20-plus inlineStepResult { label, status, detail }literals scattered acrossrun_copies,resolve_missing,handle_guard_match,run_no_symlinks, andrun_commandscollapse into single-line calls.bootstrap::StepStatus::sigil()centralises theOk → "✓",Skipped → "·",Warning → "!",Failed → "✗"glyph mapping previously duplicated betweencli::print_reportandtui::ui::render_bootstrap.cli::LinkTargetis the single canonical definition (withclap::ValueEnum);tui::appre-exports it. Values crossing the cli/tui boundary no longer need conversion.labels::diff_summary_lineandlabels::diff_dry_run_linerender the two summary lines shared bygwm labels pushandgwm milestones push. Bothcli::print_labels_diffandcli::print_milestones_diffnow call the helpers verbatim.- No CLI / TUI /
.gwm.tomlschema change - call-site refactor only. Output is byte-identical to v0.7.0-rc.1.
- 🔧 MSRV bumped to Rust 1.82 (#122). The crate already uses
std::iter::repeat_n(stable since 1.82) insrc/tui/ui.rs’s countdown bar; theCargo.tomlrust-versionfield now declares the actual floor that compiles the whole crate.std::sync::LazyLock(1.80, from #97) is covered too. CONTRIBUTING.md already documented “stable channel, 1.80+ - verified on 1.89”; users on 1.80 / 1.81 were broken regardless.
- 🐛
worktree::removeprunes admin metadata BEFORE deleting the directory (#98 / #119). Closes a “phantom worktree” failure mode:worktree::removepreviously calledfs::remove_dir_allfirst, thenwt.prune. A failure on the prune step (libgit2 error, permissions, partial state) left the on-disk directory gone butrepo.worktrees()still listing the name -gwm listshowed a ghost row,gwm pathresolved to a non-existent directory, andgwm bootstrapagainst the ghost failed with a confusing error until the user rangwm prunemanually. The new ordering prunes first (administrative metadata) and only then removes the filesystem tree, so any failure leaves the worktree visibly present on disk and recoverable through the normalgwmsurface. Symmetric to thebootstrapreorder shipped in #93: when a multi-step operation can partially fail, the step that’s hard to recover from goes first. Covered bytests/worktree_integration.rs::remove_failed_filesystem_unlink_still_prunes_metadata(forcesremove_dir_allto fail via a read-only parent dir and asserts the admin entry is already gone) plus the existingremove_prunes_admin_files_on_happy_pathcharacterization. No CLI surface or.gwm.tomlschema change. - 🐛
gwm createrefuses to silently reuse a stale local branch (#99 / #120). Closes a “I asked for fresh, got stale” foot-gun: when a local branch with the same canonical name already existed (left over from an aborted attempt, agit fetch, or a deleted-and-recreated issue),worktree::addsilently attached the new worktree to that branch’s tip - resurrecting whatever commit it pointed at, invisible to the user until they rangit loginside the new directory. The new default surfacesGwmError::BranchExists { name, oid }naming the stale tip so the user can audit, delete the ref, or re-run with--reuse-branch. The escape hatch (--reuse-branchon the CLI,reuse_branch: trueon the lib) keeps the legacy attach-to-existing-tip behaviour available for the rare case where that is the intent. Covered end-to-end bytests/cli_binary.rs::{create_refuses_stale_branch_without_reuse_flag,create_reuses_stale_branch_with_flag}and at the libgit2 layer bytests/worktree_integration.rs::{add_refuses_stale_branch_without_reuse_flag,add_attaches_to_stale_branch_with_reuse_flag}. BREAKING CHANGE for in-tree lib consumers:worktree::addnow takes areuse_branch: boolparameter (recommended default:false).
Historical delta: [0.7.0-rc.3] - 2026-05-23
Section titled “Historical delta: [0.7.0-rc.3] - 2026-05-23”Patch RC over 0.7.0-rc.2 - one correctness fix on
GitHubFetch (the #128 extraction) plus two encapsulation polishes
flagged by Copilot review on the rc.2-era extraction PRs (#131
ConfirmModal and #134 FilterState). A throwaway clippy
chore + a per-frame allocation drop in the filter bar are bundled in.
Performance
Section titled “Performance”- ⚡ Filter bar render: zero-alloc per frame
(
src/tui/ui.rs:102).Span::raw(app.filter.query().to_string())allocated a freshStringon every render tick (~5/s during typing, every 200ms otherwise). Sincequery()returns&strandSpan::rawacceptsInto<Cow<'_, str>>, pass the borrow directly. Surfaced by Copilot review on PR #141.
Changed
Section titled “Changed”- ♻️
ConfirmModal.started_atis private;is_armed() -> boolaccessor added (#131). Encapsulation polish from a Copilot review on PR #131 (the originalConfirmModalextraction in rc.2). The single external read site (src/tui/ui.rsrendering the safety-countdown bar) was migrated; no behaviour change. Test path stays through the public API (progress(now, total) == 0.0proves “armed at exactly now” - no#[cfg(test)]back-door needed). Pinned byis_armed_returns_true_after_first_press_y_and_false_after_second. - ♻️
FilterState.queryis private;query() -> &straccessor added (#134). Copilot review on PR #134 flagged thepub query: Stringfield as leaking the buffer across the module boundary - every external caller did.is_empty(),.len(), orformat!("... {}", q), all equally well served by a&straccessor. Privatising also funnels the full write surface throughpush_char/pop_char/set_query/clear, each of which maintains the cache-invalidation contract that the field-write path bypassed. 8 read sites migrated acrosssrc/tui/{mod,ui,app}.rs; 32 test sites switched toset_query()/query(). Pinned byquery_accessor_reflects_push_char_and_pop_char. - ♻️
tui::fuzzy_match_indicesre-export dropped (#134). The helper was promoted totui::even though the only in-crate consumer (tui::app::App) already imports it via the fulltui::state::filter::fuzzy_match_indicespath. The re-export widened the public surface by one symbol for no return. Trimmed topub use state::filter::FilterState;. - 🔧
Field/LinkPromptStageuse#[derive(Default)]+#[default]attribute instead of hand-writtenimpl Default. Pre-existing on dev from #123 / #126; flagged by clippy 1.95’sderivable_implslint after CI runner toolchain bump. Net: −16 lines, identical semantics. Folded into PR #139 to unblock CI green.
- 🐛
GitHubFetchcache keyed by issue/PR number + drops late results afterinvalidate()(#138). Closes two correctness bugs flagged by Copilot review on PR #137 (the rc.2 #128 extraction PR), both in the newGitHubFetchsub-struct:- Cache identity collision.
is_cachedlooked only at the per-targetissue_state/pr_stateenum, so any terminal variant forIssue(_)made everyIssue(*)key falsely hit the cache. Afterrequest(Issue(42)) → complete_issue(42, Ok(...)), a subsequentrequest(Issue(43))wrongly returnedHitCache- even though Issue 43 was never fetched. The dedupe contract promised “(target, number) is the identity” - it held on the inflight path but broke on the cache path. - Late-result race with
invalidate().complete_issue/complete_prstamped results unconditionally, even when an interveninginvalidate()had cleared the inflight slot. A shell-out that resolved after the user navigated away (andinvalidate()cleared the slot) would stamp staleLoaded(IssueStatus)into the now-active worktree’s cache. - Fix: replace the single
issue_state/pr_stateslots withissue_cache: HashMap<u64, GitHubFetchState<IssueStatus>>+pr_cache: HashMap<u64, GitHubFetchState<PrStatus>>keyed by number;complete_*early-returns when the inflight slot for the key is gone (the “still authoritative” check).invalidate()clears both maps AND the inflight set. New keyed accessorsGitHubFetch::issue_fetch_state(number)/pr_fetch_state(number)return a&'static GitHubFetchState::Idlefor absent keys (no per-call allocation). App-levelApp::issue_fetch_state()/pr_fetch_state()resolve via the currentlink.issue/link.prso the renderer call sites intui/ui.rswork unchanged. - Pinned by
tests/tui_state_github_fetch_tests.rs::{request_after_complete_for_different_number_returns_spawn_not_hit_cache, request_after_complete_for_different_pr_number_returns_spawn_not_hit_cache, complete_after_invalidate_drops_the_stale_result, complete_pr_after_invalidate_drops_the_stale_result}- all four flipped RED → GREEN. No CLI surface or.gwm.tomlschema change.
- Cache identity collision.
Compatibility
Section titled “Compatibility”No CLI surface change. No .gwm.toml schema change. Library surface
shrinks by one re-export (tui::fuzzy_match_indices) and two pub
fields (ConfirmModal.started_at, FilterState.query) - external
consumers of gwm as a lib would need to migrate to the accessors,
but the lib is not yet on crates.io so practical impact is zero.
Upgrade notes
Section titled “Upgrade notes”Historical rc.3 install note: cargo install gwm --version 0.7.0-rc.3
or cargo install --path . from a clone. No data migration. No trust
ledger changes - existing ~/.config/gwm/trusted_bootstraps.json
entries from rc.2 continue to apply.
MSRV stays at 1.82 (no toolchain bump).