POC-2 (standalone mode, /workspace/alkgit-poc2) confirms the fetch crux: gitoxide's own generation pipeline streams valid packs to any io::Write, and real git 2.43 clones/fetches over the POC-1 bridge with fsck --strict passing on every clone. - Findings: docs/research/poc2-findings.md (proceed) - git-protocol.md: pack-generation option list resolved ((b) wins); record two-stage closure (commit ancestry -> TreeContents), odb handle prerequisites, bundle::write's real role (receive-pack indexing), missing-delta-synthesis note - gitoxide.md: generation pipeline + gix-odb API contract notes (Cache !Sync, prevent_pack_unload, missing_objects check) - pocs.md: POC-2 outcome recorded; POC-3 is the remaining phase-0 gate Verification: cargo run -- selftest (gen -> bundle roundtrip -> index-pack --strict -> verify-pack -> unpack-objects --strict -> fsck -> closure cross-check -> sideband wire decode); 10k-commit/60k-object fixture: closure 60000==60000, pack 5.33MB in 5.5s at ~11MB extra RSS; real git clones (loose and packed sources) + tag checkout + incremental fetch all fsck-clean over git:// through the alkcall Connection/BiStream path
7.6 KiB
Research: gitoxide (gix) as the git engine
Status: initial pass complete
Date: 2026-09-19
Sources: local clone at /workspace/gitoxide (repo HEAD 77c8cd956,
tag-described as gix-transport-v0.59.2-130-g77c8cd956), crates.io API.
TL;DR
gitoxide covers the storage layer (odb, packs, refs, objects, fsck) and gives
us the packetline codec, but it does not give us a git server. gix-protocol
and gix-transport are client-side (fetch/clone from the point of view of the
machine asking for objects). The server half of the smart protocol — capability
advertisement, ls-refs command dispatch, fetch negotiation on the serving side,
pack generation on demand, and receive-pack application with ref transaction and
update-hook semantics — has to be built by us. This is expected and acceptable:
it is exactly the narrow protocol-shim layer our security model wants to own
anyway (see git-protocol.md).
Version alignment (important)
| crate | crates.io max stable | local clone | note |
|---|---|---|---|
| gix | 0.87.1 | 0.87.1 | in sync |
| gix-packetline | 0.22.2 | 0.22.2 | in sync |
| gix-transport | 0.59.2 | 0.59.2 | in sync |
| gix-protocol | 0.65.1 | 0.65.1 | in sync |
| gix-pack | 0.74.2 | 0.74.2 | in sync |
| gix-odb | 0.84.0 | 0.84.0 | in sync |
| gix-ref | 0.67.1 | 0.67.1 | in sync |
| gix-object | 0.64.1 | 0.64.1 | in sync |
| gix-hash | 0.26.2 | 0.26.2 | in sync |
| gix-fsck | 0.25.1 | 0.25.1 | in sync |
The published 0.87.1 (2026-08-24) matches the clone; the clone carries a
handful of unreleased commits on top. Pin published crates.io versions in
our manifests; treat the clone as a reading/reference aid, not a path
dependency. If an unreleased fix turns out to be required, that is an OQ for
architecture (gitoxide git-patch or [patch] section is the fallback).
Hash algorithm gotcha (encountered live)
gix with default-features = false and no hash feature fails to compile:
gix-hash emits compile_error!("Please set either the sha1 or the sha256 feature flag"). The workspace manifest now pins features = ["sha1"]
throughout (gix, gix-pack, gix-odb, gix-object, gix-hash) and our crates carry
a matching sha256 passthrough feature for the future. This is a
compile-time-rejected invariant, so the config can't drift silently.
What we can use off the shelf
Storage (strongest area — use as-is)
gix-odb— object database with loose + packed stores, dynamic multi-index loading,Handlewith caching; thedynamicstore refreshes on mtime changes, which suits a long-running server. API contract notes (POC-2, validated live): handles used for pack generation needprevent_pack_unload()andignore_replacements = true.Cacheis deliberately notSync(per-threadRefCellpack/object caches); shareArc<Store>across tasks and build a handle per session — generation belongs on blocking threads with the owned handle moved in.gix-pack— pack data reading (index file, multi-index, delta resolution) and pack writing (bundle::write→write_to_directoryfor index-from-stream). Pack writing produces index+pack into a directory; streaming a pack directly to a socket needs evaluation (POC candidate). Generation pipeline (POC-2, validated live):data::output(featuregenerate) is the on-demand generation path:count::objects(_unthreaded)(closure expansion;TreeContentsexpands each input commit's own tree but does NOT follow parents — feed the commit-ancestry walk through it) →entry::iter_from_counts(chunked deflate + pack-delta copy, back-pressured) →InOrderIter→bytes::FromEntriesIter(header/entries/trailer to anyio::Write, V2 only). Composition reference:gitoxide-core/src/pack/create.rs.bundle::write_to_directoryis the indexing side (consume an existing pack stream; needsstreaming-input) — receive-pack's tool, not fetch's. Deltas are copied from existing packs; no delta synthesis exists upstream (gix-deltais apply-only). Entry stats (finalize()) expose copied/recompressed/missing counts — usemissing_objectsto abort instead of emitting broken packs. Feature set for our lean build:sha1+generate+parallel(+streaming-inputfor bundle write).gix-ref— ref store withtransactionmodule (compare-and-swap semantics, reflog), which is what receive-pack needs for atomic ref updates.gix-object— object parsing/encoding (commit, tree, tag, blob).gix-fsck— connectivity checks for received packs.gix-discover— repository discovery /.gitdir resolution.gixfacade — repo opening, config, etc. For library-developer usage gitoxide recommendsdefault-features = false+ only the components needed; follow that to keep compile times down.
Wire format (use as-is)
gix-packetline— pkt-line encode/decode, both blocking andfutures-ioasync (async-iofeature). This is the one crate both the http and ssh interfaces need most; it is small and stable (0.22.x). API contract notes (POC-1, validated live):async_iocodec is built onfutures_iotraits — bridging a tokio stream takestokio::io::split+tokio_util::compat::{compat, compat_write}(two one-liners).StreamingPeekableItertreatsFlushas a stop-delimiter: on flush,read_line()returnsNone(no line is yielded) and the iterator is inert until.reset()— a command loop must reset after every request or the next command is silently swallowed. Return type isOption<io::Result<Result<PacketLineRef, decode::Error>>>(three layers; wrap it in a session type).gix-transport— definesProtocol(V0/V1/V2),client::MessageKind, and the fetch-side abstractions. Only the types are reusable server-side; its transport implementations (client) are not what we need.
Client side (exists, mostly irrelevant for us)
gix-protocol— handshake,ls-refs, fetch negotiation from the client side (fetch::Response::from_line_readerparses server responses; the negotiate module builds request arguments). Useful as a reference for response shapes we must produce server-side, and possibly reused for alkgit-to-alkgit replication later.gix-transport/src/lib.rsexports onlypub mod client— confirming no server-side half exists upstream.
What gitoxide does NOT give us (we own this)
- Capability advertisement (V0/V1 first-want line and V2 capability handshake) — must produce ourselves.
- Server-side command dispatch —
ls-refs,fetchV2 command parsing and the request→response state machine. - Server-side negotiation — evaluating client haves against our refs (ack/NAK logic, shallow handling on the serve side).
- On-demand pack generation for a fetch —
pack writing exists (Resolved (POC-2): thegix-pack bundle::write) but "stream a pack computed from a want/have set to a socket" composition needs a POC; worst case we walk objects ourselves via odb and feedgix-pack::data::output::bytesentries-to-bytes writers.data::outputpipeline streams to anyio::Write; we own only the orchestration (peel tips → commit walk → count → entries → bytes) and the missing-objects check. Delta synthesis for loose objects remains an upstream gap (optimization backlog). - receive-pack — parsing client pack stream (
gix-pack::data::inputcan parse a pack from a reader), fsck, ref CAS updates viagix-reftransaction, reporting status (unpack ok/nglines).
License
MIT OR Apache-2.0 — same as ours. Clean to depend on and to read for inspiration (attribution not required under either license, though NOTICE-type courtesy is fine).