use std::{fmt::Display, str::FromStr};
use bstr::ByteSlice;
use crate::{
errors::GitError,
hash::ObjectHash,
internal::object::{ObjectTrait, ObjectType, signature::Signature},
};
#[derive(
Eq,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
rkyv::Archive,
rkyv::Serialize,
rkyv::Deserialize,
)]
pub struct Commit {
pub id: ObjectHash,
pub tree_id: ObjectHash,
pub parent_commit_ids: Vec<ObjectHash>,
pub author: Signature,
pub committer: Signature,
pub message: String,
}
impl PartialEq for Commit {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Display for Commit {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "tree: {}", self.tree_id)?;
for parent in self.parent_commit_ids.iter() {
writeln!(f, "parent: {parent}")?;
}
writeln!(f, "author {}", self.author)?;
writeln!(f, "committer {}", self.committer)?;
writeln!(f, "{}", self.message)
}
}
impl Commit {
pub fn new(
author: Signature,
committer: Signature,
tree_id: ObjectHash,
parent_commit_ids: Vec<ObjectHash>,
message: &str,
) -> Commit {
let mut commit = Commit {
id: ObjectHash::default(),
tree_id,
parent_commit_ids,
author,
committer,
message: message.to_string(),
};
let hash = ObjectHash::from_type_and_data(ObjectType::Commit, &commit.to_data().unwrap());
commit.id = hash;
commit
}
pub fn from_tree_id(
tree_id: ObjectHash,
parent_commit_ids: Vec<ObjectHash>,
message: &str,
) -> Commit {
let author = Signature::from_data(
format!(
"author mega <admin@mega.org> {} +0800",
chrono::Utc::now().timestamp()
)
.to_string()
.into_bytes(),
)
.unwrap();
let committer = Signature::from_data(
format!(
"committer mega <admin@mega.org> {} +0800",
chrono::Utc::now().timestamp()
)
.to_string()
.into_bytes(),
)
.unwrap();
Commit::new(author, committer, tree_id, parent_commit_ids, message)
}
pub fn format_message(&self) -> String {
let mut lines = self.message.lines();
if let Some(pos) = self
.message
.lines()
.position(|line| line.contains("-----END PGP SIGNATURE-----"))
{
return self
.message
.lines()
.skip(pos + 1)
.find(|line| !line.trim().is_empty())
.map(|line| line.to_owned())
.unwrap_or_else(|| self.message.clone());
}
lines
.find(|line| !line.trim().is_empty())
.map(|line| line.to_owned())
.unwrap_or_else(|| self.message.clone())
}
}
impl ObjectTrait for Commit {
fn from_bytes(data: &[u8], hash: ObjectHash) -> Result<Self, GitError>
where
Self: Sized,
{
let mut commit = data;
let tree_end = commit.find_byte(0x0a).unwrap();
let tree_id: ObjectHash = ObjectHash::from_str(
String::from_utf8(commit[5..tree_end].to_owned()) .unwrap()
.as_str(),
)
.unwrap();
let binding = commit[tree_end + 1..].to_vec(); commit = &binding;
let author_begin = commit.find("author").unwrap();
let parent_commit_ids: Vec<ObjectHash> = commit[..author_begin]
.find_iter("parent")
.map(|parent| {
let parent_end = commit[parent..].find_byte(0x0a).unwrap();
ObjectHash::from_str(
String::from_utf8(commit[parent + 7..parent + parent_end].to_owned())
.unwrap()
.as_str(),
)
.unwrap()
})
.collect();
let binding = commit[author_begin..].to_vec();
commit = &binding;
let author =
Signature::from_data(commit[..commit.find_byte(0x0a).unwrap()].to_vec()).unwrap();
let binding = commit[commit.find_byte(0x0a).unwrap() + 1..].to_vec();
commit = &binding;
let committer =
Signature::from_data(commit[..commit.find_byte(0x0a).unwrap()].to_vec()).unwrap();
let message = unsafe {
String::from_utf8_unchecked(commit[commit.find_byte(0x0a).unwrap() + 1..].to_vec())
};
Ok(Commit {
id: hash,
tree_id,
parent_commit_ids,
author,
committer,
message,
})
}
fn get_type(&self) -> ObjectType {
ObjectType::Commit
}
fn get_size(&self) -> usize {
self.to_data().map(|data| data.len()).unwrap_or(0)
}
fn to_data(&self) -> Result<Vec<u8>, GitError> {
let mut data = Vec::new();
data.extend(b"tree ");
data.extend(self.tree_id.to_string().as_bytes());
data.extend(&[0x0a]);
for parent_tree_id in &self.parent_commit_ids {
data.extend(b"parent ");
data.extend(parent_tree_id.to_string().as_bytes());
data.extend(&[0x0a]);
}
data.extend(self.author.to_data()?);
data.extend(&[0x0a]);
data.extend(self.committer.to_data()?);
data.extend(&[0x0a]);
data.extend(self.message.as_bytes());
Ok(data)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
use crate::hash::{HashKind, set_hash_kind_for_test};
fn basic_commit() -> Commit {
let _guard = set_hash_kind_for_test(HashKind::Sha1);
let raw_commit = br#"tree 341e54913a3a43069f2927cc0f703e5a9f730df1
author benjamin.747 <benjamin.747@outlook.com> 1757467768 +0800
committer benjamin.747 <benjamin.747@outlook.com> 1757491219 +0800
gpgsig -----BEGIN PGP SIGNATURE-----
iQJNBAABCAA3FiEEs4MaYUV7JcjxsVMPyqxGczTZ6K4FAmjBMC4ZHGJlbmphbWlu
Ljc0N0BvdXRsb29rLmNvbQAKCRDKrEZzNNnorj73EADNpsyLAHsB3NgoeH+uy9Vq
G2+LRtlvqv3QMK7vbQUadXHlQYWk25SIk+WJ1kG1AnUy5fqOrLSDTA1ny+qwpH8O
+2sKCF/S1wlzqGWjCcRH5/ir9srsGIn9HbNqBjmU22NJ6Dt2jnqoUvtWfPwyqwWg
VpjYlj390cFdXTpH5hMvtlmUQB+zCSKtWQW2Ur64h/UsGtllARlACi+KHQQmA2/p
FLWNddvfJQpPM597DkGohQTD68g0PqOBhUkOHduHq7VHy68DVW+07bPNXK8JhJ8S
4dyV1sZwcVcov0GcKl0wUbEqzy4gf+zV7DQhkfrSRQMBdo5vCWahYj1AbgaTiu8a
hscshYDuWWqpxBU/+nCxOPskV29uUG1sRyXp3DqmKJZpnO9CVdw3QaVrqnMEeh2S
t/wYRI9aI1A+Mi/DETom5ifTVygMkK+3m1h7pAMOlblFEdZx2sDXPRG2IEUcatr4
Jb2+7PUJQXxUQnwHC7xHHxRh6a2h8TfEJfSoEyrgzxZ0CRxJ6XMJaJu0UwZ2xMsx
Lgmeu6miB/imwxz5R5RL2yVHbgllSlO5l12AIeBaPoarKXYPSALigQnKCXu5OM3x
Jq5qsSGtxdr6S1VgLyYHR4o69bQjzBp9K47J3IXqvrpo/ZiO/6Mspk2ZRWhGj82q
e3qERPp5b7+hA+M7jKPyJg==
=UeLf
-----END PGP SIGNATURE-----
test parse commit from bytes
"#;
let hash = ObjectHash::from_str("57d7685c60213a9da465cf900f31933be3a7ee39").unwrap();
Commit::from_bytes(raw_commit, hash).unwrap()
}
fn basic_commit_sha256() -> Commit {
let _guard = set_hash_kind_for_test(HashKind::Sha256);
let raw_commit = br#"tree 0250024cf99636335fff1070e4220c5d8f67cb8633572d54b304629ad5382760
parent 33324c6819589e8eed81d6c72f216469151a0f2dbe7f42ba021d8b63049eb754
author jackieismpc <jackieismpc@gmail.com> 1764061895 +0800
committer jackieismpc <jackieismpc@gmail.com> 1764061895 +0800
gpgsig-sha256 -----BEGIN PGP SIGNATURE-----
iQIzBAABCAAdFiEEzW/BI6wDXimDk/4lItD7G/h4TUsFAmklcscACgkQItD7G/h4
TUtKFRAAtJq9tdl9XdND1ef2dXVQYCkQQlSdNHe2AR/QRVOPI39ZjD5aajRmZoE2
rKDenNML1ruiGEm+K3ntRDjus+3QF5Xkhj1D6eImQt6RXyOlo64I+GLRKlzw80Sl
hrd+l1eeuS4n46Z0U9fo1Qgc/crSn2VhUtLHJjvRntJoOb1vNreI2Y42Zmal3oVT
fQNQ7mqzh3KuWoa8T6nVrLaLH1vl9qhRgkPcIRbFf+ECbB96qykHqcbdHuneSgfx
+REpr1cedilkQlX81JrQ8Ntf4QFUPPHALl27/G6oPLT714cflEbvcFw7rNR+ktcD
ZJIMu5Cl7X3/v5e0od/hF9uPfiLHckUsOXiMFLfqRdZx/5XeQFWRpq4eYcW7e89e
3wJoBA2lCk8SHTBfsprKMpAweXJF9FCjRT5f9Zse2grqH81aQeNJnpSOoCq86oc/
nxhi8+rbIbClLCGQoGF7sE/fvmKqcex++JnXHcHTtK002Gnh3oHX07sbahlcGuYY
kg4QhXiLTQ5GfXnEnTPdFqbOVG02vEEsNeRgkmOz4c8Pm1FTDyOkuXd/Igvy7A9R
MZwQcJ6E4MnsMnoH8FKswGqCD7ftwtJtRzryORBVzvPKALufIXDVLyBbae9dxdej
bcpUK1bGtDljlwNtbLIOu+F1y2OVh7Tn3zxaQLcEhbUe2tP6rGk=
=nJMO
-----END PGP SIGNATURE-----
signed sha256 commit for test"#;
let hash = ObjectHash::from_str(
"ed43b50437e260a4d8fedacbaa38bad28b54cc424925e4180d9f186afaa0508c",
)
.unwrap();
Commit::from_bytes(raw_commit.as_bytes(), hash).unwrap()
}
#[test]
fn test_from_bytes_with_gpgsig() {
let commit = basic_commit();
assert_eq!(
commit.id,
ObjectHash::from_str("57d7685c60213a9da465cf900f31933be3a7ee39").unwrap()
);
assert_eq!(
commit.tree_id,
ObjectHash::from_str("341e54913a3a43069f2927cc0f703e5a9f730df1").unwrap()
);
assert_eq!(commit.author.name, "benjamin.747");
assert_eq!(commit.author.email, "benjamin.747@outlook.com");
assert_eq!(commit.committer.name, "benjamin.747");
assert!(commit.message.contains("-----BEGIN PGP SIGNATURE-----"));
assert!(commit.message.contains("-----END PGP SIGNATURE-----"));
assert!(commit.message.contains("test parse commit from bytes"));
}
#[test]
fn test_from_bytes_with_gpgsig_sha256() {
let commit = basic_commit_sha256();
assert_eq!(
commit.id,
ObjectHash::from_str(
"ed43b50437e260a4d8fedacbaa38bad28b54cc424925e4180d9f186afaa0508c"
)
.unwrap()
);
assert_eq!(
commit.tree_id,
ObjectHash::from_str(
"0250024cf99636335fff1070e4220c5d8f67cb8633572d54b304629ad5382760"
)
.unwrap()
);
assert_eq!(commit.author.name, "jackieismpc");
assert_eq!(commit.author.email, "jackieismpc@gmail.com");
assert_eq!(commit.committer.name, "jackieismpc");
assert!(commit.message.contains("-----BEGIN PGP SIGNATURE-----"));
assert!(commit.message.contains("-----END PGP SIGNATURE-----"));
assert!(commit.message.contains("signed sha256 commit for test"));
}
#[test]
fn test_format_message_with_pgp_signature() {
let _guard = set_hash_kind_for_test(HashKind::Sha1);
let commit = basic_commit();
assert_eq!(commit.format_message(), "test parse commit from bytes");
}
#[test]
fn test_format_message_with_pgp_signature_sha256() {
let _guard = set_hash_kind_for_test(HashKind::Sha256);
let commit = basic_commit_sha256();
assert_eq!(commit.format_message(), "signed sha256 commit for test");
}
}