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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//! **A repository's first commit, written by the server** (the owner, 2026-10-08: a sold
//! private Gunnar is handed over with ONE welcome repository whose README thanks the buyer
//! and says how to start — with no git client anywhere in the sale).
//!
//! `Repos.Create{readme}` makes the repository and then this: one blob (the README), one tree
//! holding it, one commit of that tree with no parent, as ONE pack in the repository's own
//! hash (sha1 or sha256), put through the repository's store with the default branch set to
//! the commit — the same `GitOps::put` a push lands through, so the transaction log (and the
//! twin) carries it like any push.
//!
//! The objects are git's own formats, written here without `gix-object` (which this crate
//! keeps out of its shipped dependencies); the unit tests read them back through the store's
//! reader and check every id against `gix-object`'s (`tests/first_commit.rs`).
use std::io::Write as _;
/// The identity the first commit is written as: the server, never a person.
pub const AUTHOR: &str = "Gunnar <noreply@gunnar.invalid>";
/// git's pack object type numbers.
const OBJ_COMMIT: u8 = 1;
const OBJ_TREE: u8 = 2;
const OBJ_BLOB: u8 = 3;
/// One object: its type number, its kind word and its content.
struct Object {
num: u8,
word: &'static str,
content: Vec<u8>,
}
/// The id of `content` as a `word` object in `kind`: the hash of `<word> <len>\0<content>`.
pub fn object_id(
kind: gix_hash::Kind,
word: &str,
content: &[u8],
) -> Result<gix_hash::ObjectId, String> {
let mut h = gix_hash::hasher(kind);
h.update(format!("{word} {}\0", content.len()).as_bytes());
h.update(content);
h.try_finalize()
.map_err(|e| format!("hashing a {word}: {e:?}"))
}
/// Pure: **the pack of a repository's first commit** — one file `name` holding `text`, in a
/// tree, committed with `message` at `when_unix` (UTC). Answers the pack's bytes and the
/// commit's id (hex).
pub fn first_commit_pack(
kind: gix_hash::Kind,
name: &str,
text: &[u8],
message: &str,
when_unix: i64,
) -> Result<(Vec<u8>, String), String> {
if name.is_empty() || name.contains('/') || name.contains('\0') {
return Err(format!(
"{name:?} is not a file name at the repository's root"
));
}
let blob_id = object_id(kind, "blob", text)?;
let mut tree = Vec::new();
tree.extend_from_slice(format!("100644 {name}\0").as_bytes());
tree.extend_from_slice(blob_id.as_bytes());
let tree_id = object_id(kind, "tree", &tree)?;
let ident = format!("{AUTHOR} {when_unix} +0000");
let message = if message.ends_with('\n') {
message.to_string()
} else {
format!("{message}\n")
};
let commit = format!(
"tree {}\nauthor {ident}\ncommitter {ident}\n\n{message}",
tree_id.to_hex()
)
.into_bytes();
let commit_id = object_id(kind, "commit", &commit)?;
let objects = [
Object {
num: OBJ_BLOB,
word: "blob",
content: text.to_vec(),
},
Object {
num: OBJ_TREE,
word: "tree",
content: tree,
},
Object {
num: OBJ_COMMIT,
word: "commit",
content: commit,
},
];
Ok((pack(kind, &objects)?, commit_id.to_hex().to_string()))
}
/// A version-2 pack of whole objects, its trailer in `kind`.
fn pack(kind: gix_hash::Kind, objects: &[Object]) -> Result<Vec<u8>, String> {
let mut out = Vec::new();
out.extend_from_slice(b"PACK");
out.extend_from_slice(&2u32.to_be_bytes());
out.extend_from_slice(&(objects.len() as u32).to_be_bytes());
for o in objects {
debug_assert!(!o.word.is_empty());
// The header: type in bits 4-6 of the first byte, the size in 4 + 7·n bits.
let mut size = o.content.len() as u64;
let mut byte = (o.num << 4) | (size & 0x0f) as u8;
size >>= 4;
while size > 0 {
out.push(byte | 0x80);
byte = (size & 0x7f) as u8;
size >>= 7;
}
out.push(byte);
let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
z.write_all(&o.content).map_err(|e| e.to_string())?;
out.extend_from_slice(&z.finish().map_err(|e| e.to_string())?);
}
let mut h = gix_hash::hasher(kind);
h.update(&out);
let trailer = h
.try_finalize()
.map_err(|e| format!("the pack's trailer: {e:?}"))?;
out.extend_from_slice(trailer.as_bytes());
Ok(out)
}
// The proof (every id against `gix-object`, read back through the store a push lands in) is
// `tests/first_commit.rs`: this tier names no `gix_object` (xtask `engine_seam`).