GitBaby/AGENTS.md
zhenyi 4d5e5ba5f9 feat(server): add streaming-first server scaffolding
Introduce src/server/ as a streaming-first git/lfs server surface. All
request and response bodies cross the API as
Box<dyn AsyncRead + Unpin + Send + Sync> (alias BoxedAsyncRead) — no
Vec<u8> bodies, no read_to_end convenience, no http crate.

What's in the box:
- src/server/mod.rs: re-exports + pipe re-export of
  command::cmd::DEFAULT_OUTPUT_CAP_BYTES as server::DEFAULT_OUTPUT_CAP_BYTES
- src/server/backend.rs: ServerBackend async-trait (authorize,
  list_refs_stream, produce_pack_stream, ingest_pack_stream,
  lfs_get_stream, lfs_put_stream). Every method currently returns
  ServerError::Unimplemented("...") placeholders — the wiring is
  real, the bodies are intentionally empty.
- src/server/request.rs: ServerRequest (not Clone, body is a boxed
  reader), HeaderMap = HashMap<String, String>, RefUpdateBatch
  (oid strings, no gix::ObjectId), BoxedAsyncRead alias
- src/server/stream.rs: StreamGuard (Clone + Send + Sync) carrying
  cancel() / handle() so spawned workers can race the request
- src/server/endpoint.rs, error.rs, limits.rs: EndpointKind enum,
  ServerError, DEFAULT_LFS_OBJECT_CAP_BYTES (5 GiB cap, separate from
  the 64 MiB output cap)
- tests/server.rs: PlaceholderBackend skeleton + ENV_LOCK +
  200 ms cancel convention reused from tests/env.rs and tests/pipe.rs

Public API impact on GitBaby:
- Drop pipe from GitBaby::new — the server owns its own stream
  lifecycle, so the Pipe parameter is no longer required at
  construction time. Existing callers must be updated:
    - before: GitBaby::new(facade, pipe)
    - after:  GitBaby::new(facade)

Documentation:
- AGENTS.md gains a "Planned but unimplemented (streaming-first
  server)" section documenting the deliberately-empty contracts so
  future contributors don't "fill them in" without a concrete
  git/lfs protocol task.

CI: cargo build + cargo test (61 total, 23 new from server suite)
green.
2026-08-14 18:13:39 +08:00

5.0 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.
  • 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.