gunnar_server/
first_commit.rs1use std::io::Write as _;
16
17pub const AUTHOR: &str = "Gunnar <noreply@gunnar.invalid>";
19
20const OBJ_COMMIT: u8 = 1;
22const OBJ_TREE: u8 = 2;
23const OBJ_BLOB: u8 = 3;
24
25struct Object {
27 num: u8,
28 word: &'static str,
29 content: Vec<u8>,
30}
31
32pub 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
45pub 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
97fn 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 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