use std::io::BufRead;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{bail, Context, Result};
use git_storage_trait::{GitOps, GitServe, Oid, TxId};
use storage_git_gix::GixStore;
use super::store_cache::{self, Cache};
use super::{BeforeStore, MaintenanceReport, OpenRepo, PushReceipt};
static STORES: Cache<GixStore> = Cache::new();
fn open_shared(git_dir: &Path, hash_kind: gix_hash::Kind) -> Result<Arc<GixStore>> {
let root = PathBuf::from(git_dir);
store_cache::shared(&STORES, root, || {
GixStore::open_or_init(git_dir, hash_kind)
.with_context(|| format!("opening the gix store at {}", git_dir.display()))
})
}
pub fn shutdown() {
store_cache::clear(&STORES);
}
pub fn open(git_dir: &Path, hash_kind: gix_hash::Kind) -> Result<Arc<OpenRepo>> {
Ok(OpenRepo::new(
git_dir,
hash_kind,
Arc::new(Served(open_shared(git_dir, hash_kind)?)),
))
}
struct Served(Arc<GixStore>);
impl Served {
fn has_bitmap(&self) -> bool {
let Ok(entries) = std::fs::read_dir(self.0.git_dir().join("objects").join("pack")) else {
return false;
};
entries
.flatten()
.filter(|entry| entry.path().extension().is_some_and(|e| e == "bitmap"))
.count()
== 1
}
}
impl GitOps for Served {
fn put(&self, pack: &[u8], refs: &[git_storage_trait::RefUpdate]) -> Result<TxId> {
self.0.put(pack, refs)
}
fn put_pack(&self, bytes: &[u8]) -> Result<TxId> {
self.0.put_pack(bytes)
}
fn put_pack_observed(
&self,
bytes: &[u8],
progress: &dyn git_storage_trait::PackProgress,
) -> Result<TxId> {
self.0.put_pack_observed(bytes, progress)
}
fn publishes_pack_progress(&self) -> bool {
self.0.publishes_pack_progress()
}
fn put_refs(&self, updates: &[git_storage_trait::RefUpdate]) -> Result<TxId> {
self.0.put_refs(updates)
}
fn get(&self, oid: Oid<'_>) -> Result<Option<git_storage_trait::Stored>> {
self.0.get(oid)
}
fn has(&self, oid: Oid<'_>) -> Result<bool> {
self.0.has(oid)
}
fn size(&self, oid: Oid<'_>) -> Result<Option<u64>> {
self.0.size(oid)
}
fn extents(&self, oids: &[Oid<'_>]) -> Result<Vec<Option<git_storage_trait::Extent>>> {
self.0.extents(oids)
}
fn refs(&self) -> Result<Vec<git_storage_trait::RefRow>> {
self.0.refs()
}
fn update_ref(&self, name: &str, old: Option<Oid<'_>>, new: Option<Oid<'_>>) -> Result<TxId> {
self.0.update_ref(name, old, new)
}
fn put_refs_cas(&self, edits: &[git_storage_trait::RefCas<'_>]) -> Result<TxId> {
self.0.put_refs_cas(edits)
}
fn reachable(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> Result<Vec<Vec<u8>>> {
self.0.reachable(want, have)
}
fn gc(&self) -> Result<git_storage_trait::GcReport> {
self.0.gc()
}
}
impl GitServe for Served {
fn read(&self, oid: Oid<'_>) -> Result<Option<(git_storage_trait::ObjType, Vec<u8>)>> {
self.0.read(oid)
}
fn header(&self, oid: Oid<'_>) -> Result<Option<(git_storage_trait::ObjType, u64)>> {
self.0.header(oid)
}
fn sizes(&self, oids: &[Oid<'_>]) -> Result<Vec<Option<u64>>> {
self.0.sizes(oids)
}
fn index_now(&self) -> Result<usize> {
self.0.index_now()
}
fn head(&self) -> Result<Option<git_storage_trait::RefRow>> {
self.0.head()
}
fn set_head(&self, target: &str) -> Result<TxId> {
self.0.set_head(target)
}
fn set_head_projected(&self, target: &str) -> Result<TxId> {
self.0.set_head_projected(target)
}
fn emit_pack(
&self,
objects: &[Oid<'_>],
have: &[Oid<'_>],
caps: &git_storage_trait::Caps,
out: &mut dyn std::io::Write,
) -> Result<git_storage_trait::PackStats> {
self.0.emit_pack(objects, have, caps, out)
}
fn select(
&self,
want: &[Oid<'_>],
have: &[Oid<'_>],
) -> Result<Option<git_storage_trait::ReachSet>> {
if !have.is_empty() && !self.has_bitmap() {
return Ok(None);
}
self.0.select(want, have)
}
}
pub fn ingest(
git_dir: &Path,
hash_kind: gix_hash::Kind,
pack: &mut dyn BufRead,
objects: super::ObjectSet,
progress: Option<&dyn git_storage_trait::PackProgress>,
before_store: Option<BeforeStore<'_>>,
) -> Result<PushReceipt> {
if objects == super::ObjectSet::Required {
bail!(
"the gix storage engine cannot enumerate the objects a push introduced: that is not \
one of the eleven, and this backend keeps no extent index to answer it from. The \
policy on this repository has a gate over the pushed object set, so the push is \
refused rather than passed on an answer the engine did not give. Serve this \
repository from the znippy engine, or drop the gate."
);
}
let started = std::time::Instant::now();
let store = open_shared(git_dir, hash_kind)?;
let pushed = super::read_pushed_pack_spooling(pack, hash_kind, Some(git_dir))?;
if let Some(hook) = before_store {
hook(&pushed.bytes).context("framing the pushed pack in the transaction log")?;
}
match progress {
Some(sink) => store.put_pack_observed(&pushed.bytes, sink),
None => store.put_pack(&pushed.bytes),
}
.context("storing the pushed pack")?;
let pack_id = super::pack_trailer(&pushed.bytes, hash_kind)?;
Ok(PushReceipt {
num_objects: pushed.num_objects,
version: pushed.version,
pack_id,
pack_bytes: pushed.bytes.len() as u64,
new_oids: None,
elapsed_ms: started.elapsed().as_millis() as u64,
})
}
pub fn maintain(git_dir: &Path, hash_kind: gix_hash::Kind) -> Result<MaintenanceReport> {
let store = open_shared(git_dir, hash_kind)?;
let report = store.gc().context("sweeping dead packs")?;
Ok(MaintenanceReport {
strategy: report.strategy.to_owned(),
bytes_before: report.bytes_before,
bytes_after: report.bytes_after,
rows: report.rows,
retired_packs: report.retired_packs,
verified: report.verified,
sections_sealed: 0,
})
}
#[cfg(test)]
mod tests {
use std::process::Command;
use super::*;
fn tiny_pack(work: &Path) -> (Vec<u8>, String) {
let run = |args: &[&str]| {
let out = Command::new("git")
.args(args)
.current_dir(work)
.output()
.expect("git is on PATH (xtask preflight asserts it)");
assert!(
out.status.success(),
"git {args:?}: {:?}",
out.status.code()
);
out.stdout
};
run(&["init", "-q", "--initial-branch=main", "."]);
run(&["config", "user.email", "t@t"]);
run(&["config", "user.name", "t"]);
std::fs::write(work.join("f"), b"hello").expect("fixture file");
run(&["add", "f"]);
run(&["commit", "-qm", "one"]);
let oid = String::from_utf8(run(&["rev-parse", "HEAD"]))
.expect("utf8")
.trim()
.to_owned();
let mut child = Command::new("git")
.args(["pack-objects", "--revs", "--stdout", "-q"])
.current_dir(work)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.expect("git pack-objects");
{
use std::io::Write as _;
let mut stdin = child.stdin.take().expect("stdin");
writeln!(stdin, "{oid}").expect("write rev");
}
let out = child.wait_with_output().expect("pack-objects output");
assert!(out.status.success(), "git pack-objects failed");
assert!(!out.stdout.is_empty(), "git pack-objects produced no pack");
(out.stdout, oid)
}
#[test]
fn a_push_is_visible_to_a_reader_opened_before_it() {
let dir = tempfile::tempdir().expect("tempdir");
let work = dir.path().join("work");
std::fs::create_dir_all(&work).expect("work dir");
let (pack, oid_hex) = tiny_pack(&work);
let git_dir = dir.path().join("repo.git");
let kind = gix_hash::Kind::Sha1;
let reader = open(&git_dir, kind).expect("opening the repository");
let raw = gix_hash::ObjectId::from_hex(oid_hex.as_bytes()).expect("oid");
assert!(
!reader.serve().has(raw.as_slice()).expect("has() before"),
"the repository already held the commit before it was pushed, so this test would \
pass without the push and prove nothing"
);
let mut cursor: &[u8] = &pack;
ingest(
&git_dir,
kind,
&mut cursor,
crate::storage::ObjectSet::NotNeeded,
None,
None,
)
.expect("ingesting the pushed pack");
assert!(
reader.serve().has(raw.as_slice()).expect("has() after"),
"the reader opened before the push cannot see it: the ingest wrote through a \
different store handle and refreshed that one instead"
);
let got = reader
.serve()
.get(raw.as_slice())
.expect("get() must not error")
.expect("the commit must have stored bytes to copy");
assert!(
!got.bytes.is_empty(),
"the commit resolved to zero stored bytes, which is what `emit_pack` copies"
);
shutdown();
}
fn two_commits(work: &Path) -> (Vec<u8>, String, String) {
let run = |args: &[&str]| {
let out = Command::new("git")
.args(args)
.current_dir(work)
.output()
.expect("git is on PATH (xtask preflight asserts it)");
assert!(
out.status.success(),
"git {args:?}: {:?}",
out.status.code()
);
String::from_utf8(out.stdout)
.expect("utf8")
.trim()
.to_owned()
};
run(&["init", "-q", "--initial-branch=main", "."]);
run(&["config", "user.email", "t@t"]);
run(&["config", "user.name", "t"]);
std::fs::write(work.join("f"), b"one").expect("fixture file");
run(&["add", "f"]);
run(&["commit", "-qm", "one"]);
let first = run(&["rev-parse", "HEAD"]);
std::fs::write(work.join("f"), b"two").expect("fixture file");
run(&["commit", "-qam", "two"]);
let second = run(&["rev-parse", "HEAD"]);
let mut child = Command::new("git")
.args(["pack-objects", "--all", "--revs", "--stdout", "-q"])
.current_dir(work)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.spawn()
.expect("git pack-objects");
let pack = std::io::Read::bytes(child.stdout.take().expect("stdout"))
.collect::<std::io::Result<Vec<u8>>>()
.expect("read the pack");
assert!(child.wait().expect("pack-objects").success());
(pack, first, second)
}
#[test]
fn a_fetch_with_haves_on_a_store_without_a_bitmap_is_left_to_the_walk() {
let dir = tempfile::tempdir().expect("tempdir");
let work = dir.path().join("work");
std::fs::create_dir_all(&work).expect("work dir");
let (pack, first, second) = two_commits(&work);
let git_dir = dir.path().join("repo.git");
let kind = gix_hash::Kind::Sha1;
let reader = open(&git_dir, kind).expect("opening the repository");
ingest(
&git_dir,
kind,
&mut pack.as_slice(),
crate::storage::ObjectSet::NotNeeded,
None,
None,
)
.expect("ingesting the pack");
let raw = |hex: &str| {
gix_hash::ObjectId::from_hex(hex.as_bytes())
.expect("oid")
.as_slice()
.to_vec()
};
let (first, second) = (raw(&first), raw(&second));
assert!(
reader
.serve()
.select(&[&second], &[&first])
.expect("select")
.is_none(),
"no bitmap and a `have`: the engine's answer is a closure over everything the client \
holds, so the server must walk instead"
);
assert!(
reader
.serve()
.select(&[&second], &[])
.expect("select")
.is_some(),
"a clone has no `have` side to close over; the engine answers it as before"
);
let bitmap = open_shared(&git_dir, kind)
.expect("the shared handle")
.write_reachability_bitmap()
.expect("write a bitmap");
assert!(
bitmap.is_some(),
"one pack, closed: a bitmap must be written"
);
assert!(
reader
.serve()
.select(&[&second], &[&first])
.expect("select")
.is_some(),
"with a bitmap the engine answers a fetch cheaply, and it is asked"
);
shutdown();
}
}