Skip to main content

gunnar_server/
first_commit.rs

1//! **A repository's first commit, written by the server** (the owner, 2026-10-08: a sold
2//! private Gunnar is handed over with ONE welcome repository whose README thanks the buyer
3//! and says how to start — with no git client anywhere in the sale).
4//!
5//! `Repos.Create{readme}` makes the repository and then this: one blob (the README), one tree
6//! holding it, one commit of that tree with no parent, as ONE pack in the repository's own
7//! hash (sha1 or sha256), put through the repository's store with the default branch set to
8//! the commit — the same `GitOps::put` a push lands through, so the transaction log (and the
9//! twin) carries it like any push.
10//!
11//! The objects are git's own formats, written here without `gix-object` (which this crate
12//! keeps out of its shipped dependencies); the unit tests read them back through the store's
13//! reader and check every id against `gix-object`'s (`tests/first_commit.rs`).
14
15use std::io::Write as _;
16
17/// The identity the first commit is written as: the server, never a person.
18pub const AUTHOR: &str = "Gunnar <noreply@gunnar.invalid>";
19
20/// git's pack object type numbers.
21const OBJ_COMMIT: u8 = 1;
22const OBJ_TREE: u8 = 2;
23const OBJ_BLOB: u8 = 3;
24
25/// One object: its type number, its kind word and its content.
26struct Object {
27    num: u8,
28    word: &'static str,
29    content: Vec<u8>,
30}
31
32/// The id of `content` as a `word` object in `kind`: the hash of `<word> <len>\0<content>`.
33pub fn object_id(
34    kind: gix_hash::Kind,
35    word: &str,
36    content: &[u8],
37) -> Result<gix_hash::ObjectId, String> {
38    let mut h = gix_hash::hasher(kind);
39    h.update(format!("{word} {}\0", content.len()).as_bytes());
40    h.update(content);
41    h.try_finalize()
42        .map_err(|e| format!("hashing a {word}: {e:?}"))
43}
44
45/// Pure: **the pack of a repository's first commit** — one file `name` holding `text`, in a
46/// tree, committed with `message` at `when_unix` (UTC). Answers the pack's bytes and the
47/// commit's id (hex).
48pub fn first_commit_pack(
49    kind: gix_hash::Kind,
50    name: &str,
51    text: &[u8],
52    message: &str,
53    when_unix: i64,
54) -> Result<(Vec<u8>, String), String> {
55    if name.is_empty() || name.contains('/') || name.contains('\0') {
56        return Err(format!(
57            "{name:?} is not a file name at the repository's root"
58        ));
59    }
60    let blob_id = object_id(kind, "blob", text)?;
61    let mut tree = Vec::new();
62    tree.extend_from_slice(format!("100644 {name}\0").as_bytes());
63    tree.extend_from_slice(blob_id.as_bytes());
64    let tree_id = object_id(kind, "tree", &tree)?;
65    let ident = format!("{AUTHOR} {when_unix} +0000");
66    let message = if message.ends_with('\n') {
67        message.to_string()
68    } else {
69        format!("{message}\n")
70    };
71    let commit = format!(
72        "tree {}\nauthor {ident}\ncommitter {ident}\n\n{message}",
73        tree_id.to_hex()
74    )
75    .into_bytes();
76    let commit_id = object_id(kind, "commit", &commit)?;
77    let objects = [
78        Object {
79            num: OBJ_BLOB,
80            word: "blob",
81            content: text.to_vec(),
82        },
83        Object {
84            num: OBJ_TREE,
85            word: "tree",
86            content: tree,
87        },
88        Object {
89            num: OBJ_COMMIT,
90            word: "commit",
91            content: commit,
92        },
93    ];
94    Ok((pack(kind, &objects)?, commit_id.to_hex().to_string()))
95}
96
97/// A version-2 pack of whole objects, its trailer in `kind`.
98fn pack(kind: gix_hash::Kind, objects: &[Object]) -> Result<Vec<u8>, String> {
99    let mut out = Vec::new();
100    out.extend_from_slice(b"PACK");
101    out.extend_from_slice(&2u32.to_be_bytes());
102    out.extend_from_slice(&(objects.len() as u32).to_be_bytes());
103    for o in objects {
104        debug_assert!(!o.word.is_empty());
105        // The header: type in bits 4-6 of the first byte, the size in 4 + 7·n bits.
106        let mut size = o.content.len() as u64;
107        let mut byte = (o.num << 4) | (size & 0x0f) as u8;
108        size >>= 4;
109        while size > 0 {
110            out.push(byte | 0x80);
111            byte = (size & 0x7f) as u8;
112            size >>= 7;
113        }
114        out.push(byte);
115        let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
116        z.write_all(&o.content).map_err(|e| e.to_string())?;
117        out.extend_from_slice(&z.finish().map_err(|e| e.to_string())?);
118    }
119    let mut h = gix_hash::hasher(kind);
120    h.update(&out);
121    let trailer = h
122        .try_finalize()
123        .map_err(|e| format!("the pack's trailer: {e:?}"))?;
124    out.extend_from_slice(trailer.as_bytes());
125    Ok(out)
126}
127
128// The proof (every id against `gix-object`, read back through the store a push lands in) is
129// `tests/first_commit.rs`: this tier names no `gix_object` (xtask `engine_seam`).