use std::io::BufRead;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{bail, Context, Result};
use git_storage_trait::GitOps;
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,
open_shared(git_dir, hash_kind)?,
))
}
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();
}
}