use std::io::Write as _;
pub const AUTHOR: &str = "Gunnar <noreply@gunnar.invalid>";
const OBJ_COMMIT: u8 = 1;
const OBJ_TREE: u8 = 2;
const OBJ_BLOB: u8 = 3;
struct Object {
num: u8,
word: &'static str,
content: Vec<u8>,
}
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:?}"))
}
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()))
}
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());
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)
}