#![allow(dead_code)]
use gix_hash::{Kind, ObjectId};
use gunnar_pktline::{decode, Decoded, Packet};
use gunnar_store::git_storage_trait::{
Caps, Extent, GcReport, GitOps, GitServe, ObjType, Oid, PackStats, ReachSet, RefCas, RefRow,
RefUpdate, Stored, TxId,
};
use gunnar_store::mem::MemGitServe;
pub struct Fixture {
pub git: Counting<MemGitServe>,
pub kind: Kind,
pub one: ObjectId,
pub two: ObjectId,
pub three: ObjectId,
pub merge: ObjectId,
pub four: ObjectId,
pub tag: ObjectId,
}
impl Fixture {
pub fn build(kind: Kind) -> Self {
let git = Counting::new(MemGitServe::new(kind));
let one = commit(&git, kind, "a.txt", "one", &[], 1_700_000_001);
let two = commit(&git, kind, "b.txt", "two", &[one], 1_700_000_002);
let three = commit(&git, kind, "c.txt", "three", &[one], 1_700_000_003);
let merge = commit(&git, kind, "d.txt", "merge", &[two, three], 1_700_000_004);
let four = commit(&git, kind, "e.txt", "four", &[merge], 1_700_000_005);
let tag = write_object(&git, kind, gix_object::Kind::Tag, &tag_bytes(&merge, "v1"));
git.put_refs(&[
RefUpdate::set("refs/heads/main", four.to_string()),
RefUpdate::set("refs/heads/side", three.to_string()),
RefUpdate::set("refs/tags/v1", tag.to_string()),
])
.expect("install the references");
git.set_head("refs/heads/main").expect("point HEAD at main");
assert!(
!git.refs()
.expect("list refs")
.iter()
.any(|row| row.name == "HEAD"),
"the fixture store publishes HEAD as a row; the contract says it must not"
);
Fixture {
git,
kind,
one,
two,
three,
merge,
four,
tag,
}
}
pub fn extend(&self, tip: ObjectId, count: usize, prefix: &str) -> ObjectId {
self.extend_dated(tip, count, prefix, |index| 1_700_100_000 + index as i64)
}
pub fn extend_dated(
&self,
tip: ObjectId,
count: usize,
prefix: &str,
at: impl Fn(usize) -> i64,
) -> ObjectId {
let mut head = tip;
for index in 0..count {
head = commit(
&self.git,
self.kind,
&format!("{prefix}{index}.txt"),
&format!("{prefix} {index}"),
&[head],
at(index),
);
}
head
}
pub fn first_parent_haves(&self, tip: ObjectId, count: usize) -> Vec<ObjectId> {
let mut out = Vec::with_capacity(count);
let mut current = tip;
for _ in 0..count {
out.push(current);
let (_, data) = self
.git
.read(current.as_bytes())
.expect("read a commit")
.expect("the commit is here");
let parent = gix_object::CommitRef::from_bytes(&data, self.kind)
.expect("parse a commit")
.parents()
.next();
match parent {
Some(id) => current = id,
None => break,
}
}
out
}
pub fn force_ref(&self, name: &str, id: ObjectId) {
self.git
.put_refs(&[RefUpdate::set(name, id.to_string())])
.unwrap_or_else(|e| panic!("install {name}: {e:#}"));
}
pub fn read_ref(&self, name: &str) -> Option<ObjectId> {
self.git
.refs()
.expect("list refs")
.into_iter()
.find(|row| row.name == name)
.and_then(|row| row.oid)
.map(|raw| ObjectId::try_from(raw.as_slice()).expect("a well-sized oid"))
}
pub fn ref_names(&self) -> Vec<String> {
let mut names: Vec<String> = self
.git
.refs()
.expect("list refs")
.into_iter()
.map(|row| row.name)
.collect();
names.sort();
names
}
pub fn clear_refs(&self) {
let names: Vec<RefUpdate> = self
.git
.refs()
.expect("list refs")
.into_iter()
.map(|row| RefUpdate::delete(row.name))
.collect();
if !names.is_empty() {
self.git.put_refs(&names).expect("delete every reference");
}
}
}
pub struct Counting<S = MemGitServe> {
pub inner: S,
reads: std::sync::atomic::AtomicUsize,
commit_reads: std::sync::atomic::AtomicUsize,
headers: std::sync::atomic::AtomicUsize,
contains: std::sync::atomic::AtomicUsize,
ref_txns: std::sync::atomic::AtomicUsize,
}
impl<S: GitServe> Counting<S> {
pub fn new(inner: S) -> Self {
Counting {
inner,
reads: std::sync::atomic::AtomicUsize::new(0),
commit_reads: std::sync::atomic::AtomicUsize::new(0),
headers: std::sync::atomic::AtomicUsize::new(0),
contains: std::sync::atomic::AtomicUsize::new(0),
ref_txns: std::sync::atomic::AtomicUsize::new(0),
}
}
pub fn reset(&self) {
for counter in [
&self.reads,
&self.commit_reads,
&self.headers,
&self.contains,
&self.ref_txns,
] {
counter.store(0, std::sync::atomic::Ordering::SeqCst);
}
}
pub fn read_count(&self) -> usize {
self.reads.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn commit_read_count(&self) -> usize {
self.commit_reads.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn ref_txn_count(&self) -> usize {
self.ref_txns.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn contains_count(&self) -> usize {
self.contains.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn touches(&self) -> usize {
self.reads.load(std::sync::atomic::Ordering::SeqCst)
+ self.headers.load(std::sync::atomic::Ordering::SeqCst)
+ self.contains.load(std::sync::atomic::Ordering::SeqCst)
}
}
impl<S: GitServe> GitOps for Counting<S> {
fn put(&self, pack: &[u8], refs: &[RefUpdate]) -> anyhow::Result<TxId> {
self.inner.put(pack, refs)
}
fn put_pack(&self, bytes: &[u8]) -> anyhow::Result<TxId> {
self.inner.put_pack(bytes)
}
fn put_refs(&self, updates: &[RefUpdate]) -> anyhow::Result<TxId> {
self.inner.put_refs(updates)
}
fn get(&self, oid: Oid<'_>) -> anyhow::Result<Option<Stored>> {
self.inner.get(oid)
}
fn has(&self, oid: Oid<'_>) -> anyhow::Result<bool> {
self.contains
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
self.inner.has(oid)
}
fn size(&self, oid: Oid<'_>) -> anyhow::Result<Option<u64>> {
self.inner.size(oid)
}
fn extents(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<Extent>>> {
self.inner.extents(oids)
}
fn refs(&self) -> anyhow::Result<Vec<RefRow>> {
self.inner.refs()
}
fn update_ref(
&self,
name: &str,
old: Option<Oid<'_>>,
new: Option<Oid<'_>>,
) -> anyhow::Result<TxId> {
self.inner.update_ref(name, old, new)
}
fn put_refs_cas(&self, edits: &[RefCas<'_>]) -> anyhow::Result<TxId> {
self.ref_txns
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
self.inner.put_refs_cas(edits)
}
fn reachable(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> anyhow::Result<Vec<Vec<u8>>> {
self.inner.reachable(want, have)
}
fn gc(&self) -> anyhow::Result<GcReport> {
self.inner.gc()
}
}
impl<S: GitServe> GitServe for Counting<S> {
fn read(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, Vec<u8>)>> {
self.reads.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let got = self.inner.read(oid)?;
if matches!(got, Some((ObjType::Commit, _))) {
self.commit_reads
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
Ok(got)
}
fn header(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, u64)>> {
self.headers
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
self.inner.header(oid)
}
fn sizes(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<u64>>> {
self.headers
.fetch_add(oids.len(), std::sync::atomic::Ordering::SeqCst);
self.inner.sizes(oids)
}
fn head(&self) -> anyhow::Result<Option<RefRow>> {
self.inner.head()
}
fn set_head(&self, target: &str) -> anyhow::Result<TxId> {
self.inner.set_head(target)
}
fn emit_pack(
&self,
want: &[Oid<'_>],
have: &[Oid<'_>],
caps: &Caps,
out: &mut dyn std::io::Write,
) -> anyhow::Result<PackStats> {
self.inner.emit_pack(want, have, caps, out)
}
fn select(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> anyhow::Result<Option<ReachSet>> {
self.inner.select(want, have)
}
}
pub fn name(text: &str) -> String {
gunnar_store::refname::validate(text).expect("test reference names are valid");
text.to_owned()
}
pub fn write_pack_of(
git: &dyn GitOps,
kind: Kind,
objects: &[(gix_object::Kind, Vec<u8>)],
) -> Vec<ObjectId> {
let mut bytes = Vec::new();
let count = u32::try_from(objects.len()).expect("a fixture under 4 billion objects");
let mut writer =
gunnar_pack::PackWriter::new(&mut bytes, count, &gunnar_pack::WriteOptions::new(kind))
.expect("start a pack");
let mut ids = Vec::with_capacity(objects.len());
for (obj_kind, data) in objects {
writer.add_object(*obj_kind, data).expect("add an object");
ids.push(gix_object::compute_hash(kind, *obj_kind, data).expect("hash an object"));
}
writer.finish().expect("finish the pack");
git.put_pack(&bytes).expect("store the pack");
ids
}
pub fn write_object(
git: &dyn GitOps,
kind: Kind,
obj_kind: gix_object::Kind,
bytes: &[u8],
) -> ObjectId {
write_pack_of(git, kind, &[(obj_kind, bytes.to_vec())])[0]
}
pub fn commit(
git: &dyn GitOps,
kind: Kind,
file: &str,
content: &str,
parents: &[ObjectId],
at: i64,
) -> ObjectId {
let blob_bytes = format!("{content}\n").into_bytes();
let blob =
gix_object::compute_hash(kind, gix_object::Kind::Blob, &blob_bytes).expect("hash the blob");
let tree_bytes = tree_bytes(file, &blob);
let tree =
gix_object::compute_hash(kind, gix_object::Kind::Tree, &tree_bytes).expect("hash the tree");
let mut text = format!("tree {}\n", tree.to_hex());
for parent in parents {
text.push_str(&format!("parent {}\n", parent.to_hex()));
}
text.push_str(&format!(
"author Gunnar Test <gunnar@example.invalid> {at} +0000\n\
committer Gunnar Test <gunnar@example.invalid> {at} +0000\n\n{content}\n"
));
let ids = write_pack_of(
git,
kind,
&[
(gix_object::Kind::Blob, blob_bytes),
(gix_object::Kind::Tree, tree_bytes),
(gix_object::Kind::Commit, text.into_bytes()),
],
);
ids[2]
}
fn tree_bytes(file: &str, blob: &gix_hash::oid) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(b"100644 ");
out.extend_from_slice(file.as_bytes());
out.push(0);
out.extend_from_slice(blob.as_bytes());
out
}
fn tag_bytes(target: &gix_hash::oid, tag: &str) -> Vec<u8> {
format!(
"object {}\ntype commit\ntag {tag}\n\
tagger Gunnar Test <gunnar@example.invalid> 1700000006 +0000\n\nrelease\n",
target.to_hex()
)
.into_bytes()
}
pub struct Response {
pub packets: Vec<Packet>,
pub tail: Vec<u8>,
}
impl Response {
pub fn parse(bytes: &[u8]) -> Self {
let mut packets = Vec::new();
let mut rest = bytes;
while let Ok(Decoded::Packet(packet, used)) = decode(rest) {
packets.push(packet);
rest = &rest[used..];
}
Response {
packets,
tail: rest.to_vec(),
}
}
pub fn lines(&self) -> Vec<String> {
self.packets
.iter()
.filter_map(|packet| packet.text_str().map(str::to_owned))
.collect()
}
pub fn has_line(&self, text: &str) -> bool {
self.lines().iter().any(|line| line == text)
}
pub fn band(&self, band: u8) -> Vec<u8> {
let mut out = Vec::new();
for packet in &self.packets {
if let Packet::Data(payload) = packet {
if payload.first() == Some(&band) {
out.extend_from_slice(&payload[1..]);
}
}
}
out
}
pub fn after_packfile_header(&self) -> Vec<u8> {
let mut out = Vec::new();
let mut seen = false;
for packet in &self.packets {
if let Packet::Data(payload) = packet {
if seen {
out.extend_from_slice(payload);
} else if payload.strip_suffix(b"\n").unwrap_or(payload) == b"packfile" {
seen = true;
}
}
}
out
}
}
pub fn v2_request(command: &str, capabilities: &[&str], arguments: &[&str]) -> Vec<u8> {
let mut out = Vec::new();
gunnar_pktline::write_text(&mut out, format!("command={command}").as_bytes()).unwrap();
for capability in capabilities {
gunnar_pktline::write_text(&mut out, capability.as_bytes()).unwrap();
}
gunnar_pktline::write_delim(&mut out).unwrap();
for argument in arguments {
gunnar_pktline::write_text(&mut out, argument.as_bytes()).unwrap();
}
gunnar_pktline::write_flush(&mut out).unwrap();
out
}
pub fn index_and_count(pack: &[u8], kind: Kind) -> u32 {
let mut cursor = std::io::Cursor::new(pack);
gunnar_pack::index_pack(
&mut cursor,
None,
&gunnar_pack::IndexOptions::new(kind),
None,
)
.expect("gix could not index the pack gunnar produced")
.num_objects
}
pub fn push_bytes(commands: &[(&str, &str, &str)], capabilities: &str, pack: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
for (index, (old, new, name)) in commands.iter().enumerate() {
let mut line = format!("{old} {new} {name}").into_bytes();
if index == 0 {
line.push(0);
line.extend_from_slice(capabilities.as_bytes());
}
gunnar_pktline::write_text(&mut out, &line).unwrap();
}
gunnar_pktline::write_flush(&mut out).unwrap();
out.extend_from_slice(pack);
out
}
fn as_receipt(indexed: gunnar_pack::IndexedPack) -> gunnar_wire::pack::IndexedPack {
gunnar_wire::pack::IndexedPack {
num_objects: indexed.num_objects,
pack_id: indexed.pack_id,
index_id: indexed.index_id,
pack_version: gunnar_wire::pack::PackVersion::from_u32(match indexed.pack_version {
gix_pack::data::Version::V3 => 3,
_ => 2,
}),
hash_kind: indexed.hash_kind,
index_path: indexed.index_path,
data_path: indexed.data_path,
keep_path: indexed.keep_path,
stored_bytes: None,
}
}
pub fn validating_ingest<E>(
kind: Kind,
) -> impl Fn(&mut dyn std::io::BufRead) -> std::result::Result<gunnar_wire::pack::IndexedPack, E>
where
E: From<gunnar_wire::Error>,
{
move |pack: &mut dyn std::io::BufRead| {
gunnar_pack::index_pack(pack, None, &gunnar_pack::IndexOptions::new(kind), None)
.map(as_receipt)
.map_err(|err| gunnar_wire::Error::pack(err).into())
}
}
pub fn empty_pack(kind: Kind) -> Vec<u8> {
gunnar_pack::empty_pack(kind).expect("an empty pack is legal")
}
pub fn limited_ingest<E>(
kind: Kind,
limit: usize,
) -> impl Fn(&mut dyn std::io::BufRead) -> std::result::Result<gunnar_wire::pack::IndexedPack, E>
where
E: From<gunnar_wire::Error>,
{
move |pack: &mut dyn std::io::BufRead| {
gunnar_pack::index_pack(
pack,
None,
&gunnar_pack::IndexOptions::new(kind).alloc_limit_bytes(limit),
None,
)
.map(as_receipt)
.map_err(|err| gunnar_wire::Error::pack(err).into())
}
}
pub struct Unindexed<'a>(pub &'a dyn GitServe);
impl GitOps for Unindexed<'_> {
fn put(&self, pack: &[u8], refs: &[RefUpdate]) -> anyhow::Result<TxId> {
self.0.put(pack, refs)
}
fn put_pack(&self, bytes: &[u8]) -> anyhow::Result<TxId> {
self.0.put_pack(bytes)
}
fn put_refs(&self, updates: &[RefUpdate]) -> anyhow::Result<TxId> {
self.0.put_refs(updates)
}
fn get(&self, oid: Oid<'_>) -> anyhow::Result<Option<Stored>> {
self.0.get(oid)
}
fn has(&self, oid: Oid<'_>) -> anyhow::Result<bool> {
self.0.has(oid)
}
fn size(&self, oid: Oid<'_>) -> anyhow::Result<Option<u64>> {
self.0.size(oid)
}
fn extents(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<Extent>>> {
self.0.extents(oids)
}
fn refs(&self) -> anyhow::Result<Vec<RefRow>> {
self.0.refs()
}
fn update_ref(
&self,
name: &str,
old: Option<Oid<'_>>,
new: Option<Oid<'_>>,
) -> anyhow::Result<TxId> {
self.0.update_ref(name, old, new)
}
fn put_refs_cas(&self, edits: &[RefCas<'_>]) -> anyhow::Result<TxId> {
self.0.put_refs_cas(edits)
}
fn reachable(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> anyhow::Result<Vec<Vec<u8>>> {
self.0.reachable(want, have)
}
fn gc(&self) -> anyhow::Result<GcReport> {
self.0.gc()
}
}
impl GitServe for Unindexed<'_> {
fn read(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, Vec<u8>)>> {
self.0.read(oid)
}
fn header(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, u64)>> {
self.0.header(oid)
}
fn sizes(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<u64>>> {
self.0.sizes(oids)
}
fn head(&self) -> anyhow::Result<Option<RefRow>> {
self.0.head()
}
fn set_head(&self, target: &str) -> anyhow::Result<TxId> {
self.0.set_head(target)
}
fn emit_pack(
&self,
want: &[Oid<'_>],
have: &[Oid<'_>],
caps: &Caps,
out: &mut dyn std::io::Write,
) -> anyhow::Result<PackStats> {
self.0.emit_pack(want, have, caps, out)
}
fn select(&self, _want: &[Oid<'_>], _have: &[Oid<'_>]) -> anyhow::Result<Option<ReachSet>> {
Ok(None)
}
}
pub fn incompressible_pack(kind: Kind, blobs: usize, size: usize) -> Vec<u8> {
let mut pb = gunnar_store::conformance::pack::PackBuilder::new(kind);
let mut state: u64 = 0x2545_F491_4F6C_DD1D;
for _ in 0..blobs {
let mut body = vec![0u8; size];
for byte in body.iter_mut() {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
*byte = (state >> 33) as u8;
}
pb.object(gix_object::Kind::Blob, body);
}
pb.build().expect("a pack of whole objects is legal").bytes
}
pub struct Tracked {
inner: std::io::Cursor<Vec<u8>>,
pub taken: u64,
}
impl Tracked {
pub fn new(bytes: Vec<u8>) -> Self {
Tracked {
inner: std::io::Cursor::new(bytes),
taken: 0,
}
}
}
impl std::io::Read for Tracked {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let read = std::io::Read::read(&mut self.inner, buf)?;
self.taken += read as u64;
Ok(read)
}
}
impl std::io::BufRead for Tracked {
fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
std::io::BufRead::fill_buf(&mut self.inner)
}
fn consume(&mut self, amount: usize) {
std::io::BufRead::consume(&mut self.inner, amount);
self.taken += amount as u64;
}
}