gitbaby
A Rust library that implements git server backend operations on top of
gix (gitoxide). It provides an
async, streaming-first API for reading and writing repository objects, refs,
trees, blobs, LFS pointers, archives, bundles and more — designed to be embedded
in a git hosting product (servers, admin tools, mirror agents).
Status
- Library only. No binary, no workspace. Single crate, edition
2024(requires rustc ≥ 1.85). - The streaming-first HTTP/git-protocol server surface (
src/server/) exists as a contract but is not implemented yet — see Server (planned).
Features
| Module | Capabilities |
|---|---|
commit | get_commit, rev_parse, is_ancestor, list_commits, commit_stats, get_tree_entries, last_commit_for_path |
branch | list_branches, find_branch, branch_exists, default-branch get/set, create_branch(_force), delete_branch, rename_branch, update_head |
tag | list_tags, find_tag, tag_exists, lightweight / annotated / signed tag creation, verify_tag_signature, delete_tag |
refs | list_refs, ref_exists, resolve_revision, update_ref, delete_ref, find_refs_by_oid, atomic update_refs_atomic / delete_refs_atomic |
tree | tree_latest_commit, commit_tree (fast tree writes), walk_tree, tree_entries_with_latest, diff_trees |
blob | streaming get_blob / stream_blob, get_blob_size, blob_exists, write_blob, list_blobs / list_all_blobs, LFS pointer listing (list_lfs_pointers, list_all_lfs_pointers, get_lfs_pointers) |
diff | ChangedPath-based diff requests (DiffRequest), NumStat, ShortStat, RangeDiffSpec, whitespace modes |
merge + conflict | merge_base, merge_tree, conflict listing (list_conflict_files), resolve_conflicts |
compare | DivergeObject — divergence analysis between refs |
remote | add_remote / remove_remote, get_remote_address, fetch_remote_commit, find_remote_repository (URL cap check), find_remote_root_ref, update_remote_mirror |
archive | create_archive, streaming get_archive, create_bundle |
setup | init_repository, is_repository_exists, fast_import (git-fast-import based bulk loading) |
cleanup | prune, apply_bfg_object_map (BFG-style object history rewriting) |
config | config_get/set/add/unset_all/get_all/get_regexp (repo + global scope), managed-config variants |
blame | BlameHunk-based async blame with Range limits |
submodule | list_submodules, get_submodule |
command | Low-level child git process execution: Cmd with order-preserving Env, streaming pipes, output cap, timeouts |
cache | foyer-backed LRU+Tiered cache for commits, tree entries, blob metadata, rev→oid mappings; LFS cache; per-config on-disk store |
server | Planned streaming git/http protocol surface (see below) |
Architecture
GitBaby (src/lib.rs) ← single public entrypoint
├─ facade: Arc<dyn RepositoryFacade> ← consumer-provided repo access
└─ feature modules (src/<feature>/)
mod.rs → re-exports public types
types.rs → request/response structs (serde, additive-only fields)
helper.rs → validation + git command construction / error classification
usecase.rs→ `impl GitBaby { pub async fn ... }` ← the public API
- Every feature is a set of methods implemented directly on
GitBabyviaimpl GitBabyblocks in eachusecase.rs. The struct itself is tiny: just anArc<dyn RepositoryFacade>. RepositoryFacade(src/repo.rs) is consumer-implemented (async_trait):git_repo_dir(),git_alternate_object_directories(),gix_repo(). There is no default implementation.- Most read/write operations shell out to the system
gitbinary throughCmd(src/command/cmd.rs) — ordered env, stdin/stdout/stderr handling, and a 64 MiB output cap (DEFAULT_OUTPUT_CAP_BYTES, enforced asCmdError::PayloadTooLarge). Metadata reads (commits, tree entries) go through thegixobject store, which is also the read path that feeds the cache. - Errors are hand-written:
BabyError(src/error.rs, ~50 variants) andCmdError(src/command/error.rs) implementDisplay+std::error::Errormanually. (thiserroris a declared dependency but deliberately unused.) Env(src/command/env.rs) is an order-preservingVec<(String, String)>;withappends,setreplaces in place. Env ordering matters for spawned processes.
Quick start
[dependencies]
gitbaby = "0.1"
gix = "0.86"
async-trait = "0.1"
tokio = { version = "1", features = ["full"] }
use std::path::{Path, PathBuf};
use std::sync::Arc;
use async_trait::async_trait;
use gitbaby::repo::RepositoryFacade;
use gitbaby::error::BabyError;
use gitbaby::GitBaby;
struct MyRepo(PathBuf);
#[async_trait]
impl RepositoryFacade for MyRepo {
async fn git_repo_dir(&self) -> Result<PathBuf, BabyError> {
Ok(self.0.clone())
}
async fn git_alternate_object_directories(&self) -> Result<Vec<PathBuf>, BabyError> {
Ok(Vec::new())
}
async fn gix_repo(&self) -> Result<gix::Repository, BabyError> {
gix::open(&self.0).map_err(|e| {
BabyError::Custom(format!("open {}: {e}", self.0.display()))
})
}
}
#[tokio::main]
async fn main() -> Result<(), BabyError> {
let git = GitBaby::new(Arc::new(MyRepo("/path/to/repo".into())));
let head = git.get_commit("HEAD").await?;
println!("{head:?}");
let tree = git.get_tree_entries("HEAD", Path::new("")).await?;
println!("{} entries", tree.len());
let oid = git.rev_parse("HEAD").await?;
println!("HEAD = {oid}");
let branches = git.list_branches(gitbaby::branch::ListBranchesOptions::default()).await?;
println!("{} branches", branches.len());
Ok(())
}
All operations are async and need a Tokio runtime (#[tokio::test] / #[tokio::main]).
Server (planned)
src/server/ defines the streaming-first server contract but every
ServerBackend method still returns ServerError::Unimplemented(...). The
contract is intentional:
- Methods:
authorize,list_refs_stream,produce_pack_stream,ingest_pack_stream,lfs_get_stream,lfs_put_stream(async-trait). - All bodies cross the API as
Box<dyn AsyncRead + Unpin + Send + Sync>(BoxedAsyncRead) — noVec<u8>bodies; ingest methods returnIngestReport. HeaderMap = HashMap<String, String>(nohttpcrate).- Output cap defaults to
DEFAULT_OUTPUT_CAP_BYTES(64 MiB); LFS uploads cap atDEFAULT_LFS_OBJECT_CAP_BYTES(5 GiB) —server::DEFAULT_LFS_OBJECT_CAP_BYTES. RefUpdateBatchis server-local (oid strings, nogixtypes);ServerRequestis notClone;StreamGuardisClone + Send + Syncwithcancel()/handle().
Do not implement this surface until a concrete git/lfs protocol task exists.
Cache
src/cache/is a single-process foyer-backed store: fixed on-disk directory per config, in-process invalidation hooks (hook_ref_updated,hook_blob_written). Do not share across processes on the same repo — metadata (rev→oid, blob-exists negatives) can go stale.- Namespaces: commit info (
NS_COMMIT), tree entries (NS_TREE_ENTRIES), rev→oid (NS_REV), blob metadata/exists (NS_BLOB,NS_BLOB_EXISTS,NS_BLOB_SIZE), LFS (NS_LFS). - Lifecycle:
init_global_cache/init_global_cache_async/shutdown_global_cache/try_global_cache;CachePools::open(cfg)/close().
Compatibility conventions
Serde-serialized cache values (CommitInfo, Vec<TreeEntry>, …) are
additive-only: never rename or remove a field, and every new field must be
#[serde(default)] so stale cached payloads still deserialize.
Testing
cargo test # unit + #[tokio::test] integration tests in tests/
cargo test --test <file>
cargo clippy --all-targets -- -D warnings
cargo fmt --check
License
TBD.