AGENTS.md@public/files/gitbutler/crates · git:20260914.a750666 · 2026-09-14 · sha256 7a8da48f33901231

AGENTS.md@public/files/gitbutler/crates git:20260914.a750666A

Immutable. This exact content never changes and is served at /api/v1/blob/7a8da48f33901231.

# Rust Instructions

These are defaults for Rust source and test changes under `crates/`; more
specific instruction files override them. Do not apply them to generated,
vendored, or fixture data unless the task is specifically about that code.

## API Boundaries and Legacy

- Treat `gitbutler-*` crates as legacy-heavy, but preserve local ownership and
  nearby patterns for localized fixes. Do not introduce new `gitbutler-*` usage
  in newer code unless the surrounding legacy boundary requires it. Keep legacy
  fixes localized; migrate only when necessary, tiny, tested, and
  behavior-neutral.
- Minimize new `VirtualBranchesHandle` usage. At API boundaries, prefer
  `but_ctx::Context` workspace helpers and newer `but-*` APIs.
- `but-api` is the API surface for Tauri, Electron/N-API, CLI, and TUI callers.
  Outer callers should prefer existing `but-api` functions when they fit, but
  lower-level crates should not depend on `but-api`.
- Preserve existing `but-api` macro, transport, serialization, and conversion
  patterns instead of hand-rolling parallel wrappers.
- Keep transport DTOs at the API boundary, commonly in a local `json` module,
  and convert them to domain types before calling lower-level crates.
- For permissioned `but-api` functions, follow the existing composition shape:
  acquire permission near the wrapper and delegate to a `_with_perm` or other
  permission-taking implementation.
- API consumers such as the CLI, TUI, and other composed callers should use
  `_with_perm` variants when they already hold permission to avoid extra locking
  and deadlock risk.
- Keep `Context` at API/composition boundaries; pass granular dependencies
  like repo, workspace, metadata, database handles, or an `Editor` into
  lower-level crates where possible.
- Acquire repository access locks at top-level API/command boundaries. Do not
  call helpers that acquire permissions while holding a guard; drop the guard or
  call a permission-taking helper.
- Preserve existing `DryRun` semantics; add dry-run support when the API/product
  surface calls for preview behavior. Dry runs should avoid persisting refs,
  objects, or oplog entries.
- When an API offers both action-only and timeline-recording behavior, keep the
  `*_only*` function free of oplog side effects; prepare a best-effort oplog
  snapshot in the wrapper and commit it only after the mutation succeeds.
- New user-facing mutations should participate in undo: record an oplog
  snapshot via the existing `SnapshotDetails`/`OperationKind` wrapper pattern,
  and call out any mutation intentionally left out of the timeline rather than
  omitting it silently. Undo is snapshot-based; do not design bespoke inverse
  operations for it.
- Before changing or reviewing code that derives graph/workspace/branch/stack/commit
  relationships, reachability, dependencies, ordering, operation targets, or Git
  graph/history/ref-placement mutations, use `crates/WORKSPACE_MODEL.md` as the
  reference. In short: prefer commit IDs and refs at API boundaries, convert to
  operation-local selectors inside editor-backed operations, use
  `but_graph::Graph` for relationship/reachability questions, use
  `but_rebase::graph_rebase::Editor` for Git graph/history/ref rewrites where an
  editor model exists, and treat `but_graph::Workspace` and
  `but_workspace::RefInfo` as lossy presentation/compatibility views unless an
  existing workspace-shaped boundary requires them.

## Code Shape and Naming

- Use Git/GitButler domain names and keep types/helpers in the crate or module
  that owns the concept.
- Avoid vague names like `Manager`, `Service`, `Helper`, `Util`, or `Processor`
  unless nearby code uses that concept.
- Solve the present problem directly. Avoid speculative abstractions: one-use
  traits/types/functions, fake extension points, ceremonial wrappers/modules,
  and public APIs larger than real callers need.
- Keep module boundaries discoverable from names and the call graph.
- Avoid drive-by refactors while fixing a specific bug.
- Prefer explicit domain enum matches when wildcard arms would hide behavior.
- Comment public API purpose/invariants/errors and complex algorithm intent;
  avoid comments that merely repeat the code.

## Git Repository Semantics

- Do not rediscover Git repositories inside business logic; pass repo/context
  explicitly.
- Prefer repository APIs over shelling out to `git`, except at
  shell/executable boundaries such as hooks, debug tooling, tests, or Git
  interop helpers.
- Preserve Git graph semantics even when the shape looks redundant or odd; do
  not deduplicate, reorder, or smooth graph data unless Git semantics allow it.
- Use `gix` for new repository logic. Treat `git2` and `Context::git2_repo` as
  legacy/boundary-only escape hatches for libgit2 checkout/index, hooks,
  transport/auth, or existing code that cannot reasonably move yet.
- Keep Git paths, refnames, commit messages, and diff payloads byte-preserving
  until UI/API boundaries; avoid lossy `String` conversion in business logic.
- Treat filesystem and worktree writes carefully; use existing locking,
  transaction, and access patterns.
- Avoid implicit `SystemTime::now()` in testable business logic; pass time in
  when deterministic behavior matters.
- Use existing reload/invalidation helpers after external repo or worktree
  changes, especially when working through `Context` caches.
- Use `anyhow::Context` to explain what operation failed. When frontend/API
  consumers need classification, use existing `but_error::Code` patterns; do
  not make consumers match error strings.

## Database Migrations (`but-db`)

- Keep migrations forward-compatible: stay on the current `SchemaVersion` and
  leave columns or tables that new code stopped using in place. Bumping the
  version locks every older binary out of the database; reserve it for planned,
  coordinated breaks, never routine cleanup.

## Version Control

- Assume the worktree may contain other agents' changes. Do not overwrite, clean
  up, stage, commit, or amend changes you did not make.
- When asked to branch, commit, push, or open a PR, use the GitButler `but`
  CLI/workflow when available.
- When the user says "ship it", commit your changes on a session branch,
  creating one if needed, then push and open or update the PR. Reuse the
  existing branch/PR when it already fits the session.
- For small cleanup or follow-up fixes on your own branch, amend the relevant
  existing commit(s) when that is the cleaner history.
- Keep commit messages and PR descriptions succinct: why, impact, and core
  decisions. Do not list local validation commands in commit messages or add AI
  co-author trailers or tool branding.

## Testing and Validation

- Run the narrowest relevant test or check first, for example `cargo test
  -p <crate> <test-name>` or `cargo check -p <crate> --all-targets`.
- Use existing test-support crates and fixtures; prefer read-only fixtures for
  read-only behavior.
- For graph, rebase, or workspace behavior, prefer fixture-backed before/after
  snapshots using existing visualizers, plus targeted structural assertions for
  the invariant.
- When snapshot output is volatile, stabilize inputs or normalize output; do not
  replace strong snapshots with vague assertions.
- When optimizing Git traversal or workspace behavior, preserve semantics with a
  fixture-backed regression test.
- Run `cargo fmt` for Rust formatting, avoiding unrelated dirty files.
- Run `cargo clippy --fix --allow-dirty` only after inspecting the diff scope and
  only when generated edits stay within the intended change.
- Run `cargo machete` when dependencies changed; verify targets, features, and
  platform-specific usage before removing dependencies.
- After changing Rust APIs or types exposed through `@gitbutler/but-sdk`, run
  `pnpm build:sdk && pnpm format`; this updates
  `packages/but-sdk/src/generated`.
- Use workspace-wide checks only when the change affects shared contracts or
  multiple crates.

## Assertions

- Explain why a standard Rust assertion holds concisely using the last-argument message, like `assert!(1!=2, "arithmetic unit on CPU works")`
- Snapshot assertions use `snapbox`, like `snapbox::assert_data_eq!(actual, snapbox::str![[r#"..."#]])`. `snapbox::assert_data_eq!` has no message argument, so explain why a snapshot holds with a `// comment` on the line above it.
- Regenerate inline snapshots by running the tests with `SNAPSHOTS=overwrite`.
- Remove unstable output before asserting (sanitize ids/timestamps/paths, e.g. the `but_testsupport` helpers) rather than snapshotting it raw. `snapbox` also pattern-matches expected data by default (`[..]` and `...` wildcards, path-separator normalization); append `.raw()` to the expected `snapbox::str!` when the value must match exactly (e.g. it contains backslashes or literal `[..]`/`...`).