GitBaby/AGENTS.md
zhenyi 5d32290cf1 feat(cache): split cache module into submodules and wire invalidation hooks
Two-part change:

1) Submodule split of src/cache/ (was a single ~150-line mod.rs)
   - src/cache/config.rs: CacheConfig, defaults, default_cache_dir(),
     DEFAULT_NAME / DEFAULT_MEMORY_CAPACITY_BYTES /
     DEFAULT_DISK_CAPACITY_BYTES constants
   - src/cache/key.rs: CacheKey / CacheValue type aliases and key
     derivation helpers (rev -> oid, blob-exists negatives)
   - src/cache/store.rs: CacheStore — the foyer HybridCache wrapper
     (open / close / get / insert / remove / contains), all errors
     mapped to BabyError::Cache with op-tagged source
   - src/cache/pools.rs: CachePools — multi-store aggregator used by
     the global singleton; replaces the old monolithic CacheStore
   - src/cache/stats.rs: CacheStats + counter accessors
   - src/cache/invalidation.rs: hook_ref_updated / hook_blob_written
     (single-process invalidation hooks, fired from usecase impls)
   - src/cache/mod.rs: now just submodule re-exports of
     (config, global, invalidation, key, pools, stats, store)
   - src/cache/global.rs: GITBABY_CACHE singleton + init / try /
     shutdown now operate on Arc<CachePools> instead of Arc<CacheStore>

2) Cache integration in usecase modules
   - src/blob/usecase.rs: read paths go through cache.get / insert;
     invalidation hooks fire on write
   - src/commit/usecase.rs: commit-list results cached, invalidated on
     commit write
   - src/branch/usecase.rs: branch-list results cached, invalidated on
     ref update
   - src/refs/usecase.rs: lookup-cache + ref-update hook
   - src/tags/usecase.rs: tag-info cache + write hook

3) AGENTS.md
   - Document that cached serde payloads (CommitInfo, Vec<TreeEntry>,
     etc.) follow additive-only compatibility: never rename/remove a
     field, every new field gets #[serde(default)] so stale payloads
     still deserialize.
   - Note that the foyer cache is single-process: fixed on-disk dir
     per config + in-process invalidation hooks. Sharing across
     processes on the same repo can leave stale metadata; do not do
     that.

CI: cargo build + cargo test (61 passed) green.

BREAKING: the public cache surface moved. Anyone reaching into
cache::CacheStore directly should switch to cache::CachePools (or
cache::global::try_global_cache). The high-level helpers in
cache::global keep the same name; only the inner type changed.
2026-08-14 18:38:18 +08:00

5.6 KiB

AGENTS.md — gitbaby

What this is

  • Single-crate Rust library (no binary, no workspace). Edition 2024.
  • Public entrypoint: GitBaby::new(facade: Arc<dyn RepositoryFacade>, pipe: Pipe) in src/lib.rs.
  • Consumers must implement RepositoryFacade (src/repo.rs) — three async fns returning a path/paths and a gix::Repository. There is no default impl.

Build / test / verify

  • Standard cargo only. No CI, no pre-commit, no scripts, no rust-toolchain pin.
  • Edition 2024 requires a recent stable toolchain (rustc ≥ 1.85). Local env verified at rustc 1.97.1.
  • Useful commands (no order dependency beyond what's standard):
    • cargo build / cargo check / cargo build --release
    • cargo test (sync + #[tokio::test] integration tests in tests/)
    • cargo test --test <file> to run a single integration test file
    • cargo test <name> to filter by test name substring
    • cargo clippy --all-targets -- -D warnings if strict
    • cargo fmt --check / cargo fmt (default rustfmt; no rustfmt.toml)

Architecture notes

  • Error pattern is hand-written. BabyError (src/error.rs, ~50 variants) and CmdError (src/command/error.rs) implement Display + std::error::Error by hand. thiserror is in Cargo.toml but not used via #[derive(thiserror::Error)]. Do not "modernize" by adding the derive — keep the hand-written impls consistent.
  • Env in src/command/env.rs is order-preserving (Vec<(String, String)>), not a HashMap. with appends, set replaces in place. This matters for env-var ordering in spawned processes.
  • Cmd::Display = prog + config_args + args (config_args are prepended to args at runtime).
  • Pipe is Clone; cancel_pipe() sets cancelled=true and sends start_kill() to every running child but does not remove them from cmds.

Module layout convention

  • Most feature modules follow src/<feature>/{mod.rs, types.rs, helper.rs, usecase.rs}; mod.rs re-exports the public types from types.rs.
  • src/share/ is the outlier: it uses {cmd.rs, env.rs, error.rs, path.rs} instead. Don't "normalize" it.
  • src/command/ uses {cmd.rs, context.rs, env.rs, error.rs, pipe.rs} and has its own error.rs separate from src/error.rs.

Test quirks (tests/)

  • tests/cmd_run.rs defines a local unwrap_or_recover_or_panic!-style macro for asserting command output — reuse it, don't reinvent.
  • tests/env.rs mutates process env. It guards tests with static ENV_LOCK: Mutex<()> and uses keys prefixed GITBABY_TEST_ENV_<suffix> for isolation. std::env::set_var / remove_var are wrapped in local unsafe fns; follow that pattern when adding env-mutating tests.
  • tests/pipe.rs async cancel tests call tokio::time::sleep(Duration::from_millis(200)) after cancel_pipe() to let Child::try_wait observe exit — keep that delay when adding new cancel tests.

Gotchas

  • No README. No docs beyond the code. Treat Cargo.toml + source as the only source of truth.
  • gix = "0.86", tokio = 1.53 (full features), time = 0.3 — all recent; do not bump them speculatively.
  • Cache values that are serde-serialized (e.g. CommitInfo, Vec<TreeEntry>) follow a compatibility convention: fields are additive only — never rename/remove a field, and every new field must carry #[serde(default)] so stale cached payloads still deserialize.
  • The foyer-backed cache (src/cache/) is single-process: it uses a fixed on-disk directory per config and relies on in-process invalidation hooks (hook_ref_updated, hook_blob_written). It must not be shared across processes on the same repo, or metadata (rev→oid, blob-exists negatives) can go stale.
  • Public API surfaces async (async_trait); sync callers must use a runtime (tests use #[tokio::test]).

Planned but unimplemented (streaming-first server)

  • src/server/ exists (mod.rs, backend.rs, endpoint.rs, error.rs, limits.rs, request.rs, stream.rs) but every ServerBackend method currently returns ServerError::Unimplemented("…") placeholders. Do not "fill them in" with arbitrary bodies — wait for a concrete git/lfs protocol task before adding logic.
  • Streaming-only contract is intentional:
    • All request/response bodies cross the API as Box<dyn AsyncRead + Unpin + Send + Sync> (alias BoxedAsyncRead). Never introduce a Vec<u8> body, ServerResponse::body, or read_to_end convenience into the public surface.
    • HeaderMap = HashMap<String, String> (no http crate).
    • output_cap defaults to crate::command::cmd::DEFAULT_OUTPUT_CAP_BYTES (64 MiB). The server re-exports it as server::DEFAULT_OUTPUT_CAP_BYTES.
    • LFS upload cap is DEFAULT_LFS_OBJECT_CAP_BYTES (5 GiB) — a separate constant, not a magic number.
    • RefUpdateBatch is a server-local struct (oid strings, not gix::ObjectId) so the API does not depend on gix types. Do not alias refs::types::RefUpdate here.
  • ServerBackend async-trait method set: authorize, list_refs_stream, produce_pack_stream, ingest_pack_stream, lfs_get_stream, lfs_put_stream. Methods returning a body must return BoxedAsyncRead; ingest methods return IngestReport. The legacy head_object / write_ref / get_object shapes are intentionally removed.
  • ServerRequest is not Clone (body is a boxed reader); it carries a manual Debug impl. StreamGuard is Clone + Send + Sync and exposes cancel() / handle() so spawned workers can race the request.
  • Tests: tests/server.rs reuses the project-wide static ENV_LOCK: Mutex<()> pattern from tests/env.rs and the tokio::time::sleep(Duration::from_millis(200)) cancel convention from tests/pipe.rs. Add new server tests there.