1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
//! **A tiny hand-built git, for tests that must know the answer in advance.**
//!
//! `blame` and `search` are only worth trusting if a test can assert the *exact*
//! commit a line came from or the *exact* file:line a string sits at — not
//! merely that some result came back (measured, not asserted). That needs a repository whose
//! commit graph the test authored: this builds blobs, trees and commits into a
//! real [`MemGitServe`], the same in-memory backend the store conformance suite
//! runs against, so the oids are real git object ids and `blame`/`search` walk
//! them through exactly the readers the appliance uses.
//!
//! Each object is stored as its own one-object pack. [`MemGitServe`] indexes a
//! pack on `put_pack` and never checks connectivity *across* packs, so a tree
//! may name a blob a prior pack carried — which is what lets these helpers build
//! a graph one object at a time.
use gunnar_store::conformance::pack::PackBuilder;
use gunnar_store::git_storage_trait::{GitOps, GitServe};
use gunnar_store::mem::MemGitServe;
use gix_hash::ObjectId;
use gix_object::Kind;
/// The tree mode of a regular non-executable file, as git writes it.
pub const TREE_MODE_BLOB: &str = "100644";
/// The tree mode of a subtree.
pub const TREE_MODE_TREE: &str = "40000";
/// A repository the test wrote, object by object.
pub struct Repo {
git: MemGitServe,
hash: gix_hash::Kind,
}
impl Repo {
/// A fresh, empty sha256 repository.
pub fn new() -> Self {
let hash = gix_hash::Kind::Sha256;
Repo {
git: MemGitServe::new(hash),
hash,
}
}
/// The backend, as the trait object `blame_file` and `search_tree` take.
pub fn serve(&self) -> &dyn GitServe {
&self.git
}
/// The hash kind, for a caller that must pass it alongside [`Repo::serve`].
pub fn hash_kind(&self) -> gix_hash::Kind {
self.hash
}
/// Store one object and return the id git would give it.
fn put(&self, kind: Kind, content: Vec<u8>) -> ObjectId {
let id = gix_object::compute_hash(self.hash, kind, &content).expect("hashing an object");
let mut builder = PackBuilder::new(self.hash);
builder.object(kind, content);
let fixture = builder.build().expect("building a one-object pack");
self.git.put_pack(&fixture.bytes).expect("storing the pack");
id
}
/// A blob holding `content`.
pub fn blob(&self, content: &[u8]) -> ObjectId {
self.put(Kind::Blob, content.to_vec())
}
/// A tree of `(mode, name, oid)` entries.
///
/// Entries are sorted by name, as git writes them; the readers do not depend
/// on the order, but a canonical tree keeps the oids the ones stock git
/// would compute.
pub fn tree(&self, entries: &[(&str, &str, ObjectId)]) -> ObjectId {
let mut sorted = entries.to_vec();
sorted.sort_by(|a, b| a.1.as_bytes().cmp(b.1.as_bytes()));
let mut body = Vec::new();
for (mode, name, oid) in sorted {
body.extend_from_slice(mode.as_bytes());
body.push(b' ');
body.extend_from_slice(name.as_bytes());
body.push(0);
body.extend_from_slice(oid.as_bytes());
}
self.put(Kind::Tree, body)
}
/// A commit of `tree` with `parents`, authored (and committed) by `author`.
pub fn commit(
&self,
tree: ObjectId,
parents: &[ObjectId],
author: &str,
message: &str,
) -> ObjectId {
let mut body = format!("tree {}\n", tree.to_hex());
for parent in parents {
body.push_str(&format!("parent {}\n", parent.to_hex()));
}
let who = format!("{author} <{author}@example.com> 1700000000 +0000");
body.push_str(&format!("author {who}\n"));
body.push_str(&format!("committer {who}\n\n"));
body.push_str(message);
body.push('\n');
self.put(Kind::Commit, body.into_bytes())
}
}