1use crate::traits::BlockStore;
73use bytes::Bytes;
74use ipfrs_core::{Block, Cid, Error, Result};
75use serde::{Deserialize, Deserializer, Serialize, Serializer};
76use std::collections::HashMap;
77use std::sync::Arc;
78use std::time::{SystemTime, UNIX_EPOCH};
79
80fn serialize_cid<S>(cid: &Cid, serializer: S) -> std::result::Result<S::Ok, S::Error>
82where
83 S: Serializer,
84{
85 serializer.serialize_bytes(&cid.to_bytes())
86}
87
88fn deserialize_cid<'de, D>(deserializer: D) -> std::result::Result<Cid, D::Error>
89where
90 D: Deserializer<'de>,
91{
92 let bytes: Vec<u8> = Deserialize::deserialize(deserializer)?;
93 Cid::try_from(bytes).map_err(serde::de::Error::custom)
94}
95
96fn serialize_cid_vec<S>(cids: &[Cid], serializer: S) -> std::result::Result<S::Ok, S::Error>
97where
98 S: Serializer,
99{
100 use serde::ser::SerializeSeq;
101 let mut seq = serializer.serialize_seq(Some(cids.len()))?;
102 for cid in cids {
103 seq.serialize_element(&cid.to_bytes())?;
104 }
105 seq.end()
106}
107
108fn deserialize_cid_vec<'de, D>(deserializer: D) -> std::result::Result<Vec<Cid>, D::Error>
109where
110 D: Deserializer<'de>,
111{
112 let bytes_vec: Vec<Vec<u8>> = Deserialize::deserialize(deserializer)?;
113 bytes_vec
114 .into_iter()
115 .map(|bytes| Cid::try_from(bytes).map_err(serde::de::Error::custom))
116 .collect()
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
121pub struct Commit {
122 #[serde(skip)]
124 pub cid: Option<Cid>,
125
126 #[serde(
128 serialize_with = "serialize_cid_vec",
129 deserialize_with = "deserialize_cid_vec"
130 )]
131 pub parents: Vec<Cid>,
132
133 #[serde(serialize_with = "serialize_cid", deserialize_with = "deserialize_cid")]
135 pub root: Cid,
136
137 pub message: String,
139
140 pub author: Author,
142
143 pub timestamp: u64,
145
146 pub metadata: HashMap<String, String>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
152pub struct Author {
153 pub name: String,
155 pub email: String,
157}
158
159impl Commit {
160 pub fn new(
162 parents: Vec<Cid>,
163 root: Cid,
164 message: String,
165 author: Author,
166 metadata: HashMap<String, String>,
167 ) -> Self {
168 let timestamp = SystemTime::now()
169 .duration_since(UNIX_EPOCH)
170 .unwrap()
171 .as_secs();
172
173 Self {
174 cid: None,
175 parents,
176 root,
177 message,
178 author,
179 timestamp,
180 metadata,
181 }
182 }
183
184 pub fn finalize(&mut self) -> Result<Cid> {
186 let bytes = oxicode::serde::encode_to_vec(self, oxicode::config::standard())
187 .map_err(|e| Error::Serialization(format!("Failed to serialize commit: {e}")))?;
188
189 let block = Block::new(Bytes::from(bytes))?;
190 let cid = *block.cid();
191 self.cid = Some(cid);
192 Ok(cid)
193 }
194
195 pub fn from_block(block: &Block) -> Result<Self> {
197 let mut commit: Commit =
198 oxicode::serde::decode_owned_from_slice(block.data(), oxicode::config::standard())
199 .map(|(v, _)| v)
200 .map_err(|e| Error::Serialization(format!("Failed to deserialize commit: {e}")))?;
201 commit.cid = Some(*block.cid());
202 Ok(commit)
203 }
204
205 pub fn to_block(&self) -> Result<Block> {
207 let bytes = oxicode::serde::encode_to_vec(self, oxicode::config::standard())
208 .map_err(|e| Error::Serialization(format!("Failed to serialize commit: {e}")))?;
209 Block::new(Bytes::from(bytes))
210 }
211
212 pub fn is_initial(&self) -> bool {
214 self.parents.is_empty()
215 }
216
217 pub fn cid(&self) -> &Cid {
219 self.cid.as_ref().expect("Commit not finalized")
220 }
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
225pub struct Ref {
226 pub name: String,
228 #[serde(serialize_with = "serialize_cid", deserialize_with = "deserialize_cid")]
230 pub commit: Cid,
231 pub ref_type: RefType,
233}
234
235#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
237pub enum RefType {
238 Branch,
240 Tag,
242}
243
244impl Ref {
245 pub fn branch(name: String, commit: Cid) -> Self {
247 Self {
248 name,
249 commit,
250 ref_type: RefType::Branch,
251 }
252 }
253
254 pub fn tag(name: String, commit: Cid) -> Self {
256 Self {
257 name,
258 commit,
259 ref_type: RefType::Tag,
260 }
261 }
262
263 pub fn to_block(&self) -> Result<Block> {
265 let bytes = oxicode::serde::encode_to_vec(self, oxicode::config::standard())
266 .map_err(|e| Error::Serialization(format!("Failed to serialize ref: {e}")))?;
267 Block::new(Bytes::from(bytes))
268 }
269
270 pub fn from_block(block: &Block) -> Result<Self> {
272 oxicode::serde::decode_owned_from_slice(block.data(), oxicode::config::standard())
273 .map(|(v, _)| v)
274 .map_err(|e| Error::Serialization(format!("Failed to deserialize ref: {e}")))
275 }
276}
277
278pub struct VersionControl<S: BlockStore> {
280 store: Arc<S>,
282 current_branch: parking_lot::RwLock<String>,
284 head: parking_lot::RwLock<Option<Cid>>,
286 refs_cache: dashmap::DashMap<String, Cid>,
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292pub enum MergeStrategy {
293 FastForward,
295 ThreeWay,
297 Ours,
299 Theirs,
301}
302
303#[derive(Debug, Clone, PartialEq)]
305pub enum MergeResult {
306 FastForward { target: Cid },
308 MergeCommit { commit: Cid },
310 Conflicts { conflicts: Vec<String> },
312}
313
314impl<S: BlockStore> VersionControl<S> {
315 pub fn new(store: Arc<S>) -> Self {
317 Self {
318 store,
319 current_branch: parking_lot::RwLock::new("main".to_string()),
320 head: parking_lot::RwLock::new(None),
321 refs_cache: dashmap::DashMap::new(),
322 }
323 }
324
325 pub fn list_refs(&self) -> Vec<(String, Cid)> {
327 self.refs_cache
328 .iter()
329 .map(|entry| (entry.key().clone(), *entry.value()))
330 .collect()
331 }
332
333 pub async fn commit(
341 &self,
342 root: Cid,
343 message: String,
344 author: Author,
345 metadata: HashMap<String, String>,
346 ) -> Result<Cid> {
347 let parents = if let Some(head) = *self.head.read() {
349 vec![head]
350 } else {
351 vec![] };
353
354 let mut commit = Commit::new(parents, root, message, author, metadata);
356 let commit_cid = commit.finalize()?;
357
358 let commit_block = commit.to_block()?;
360 self.store.put(&commit_block).await?;
361
362 *self.head.write() = Some(commit_cid);
364
365 let branch_name = self.current_branch.read().clone();
367 self.update_ref(&branch_name, commit_cid, RefType::Branch)
368 .await?;
369
370 Ok(commit_cid)
371 }
372
373 pub async fn checkout(&self, commit_cid: &Cid) -> Result<Cid> {
377 let commit_block = self
379 .store
380 .get(commit_cid)
381 .await?
382 .ok_or_else(|| Error::NotFound(format!("Commit not found: {commit_cid}")))?;
383
384 let commit = Commit::from_block(&commit_block)?;
385
386 *self.head.write() = Some(*commit_cid);
388
389 Ok(commit.root)
391 }
392
393 pub async fn checkout_ref(&self, ref_name: &str) -> Result<Cid> {
395 let ref_obj = self.get_ref(ref_name).await?;
397
398 if ref_obj.ref_type == RefType::Branch {
400 *self.current_branch.write() = ref_name.to_string();
401 }
402
403 self.checkout(&ref_obj.commit).await
405 }
406
407 pub async fn create_branch(&self, branch_name: &str) -> Result<()> {
409 let head = self
410 .head
411 .read()
412 .ok_or_else(|| Error::Storage("No HEAD commit".to_string()))?;
413
414 self.update_ref(branch_name, head, RefType::Branch).await
415 }
416
417 pub async fn create_tag(&self, tag_name: &str) -> Result<()> {
419 let head = self
420 .head
421 .read()
422 .ok_or_else(|| Error::Storage("No HEAD commit".to_string()))?;
423
424 self.update_ref(tag_name, head, RefType::Tag).await
425 }
426
427 async fn update_ref(&self, name: &str, commit: Cid, ref_type: RefType) -> Result<()> {
429 self.refs_cache.insert(name.to_string(), commit);
431
432 let ref_obj = Ref {
434 name: name.to_string(),
435 commit,
436 ref_type,
437 };
438
439 let ref_block = ref_obj.to_block()?;
440 self.store.put(&ref_block).await?;
441
442 Ok(())
443 }
444
445 #[allow(clippy::unused_async)]
447 async fn get_ref(&self, name: &str) -> Result<Ref> {
448 if let Some(commit_cid) = self.refs_cache.get(name) {
450 let ref_type = if name.starts_with("refs/tags/") || name.contains("/tags/") {
452 RefType::Tag
453 } else {
454 RefType::Branch
455 };
456
457 return Ok(Ref {
458 name: name.to_string(),
459 commit: *commit_cid,
460 ref_type,
461 });
462 }
463
464 Err(Error::NotFound(format!("Ref not found: {name}")))
465 }
466
467 pub fn head(&self) -> Option<Cid> {
469 *self.head.read()
470 }
471
472 pub fn current_branch(&self) -> String {
474 self.current_branch.read().clone()
475 }
476
477 pub async fn log(&self, commit_cid: &Cid, limit: usize) -> Result<Vec<Commit>> {
479 let mut commits = Vec::new();
480 let mut current = Some(*commit_cid);
481
482 while let Some(cid) = current {
483 if commits.len() >= limit {
484 break;
485 }
486
487 let commit_block = self
489 .store
490 .get(&cid)
491 .await?
492 .ok_or_else(|| Error::NotFound(format!("Commit not found: {cid}")))?;
493
494 let commit = Commit::from_block(&commit_block)?;
495
496 current = commit.parents.first().copied();
498
499 commits.push(commit);
500 }
501
502 Ok(commits)
503 }
504
505 pub fn store(&self) -> &Arc<S> {
507 &self.store
508 }
509
510 pub async fn find_common_ancestor(&self, commit1: &Cid, commit2: &Cid) -> Result<Option<Cid>> {
512 let mut ancestors1 = std::collections::HashSet::new();
514 let mut queue = vec![*commit1];
515
516 while let Some(cid) = queue.pop() {
517 if !ancestors1.insert(cid) {
518 continue; }
520
521 let block = self
522 .store
523 .get(&cid)
524 .await?
525 .ok_or_else(|| Error::NotFound(format!("Commit not found: {cid}")))?;
526
527 let commit = Commit::from_block(&block)?;
528 queue.extend(commit.parents.iter().copied());
529 }
530
531 let mut queue = vec![*commit2];
533 let mut visited = std::collections::HashSet::new();
534
535 while let Some(cid) = queue.pop() {
536 if !visited.insert(cid) {
537 continue;
538 }
539
540 if ancestors1.contains(&cid) {
541 return Ok(Some(cid));
542 }
543
544 let block = self
545 .store
546 .get(&cid)
547 .await?
548 .ok_or_else(|| Error::NotFound(format!("Commit not found: {cid}")))?;
549
550 let commit = Commit::from_block(&block)?;
551 queue.extend(commit.parents.iter().copied());
552 }
553
554 Ok(None)
555 }
556
557 pub async fn is_ancestor(&self, ancestor: &Cid, descendant: &Cid) -> Result<bool> {
559 if ancestor == descendant {
560 return Ok(true);
561 }
562
563 let mut queue = vec![*descendant];
564 let mut visited = std::collections::HashSet::new();
565
566 while let Some(cid) = queue.pop() {
567 if !visited.insert(cid) {
568 continue;
569 }
570
571 if &cid == ancestor {
572 return Ok(true);
573 }
574
575 let block = self
576 .store
577 .get(&cid)
578 .await?
579 .ok_or_else(|| Error::NotFound(format!("Commit not found: {cid}")))?;
580
581 let commit = Commit::from_block(&block)?;
582 queue.extend(commit.parents.iter().copied());
583 }
584
585 Ok(false)
586 }
587
588 pub async fn merge(
596 &self,
597 branch_cid: &Cid,
598 message: String,
599 author: Author,
600 strategy: MergeStrategy,
601 ) -> Result<MergeResult> {
602 let head_cid = self
603 .head
604 .read()
605 .ok_or_else(|| Error::Storage("No HEAD commit".to_string()))?;
606
607 if &head_cid == branch_cid {
609 return Ok(MergeResult::FastForward { target: head_cid });
610 }
611
612 if self.is_ancestor(&head_cid, branch_cid).await? {
614 *self.head.write() = Some(*branch_cid);
616
617 let branch_name = self.current_branch.read().clone();
619 self.refs_cache.insert(branch_name.clone(), *branch_cid);
620
621 return Ok(MergeResult::FastForward {
622 target: *branch_cid,
623 });
624 }
625
626 if strategy == MergeStrategy::FastForward {
628 return Err(Error::Storage(
629 "Fast-forward not possible, branches have diverged".to_string(),
630 ));
631 }
632
633 let head_block = self
635 .store
636 .get(&head_cid)
637 .await?
638 .ok_or_else(|| Error::NotFound(format!("HEAD commit not found: {head_cid}")))?;
639 let head_commit = Commit::from_block(&head_block)?;
640
641 let branch_block = self
642 .store
643 .get(branch_cid)
644 .await?
645 .ok_or_else(|| Error::NotFound(format!("Branch commit not found: {branch_cid}")))?;
646 let branch_commit = Commit::from_block(&branch_block)?;
647
648 match strategy {
650 MergeStrategy::ThreeWay | MergeStrategy::Ours | MergeStrategy::Theirs => {
651 let merge_root = match strategy {
659 MergeStrategy::Ours => head_commit.root,
660 MergeStrategy::Theirs => branch_commit.root,
661 MergeStrategy::ThreeWay => {
662 branch_commit.root
664 }
665 _ => unreachable!(),
666 };
667
668 let mut merge_commit = Commit::new(
670 vec![head_cid, *branch_cid],
671 merge_root,
672 message,
673 author,
674 HashMap::new(),
675 );
676
677 let merge_cid = merge_commit.finalize()?;
678 let merge_block = merge_commit.to_block()?;
679 self.store.put(&merge_block).await?;
680
681 *self.head.write() = Some(merge_cid);
683
684 let branch_name = self.current_branch.read().clone();
686 self.refs_cache.insert(branch_name.clone(), merge_cid);
687
688 Ok(MergeResult::MergeCommit { commit: merge_cid })
689 }
690 MergeStrategy::FastForward => unreachable!(), }
692 }
693
694 pub async fn merge_branch(
696 &self,
697 branch_name: &str,
698 message: String,
699 author: Author,
700 strategy: MergeStrategy,
701 ) -> Result<MergeResult> {
702 let branch_ref = self.get_ref(branch_name).await?;
703 self.merge(&branch_ref.commit, message, author, strategy)
704 .await
705 }
706}
707
708pub struct CommitBuilder {
710 parents: Vec<Cid>,
711 root: Option<Cid>,
712 message: Option<String>,
713 author: Option<Author>,
714 metadata: HashMap<String, String>,
715}
716
717impl CommitBuilder {
718 pub fn new() -> Self {
720 Self {
721 parents: Vec::new(),
722 root: None,
723 message: None,
724 author: None,
725 metadata: HashMap::new(),
726 }
727 }
728
729 #[must_use]
731 pub fn parents(mut self, parents: Vec<Cid>) -> Self {
732 self.parents = parents;
733 self
734 }
735
736 #[must_use]
738 pub fn root(mut self, root: Cid) -> Self {
739 self.root = Some(root);
740 self
741 }
742
743 #[must_use]
745 pub fn message(mut self, message: String) -> Self {
746 self.message = Some(message);
747 self
748 }
749
750 #[must_use]
752 pub fn author(mut self, author: Author) -> Self {
753 self.author = Some(author);
754 self
755 }
756
757 #[must_use]
759 pub fn metadata(mut self, key: String, value: String) -> Self {
760 self.metadata.insert(key, value);
761 self
762 }
763
764 pub fn build(self) -> Result<Commit> {
766 let root = self
767 .root
768 .ok_or_else(|| Error::Storage("Root CID is required".to_string()))?;
769 let message = self
770 .message
771 .ok_or_else(|| Error::Storage("Commit message is required".to_string()))?;
772 let author = self
773 .author
774 .ok_or_else(|| Error::Storage("Author is required".to_string()))?;
775
776 Ok(Commit::new(
777 self.parents,
778 root,
779 message,
780 author,
781 self.metadata,
782 ))
783 }
784}
785
786impl Default for CommitBuilder {
787 fn default() -> Self {
788 Self::new()
789 }
790}
791
792#[cfg(test)]
793mod tests {
794 use super::*;
795 use crate::blockstore::{BlockStoreConfig, SledBlockStore};
796 use std::path::PathBuf;
797
798 #[tokio::test]
799 async fn test_commit_creation() {
800 let author = Author {
801 name: "Test User".to_string(),
802 email: "test@example.com".to_string(),
803 };
804
805 let root_block = Block::new(Bytes::from("model weights")).unwrap();
806 let root_cid = *root_block.cid();
807
808 let mut commit = Commit::new(
809 vec![],
810 root_cid,
811 "Initial commit".to_string(),
812 author,
813 HashMap::new(),
814 );
815
816 let commit_cid = commit.finalize().unwrap();
817 assert!(commit.cid.is_some());
818 assert_eq!(commit.cid(), &commit_cid);
819 assert!(commit.is_initial());
820 }
821
822 #[tokio::test]
823 async fn test_commit_serialization() {
824 let author = Author {
825 name: "Test User".to_string(),
826 email: "test@example.com".to_string(),
827 };
828
829 let root_block = Block::new(Bytes::from("model weights")).unwrap();
830 let root_cid = *root_block.cid();
831
832 let mut commit = Commit::new(
833 vec![],
834 root_cid,
835 "Initial commit".to_string(),
836 author.clone(),
837 HashMap::new(),
838 );
839
840 commit.finalize().unwrap();
841 let commit_block = commit.to_block().unwrap();
842 let deserialized = Commit::from_block(&commit_block).unwrap();
843
844 assert_eq!(commit, deserialized);
845 }
846
847 #[tokio::test]
848 async fn test_version_control_initial_commit() {
849 let config = BlockStoreConfig {
850 path: PathBuf::from("/tmp/ipfrs-vcs-test-initial"),
851 cache_size: 10 * 1024 * 1024,
852 };
853 let _ = std::fs::remove_dir_all(&config.path);
854
855 let store = Arc::new(SledBlockStore::new(config).unwrap());
856 let vcs = VersionControl::new(store.clone());
857
858 let model_block = Block::new(Bytes::from("model v1")).unwrap();
860 let model_cid = *model_block.cid();
861 store.put(&model_block).await.unwrap();
862
863 let author = Author {
865 name: "Test User".to_string(),
866 email: "test@example.com".to_string(),
867 };
868
869 let commit_cid = vcs
870 .commit(
871 model_cid,
872 "Initial commit".to_string(),
873 author,
874 HashMap::new(),
875 )
876 .await
877 .unwrap();
878
879 assert_eq!(vcs.head(), Some(commit_cid));
881
882 let commit_block = store.get(&commit_cid).await.unwrap().unwrap();
884 let commit = Commit::from_block(&commit_block).unwrap();
885 assert_eq!(commit.root, model_cid);
886 assert_eq!(commit.message, "Initial commit");
887 assert!(commit.is_initial());
888 }
889
890 #[tokio::test]
891 async fn test_version_control_multiple_commits() {
892 let config = BlockStoreConfig {
893 path: PathBuf::from("/tmp/ipfrs-vcs-test-multiple"),
894 cache_size: 10 * 1024 * 1024,
895 };
896 let _ = std::fs::remove_dir_all(&config.path);
897
898 let store = Arc::new(SledBlockStore::new(config).unwrap());
899 let vcs = VersionControl::new(store.clone());
900
901 let author = Author {
902 name: "Test User".to_string(),
903 email: "test@example.com".to_string(),
904 };
905
906 let model1 = Block::new(Bytes::from("model v1")).unwrap();
908 store.put(&model1).await.unwrap();
909 let commit1 = vcs
910 .commit(
911 *model1.cid(),
912 "First commit".to_string(),
913 author.clone(),
914 HashMap::new(),
915 )
916 .await
917 .unwrap();
918
919 let model2 = Block::new(Bytes::from("model v2")).unwrap();
921 store.put(&model2).await.unwrap();
922 let commit2 = vcs
923 .commit(
924 *model2.cid(),
925 "Second commit".to_string(),
926 author,
927 HashMap::new(),
928 )
929 .await
930 .unwrap();
931
932 assert_eq!(vcs.head(), Some(commit2));
934
935 let commit2_block = store.get(&commit2).await.unwrap().unwrap();
937 let commit2_obj = Commit::from_block(&commit2_block).unwrap();
938 assert_eq!(commit2_obj.parents, vec![commit1]);
939 }
940
941 #[tokio::test]
942 async fn test_checkout() {
943 let config = BlockStoreConfig {
944 path: PathBuf::from("/tmp/ipfrs-vcs-test-checkout"),
945 cache_size: 10 * 1024 * 1024,
946 };
947 let _ = std::fs::remove_dir_all(&config.path);
948
949 let store = Arc::new(SledBlockStore::new(config).unwrap());
950 let vcs = VersionControl::new(store.clone());
951
952 let author = Author {
953 name: "Test User".to_string(),
954 email: "test@example.com".to_string(),
955 };
956
957 let model1 = Block::new(Bytes::from("model v1")).unwrap();
959 store.put(&model1).await.unwrap();
960 let commit1 = vcs
961 .commit(
962 *model1.cid(),
963 "First".to_string(),
964 author.clone(),
965 HashMap::new(),
966 )
967 .await
968 .unwrap();
969
970 let model2 = Block::new(Bytes::from("model v2")).unwrap();
971 store.put(&model2).await.unwrap();
972 let _commit2 = vcs
973 .commit(*model2.cid(), "Second".to_string(), author, HashMap::new())
974 .await
975 .unwrap();
976
977 let root = vcs.checkout(&commit1).await.unwrap();
979 assert_eq!(root, *model1.cid());
980 assert_eq!(vcs.head(), Some(commit1));
981 }
982
983 #[tokio::test]
984 async fn test_commit_log() {
985 let config = BlockStoreConfig {
986 path: PathBuf::from("/tmp/ipfrs-vcs-test-log"),
987 cache_size: 10 * 1024 * 1024,
988 };
989 let _ = std::fs::remove_dir_all(&config.path);
990
991 let store = Arc::new(SledBlockStore::new(config).unwrap());
992 let vcs = VersionControl::new(store.clone());
993
994 let author = Author {
995 name: "Test User".to_string(),
996 email: "test@example.com".to_string(),
997 };
998
999 let mut commits = Vec::new();
1001 for i in 1..=3 {
1002 let model = Block::new(Bytes::from(format!("model v{}", i))).unwrap();
1003 store.put(&model).await.unwrap();
1004 let commit = vcs
1005 .commit(
1006 *model.cid(),
1007 format!("Commit {}", i),
1008 author.clone(),
1009 HashMap::new(),
1010 )
1011 .await
1012 .unwrap();
1013 commits.push(commit);
1014 }
1015
1016 let log = vcs.log(&commits[2], 10).await.unwrap();
1018 assert_eq!(log.len(), 3);
1019 assert_eq!(log[0].message, "Commit 3");
1020 assert_eq!(log[1].message, "Commit 2");
1021 assert_eq!(log[2].message, "Commit 1");
1022 }
1023
1024 #[test]
1025 fn test_commit_builder() {
1026 let author = Author {
1027 name: "Builder".to_string(),
1028 email: "builder@example.com".to_string(),
1029 };
1030
1031 let root_block = Block::new(Bytes::from("root")).unwrap();
1032
1033 let commit = CommitBuilder::new()
1034 .root(*root_block.cid())
1035 .message("Test commit".to_string())
1036 .author(author.clone())
1037 .metadata("key1".to_string(), "value1".to_string())
1038 .build()
1039 .unwrap();
1040
1041 assert_eq!(commit.message, "Test commit");
1042 assert_eq!(commit.author, author);
1043 assert_eq!(commit.metadata.get("key1").unwrap(), "value1");
1044 }
1045}