1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::hash::Hash;
6
7#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
9pub struct Commit {
10 pub tree: Hash,
12 pub parents: Vec<Hash>,
14 pub author: String,
16 pub timestamp: i64,
18 pub message: String,
20 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
22 pub metadata: BTreeMap<String, String>,
23}
24
25impl Commit {
26 pub fn new(
28 tree: Hash,
29 parents: Vec<Hash>,
30 author: impl Into<String>,
31 message: impl Into<String>,
32 ) -> Self {
33 Self {
34 tree,
35 parents,
36 author: author.into(),
37 timestamp: std::time::SystemTime::now()
38 .duration_since(std::time::UNIX_EPOCH)
39 .map(|d| d.as_secs() as i64)
40 .unwrap_or(0),
41 message: message.into(),
42 metadata: BTreeMap::new(),
43 }
44 }
45
46 pub fn with_timestamp(
48 tree: Hash,
49 parents: Vec<Hash>,
50 author: impl Into<String>,
51 timestamp: i64,
52 message: impl Into<String>,
53 ) -> Self {
54 Self {
55 tree,
56 parents,
57 author: author.into(),
58 timestamp,
59 message: message.into(),
60 metadata: BTreeMap::new(),
61 }
62 }
63
64 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
66 self.metadata.insert(key.into(), value.into());
67 self
68 }
69
70 pub fn is_root(&self) -> bool {
72 self.parents.is_empty()
73 }
74
75 pub fn is_merge(&self) -> bool {
77 self.parents.len() > 1
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn test_commit_new() {
87 let c = Commit::new(Hash::ZERO, vec![], "author", "message");
88 assert_eq!(c.tree, Hash::ZERO);
89 assert!(c.parents.is_empty());
90 assert_eq!(c.author, "author");
91 assert_eq!(c.message, "message");
92 assert!(c.is_root());
93 assert!(!c.is_merge());
94 }
95
96 #[test]
97 fn test_commit_with_parents() {
98 let parent =
99 Hash::from_hex("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789")
100 .unwrap();
101 let c = Commit::new(Hash::ZERO, vec![parent], "author", "message");
102 assert!(!c.is_root());
103 assert!(!c.is_merge());
104 }
105
106 #[test]
107 fn test_commit_merge() {
108 let p1 = Hash::from_hex("1111111111111111111111111111111111111111111111111111111111111111")
109 .unwrap();
110 let p2 = Hash::from_hex("2222222222222222222222222222222222222222222222222222222222222222")
111 .unwrap();
112 let c = Commit::new(Hash::ZERO, vec![p1, p2], "author", "merge");
113 assert!(c.is_merge());
114 }
115
116 #[test]
117 fn test_commit_with_metadata() {
118 let c = Commit::new(Hash::ZERO, vec![], "author", "message")
119 .with_metadata("key1", "value1")
120 .with_metadata("key2", "value2");
121 assert_eq!(c.metadata.get("key1"), Some(&"value1".to_string()));
122 assert_eq!(c.metadata.get("key2"), Some(&"value2".to_string()));
123 }
124
125 #[test]
126 fn test_commit_cbor_roundtrip() {
127 let c = Commit::with_timestamp(Hash::ZERO, vec![], "author", 1234567890, "message")
128 .with_metadata("foo", "bar");
129
130 let mut bytes = Vec::new();
131 ciborium::into_writer(&c, &mut bytes).unwrap();
132
133 let parsed: Commit = ciborium::from_reader(&bytes[..]).unwrap();
134 assert_eq!(c, parsed);
135 }
136
137 #[test]
138 fn test_commit_cbor_determinism() {
139 let mut c1 = Commit::with_timestamp(Hash::ZERO, vec![], "a", 0, "m");
141 c1.metadata.insert("z".to_string(), "1".to_string());
142 c1.metadata.insert("a".to_string(), "2".to_string());
143
144 let mut c2 = Commit::with_timestamp(Hash::ZERO, vec![], "a", 0, "m");
145 c2.metadata.insert("a".to_string(), "2".to_string());
146 c2.metadata.insert("z".to_string(), "1".to_string());
147
148 let mut bytes1 = Vec::new();
149 let mut bytes2 = Vec::new();
150 ciborium::into_writer(&c1, &mut bytes1).unwrap();
151 ciborium::into_writer(&c2, &mut bytes2).unwrap();
152
153 assert_eq!(bytes1, bytes2);
154 }
155}