collin/anvil
RenderedSource
anvil
A minimal, self-hosted git forge in Rust, built on gix (gitoxide).
Design rules
- Pure gitoxide — never the git binary. The product (server, CI broker,
mirroring, deploy image) must not invoke or depend on a
gitexecutable. When gix lacks a capability, implement the protocol on gix plumbing instead of shelling out — e.g. gix can't push yet, soanvil-git/src/push.rsspeaks the send-pack wire format itself (withgitserver-corecovering the server side). Tests may use the git CLI, but only as a fixture/interop check, never on the code path under test.
Dev setup: git config core.hooksPath .githooks — the pre-commit hook runs
cargo +nightly fmt (nightly, for the unstable options in rustfmt.toml),
cargo sort-derives (cargo install cargo-sort-derives), and
cargo clippy --workspace --all-targets -- -D warnings.
Viewing attachments referenced in tasks
TODO items and tickets may embed an uploaded image as
. These bytes live outside git
history, so to actually see one, get the bytes and Read the file. Two ways:
Preferred — over git, no credentials. anvil mirrors every attachment into
refs/anvil/attachments (a flat tree of <hash> → blob, off the branch
namespace so a default pull never drags it down). From a clone, opt in once:
git fetch origin '+refs/anvil/attachments:refs/anvil/attachments'
git cat-file -p "refs/anvil/attachments:<hash>" > /tmp/att && # then Read /tmp/att
This uses the clone's existing git auth — no PAT — and works offline afterward.
Fallback — HTTP with a PAT (no clone, or the ref isn't fetched). Credentials
live in the git-ignored .anvil-credentials at the repo root (ANVIL_BASE_URL +
a read-only ANVIL_TOKEN); source it, then:
curl -fsS -H "Authorization: Bearer $ANVIL_TOKEN" \
"$ANVIL_BASE_URL/{owner}/{repo}/-/attachments/{hash}" -o /tmp/att && # Read it
The PAT is read-only (GET/HEAD only), safe to hold. If neither the ref nor
.anvil-credentials is available, ask the user rather than guessing.
Current status, resume notes, the agreed next steps (a/b/c), and the roadmap live in the TODO. Read it first:
@TODO.md