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
130
131
132
133
134
135
136
137
138
139
140
141
142
//! **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 items: Vec<(u8, &[u8])> = objects
.iter()
.map(|o| {
debug_assert!(!o.word.is_empty());
(o.num, o.content.as_slice())
})
.collect();
pack_of(kind, &items)
}
/// **A version-2 pack of whole objects** — `(type number, content)` each, its trailer in
/// `kind`. The one pack writer of the objects the server makes itself: a repository's
/// first commit here, a pull request's merge commit in [`crate::merge_engine`].
pub(crate) fn pack_of(kind: gix_hash::Kind, objects: &[(u8, &[u8])]) -> 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 (num, content) in objects {
// The header: type in bits 4-6 of the first byte, the size in 4 + 7·n bits.
let mut size = content.len() as u64;
let mut byte = (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(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`).