use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::hash::Hash;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Commit {
pub tree: Hash,
pub parents: Vec<Hash>,
pub author: String,
pub timestamp: i64,
pub message: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, String>,
}
impl Commit {
pub fn new(
tree: Hash,
parents: Vec<Hash>,
author: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
tree,
parents,
author: author.into(),
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0),
message: message.into(),
metadata: BTreeMap::new(),
}
}
pub fn with_timestamp(
tree: Hash,
parents: Vec<Hash>,
author: impl Into<String>,
timestamp: i64,
message: impl Into<String>,
) -> Self {
Self {
tree,
parents,
author: author.into(),
timestamp,
message: message.into(),
metadata: BTreeMap::new(),
}
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn is_root(&self) -> bool {
self.parents.is_empty()
}
pub fn is_merge(&self) -> bool {
self.parents.len() > 1
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_commit_new() {
let c = Commit::new(Hash::ZERO, vec![], "author", "message");
assert_eq!(c.tree, Hash::ZERO);
assert!(c.parents.is_empty());
assert_eq!(c.author, "author");
assert_eq!(c.message, "message");
assert!(c.is_root());
assert!(!c.is_merge());
}
#[test]
fn test_commit_with_parents() {
let parent =
Hash::from_hex("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789")
.unwrap();
let c = Commit::new(Hash::ZERO, vec![parent], "author", "message");
assert!(!c.is_root());
assert!(!c.is_merge());
}
#[test]
fn test_commit_merge() {
let p1 = Hash::from_hex("1111111111111111111111111111111111111111111111111111111111111111")
.unwrap();
let p2 = Hash::from_hex("2222222222222222222222222222222222222222222222222222222222222222")
.unwrap();
let c = Commit::new(Hash::ZERO, vec![p1, p2], "author", "merge");
assert!(c.is_merge());
}
#[test]
fn test_commit_with_metadata() {
let c = Commit::new(Hash::ZERO, vec![], "author", "message")
.with_metadata("key1", "value1")
.with_metadata("key2", "value2");
assert_eq!(c.metadata.get("key1"), Some(&"value1".to_string()));
assert_eq!(c.metadata.get("key2"), Some(&"value2".to_string()));
}
#[test]
fn test_commit_cbor_roundtrip() {
let c = Commit::with_timestamp(Hash::ZERO, vec![], "author", 1234567890, "message")
.with_metadata("foo", "bar");
let mut bytes = Vec::new();
ciborium::into_writer(&c, &mut bytes).unwrap();
let parsed: Commit = ciborium::from_reader(&bytes[..]).unwrap();
assert_eq!(c, parsed);
}
#[test]
fn test_commit_cbor_determinism() {
let mut c1 = Commit::with_timestamp(Hash::ZERO, vec![], "a", 0, "m");
c1.metadata.insert("z".to_string(), "1".to_string());
c1.metadata.insert("a".to_string(), "2".to_string());
let mut c2 = Commit::with_timestamp(Hash::ZERO, vec![], "a", 0, "m");
c2.metadata.insert("a".to_string(), "2".to_string());
c2.metadata.insert("z".to_string(), "1".to_string());
let mut bytes1 = Vec::new();
let mut bytes2 = Vec::new();
ciborium::into_writer(&c1, &mut bytes1).unwrap();
ciborium::into_writer(&c2, &mut bytes2).unwrap();
assert_eq!(bytes1, bytes2);
}
}