Skip to main content

ipfrs_storage/
vcs.rs

1//! Version Control System for Differentiable Storage
2//!
3//! Provides Git-like version control for tensor models and gradients:
4//! - Commit tracking with parent links (DAG structure)
5//! - Branch and tag management
6//! - Checkout to specific commits
7//! - Merge support for collaborative training
8//!
9//! This enables reproducible model states and collaborative training workflows.
10//!
11//! # Example
12//!
13//! ```rust,ignore
14//! use ipfrs_storage::{VersionControl, Author, SledBlockStore, BlockStoreConfig};
15//! use ipfrs_core::Block;
16//! use bytes::Bytes;
17//! use std::sync::Arc;
18//! use std::collections::HashMap;
19//! use std::path::PathBuf;
20//!
21//! # async fn example() -> ipfrs_core::Result<()> {
22//! // Create a block store
23//! let config = BlockStoreConfig {
24//!     path: PathBuf::from(".ipfrs/vcs"),
25//!     cache_size: 100 * 1024 * 1024,
26//! };
27//! let store = Arc::new(SledBlockStore::new(config)?);
28//!
29//! // Initialize version control
30//! let vcs = VersionControl::new(store.clone());
31//!
32//! // Store model v1 and create initial commit
33//! let model_v1 = Block::new(Bytes::from("model weights v1"))?;
34//! store.put(&model_v1).await?;
35//!
36//! let author = Author {
37//!     name: "AI Researcher".to_string(),
38//!     email: "researcher@example.com".to_string(),
39//! };
40//!
41//! let commit1 = vcs.commit(
42//!     *model_v1.cid(),
43//!     "Initial model".to_string(),
44//!     author.clone(),
45//!     HashMap::new(),
46//! ).await?;
47//!
48//! // Train model, create v2, and commit
49//! let model_v2 = Block::new(Bytes::from("model weights v2"))?;
50//! store.put(&model_v2).await?;
51//!
52//! let commit2 = vcs.commit(
53//!     *model_v2.cid(),
54//!     "After 100 epochs".to_string(),
55//!     author,
56//!     HashMap::new(),
57//! ).await?;
58//!
59//! // Checkout to previous version
60//! let model_cid = vcs.checkout(&commit1).await?;
61//! let previous_model = store.get(&model_cid).await?;
62//!
63//! // View commit history
64//! let history = vcs.log(&commit2, 10).await?;
65//! for commit in history {
66//!     println!("{}: {}", commit.timestamp, commit.message);
67//! }
68//! # Ok(())
69//! # }
70//! ```
71
72use 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
80// Custom serialization for Cid
81fn 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/// IPLD schema for a commit in the version control system
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
121pub struct Commit {
122    /// CID of the commit (computed from serialized commit, not included in serialization)
123    #[serde(skip)]
124    pub cid: Option<Cid>,
125
126    /// CIDs of parent commits (empty for initial commit)
127    #[serde(
128        serialize_with = "serialize_cid_vec",
129        deserialize_with = "deserialize_cid_vec"
130    )]
131    pub parents: Vec<Cid>,
132
133    /// CID of the root block this commit points to (e.g., model weights)
134    #[serde(serialize_with = "serialize_cid", deserialize_with = "deserialize_cid")]
135    pub root: Cid,
136
137    /// Commit message describing the changes
138    pub message: String,
139
140    /// Author of the commit
141    pub author: Author,
142
143    /// Unix timestamp when commit was created
144    pub timestamp: u64,
145
146    /// Optional metadata (e.g., training config, hyperparameters)
147    pub metadata: HashMap<String, String>,
148}
149
150/// Author information for a commit
151#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
152pub struct Author {
153    /// Author name
154    pub name: String,
155    /// Author email
156    pub email: String,
157}
158
159impl Commit {
160    /// Create a new commit
161    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            .expect("system time is after UNIX epoch")
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    /// Serialize commit to bytes and compute CID
185    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    /// Create a commit from a block
196    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    /// Convert commit to a block for storage
206    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    /// Check if this is an initial commit (no parents)
213    pub fn is_initial(&self) -> bool {
214        self.parents.is_empty()
215    }
216
217    /// Get a reference to the commit CID (panics if not finalized)
218    pub fn cid(&self) -> &Cid {
219        self.cid.as_ref().expect("Commit not finalized")
220    }
221}
222
223/// Reference to a commit (branch or tag)
224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
225pub struct Ref {
226    /// Name of the reference (e.g., "main", "dev", "v1.0")
227    pub name: String,
228    /// CID of the commit this ref points to
229    #[serde(serialize_with = "serialize_cid", deserialize_with = "deserialize_cid")]
230    pub commit: Cid,
231    /// Type of reference
232    pub ref_type: RefType,
233}
234
235/// Type of reference
236#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
237pub enum RefType {
238    /// Mutable branch pointer
239    Branch,
240    /// Immutable tag pointer
241    Tag,
242}
243
244impl Ref {
245    /// Create a new branch reference
246    pub fn branch(name: String, commit: Cid) -> Self {
247        Self {
248            name,
249            commit,
250            ref_type: RefType::Branch,
251        }
252    }
253
254    /// Create a new tag reference
255    pub fn tag(name: String, commit: Cid) -> Self {
256        Self {
257            name,
258            commit,
259            ref_type: RefType::Tag,
260        }
261    }
262
263    /// Convert ref to a block for storage
264    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    /// Create a ref from a block
271    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
278/// Version Control System for managing commits, branches, and tags
279pub struct VersionControl<S: BlockStore> {
280    /// Underlying block store
281    store: Arc<S>,
282    /// Current branch name
283    current_branch: parking_lot::RwLock<String>,
284    /// HEAD pointer (current commit CID)
285    head: parking_lot::RwLock<Option<Cid>>,
286    /// In-memory refs cache (ref name -> commit CID)
287    refs_cache: dashmap::DashMap<String, Cid>,
288}
289
290/// Merge strategy for combining branches
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292pub enum MergeStrategy {
293    /// Fast-forward merge (only when target is ancestor of source)
294    FastForward,
295    /// Three-way merge (creates merge commit)
296    ThreeWay,
297    /// Ours (keep current branch's changes on conflict)
298    Ours,
299    /// Theirs (accept incoming branch's changes on conflict)
300    Theirs,
301}
302
303/// Result of a merge operation
304#[derive(Debug, Clone, PartialEq)]
305pub enum MergeResult {
306    /// Fast-forward merge succeeded
307    FastForward { target: Cid },
308    /// Merge commit created
309    MergeCommit { commit: Cid },
310    /// Conflicts detected (contains conflicting paths)
311    Conflicts { conflicts: Vec<String> },
312}
313
314impl<S: BlockStore> VersionControl<S> {
315    /// Create a new version control system
316    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    /// List all refs
326    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    /// Create a new commit
334    ///
335    /// # Arguments
336    /// * `root` - CID of the root block (e.g., model weights)
337    /// * `message` - Commit message
338    /// * `author` - Author information
339    /// * `metadata` - Optional metadata
340    pub async fn commit(
341        &self,
342        root: Cid,
343        message: String,
344        author: Author,
345        metadata: HashMap<String, String>,
346    ) -> Result<Cid> {
347        // Get parent commits (current HEAD)
348        let parents = if let Some(head) = *self.head.read() {
349            vec![head]
350        } else {
351            vec![] // Initial commit
352        };
353
354        // Create and finalize commit
355        let mut commit = Commit::new(parents, root, message, author, metadata);
356        let commit_cid = commit.finalize()?;
357
358        // Store commit block
359        let commit_block = commit.to_block()?;
360        self.store.put(&commit_block).await?;
361
362        // Update HEAD
363        *self.head.write() = Some(commit_cid);
364
365        // Update current branch ref
366        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    /// Checkout to a specific commit
374    ///
375    /// Returns the root CID of the commit (e.g., model weights to load)
376    pub async fn checkout(&self, commit_cid: &Cid) -> Result<Cid> {
377        // Load commit
378        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        // Update HEAD to this commit
387        *self.head.write() = Some(*commit_cid);
388
389        // Return root CID for application to load
390        Ok(commit.root)
391    }
392
393    /// Checkout to a branch or tag
394    pub async fn checkout_ref(&self, ref_name: &str) -> Result<Cid> {
395        // Load ref
396        let ref_obj = self.get_ref(ref_name).await?;
397
398        // Update current branch if it's a branch
399        if ref_obj.ref_type == RefType::Branch {
400            *self.current_branch.write() = ref_name.to_string();
401        }
402
403        // Checkout to the commit
404        self.checkout(&ref_obj.commit).await
405    }
406
407    /// Create a new branch at the current HEAD
408    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    /// Create a new tag at the current HEAD
418    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    /// Update a reference (branch or tag)
428    async fn update_ref(&self, name: &str, commit: Cid, ref_type: RefType) -> Result<()> {
429        // Store in cache
430        self.refs_cache.insert(name.to_string(), commit);
431
432        // Also persist the ref as a block for durability
433        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    /// Get a reference by name
446    #[allow(clippy::unused_async)]
447    async fn get_ref(&self, name: &str) -> Result<Ref> {
448        // Check cache first
449        if let Some(commit_cid) = self.refs_cache.get(name) {
450            // Determine ref type based on naming convention
451            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    /// Get the current HEAD commit CID
468    pub fn head(&self) -> Option<Cid> {
469        *self.head.read()
470    }
471
472    /// Get the current branch name
473    pub fn current_branch(&self) -> String {
474        self.current_branch.read().clone()
475    }
476
477    /// Get commit history (walk the DAG backwards)
478    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            // Load commit
488            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            // Move to parent
497            current = commit.parents.first().copied();
498
499            commits.push(commit);
500        }
501
502        Ok(commits)
503    }
504
505    /// Get the underlying store
506    pub fn store(&self) -> &Arc<S> {
507        &self.store
508    }
509
510    /// Find common ancestor between two commits
511    pub async fn find_common_ancestor(&self, commit1: &Cid, commit2: &Cid) -> Result<Option<Cid>> {
512        // Get all ancestors of commit1
513        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; // Already visited
519            }
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        // Walk commit2's ancestors until we find one in ancestors1
532        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    /// Check if commit1 is an ancestor of commit2 (i.e., fast-forward is possible)
558    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    /// Merge a branch into the current HEAD
589    ///
590    /// # Arguments
591    /// * `branch_cid` - The commit CID to merge into current HEAD
592    /// * `message` - Merge commit message
593    /// * `author` - Author of the merge commit
594    /// * `strategy` - Merge strategy to use
595    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        // Check if already up to date
608        if &head_cid == branch_cid {
609            return Ok(MergeResult::FastForward { target: head_cid });
610        }
611
612        // Check if fast-forward is possible
613        if self.is_ancestor(&head_cid, branch_cid).await? {
614            // Fast-forward merge
615            *self.head.write() = Some(*branch_cid);
616
617            // Update current branch ref
618            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        // Fast-forward only strategy fails if not possible
627        if strategy == MergeStrategy::FastForward {
628            return Err(Error::Storage(
629                "Fast-forward not possible, branches have diverged".to_string(),
630            ));
631        }
632
633        // Load both commits
634        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        // Three-way merge: create a merge commit with both parents
649        match strategy {
650            MergeStrategy::ThreeWay | MergeStrategy::Ours | MergeStrategy::Theirs => {
651                // For now, we'll use the branch's root for the merge
652                // In a real implementation, we'd need to:
653                // 1. Find common ancestor
654                // 2. Compute diff from ancestor to head
655                // 3. Compute diff from ancestor to branch
656                // 4. Apply both diffs and resolve conflicts
657                // For simplicity, we'll use the strategy to pick a root:
658                let merge_root = match strategy {
659                    MergeStrategy::Ours => head_commit.root,
660                    MergeStrategy::Theirs => branch_commit.root,
661                    MergeStrategy::ThreeWay => {
662                        // Use branch's root (in real impl, would merge properly)
663                        branch_commit.root
664                    }
665                    _ => unreachable!(),
666                };
667
668                // Create merge commit with both parents
669                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                // Update HEAD
682                *self.head.write() = Some(merge_cid);
683
684                // Update current branch ref
685                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!(), // Already handled above
691        }
692    }
693
694    /// Merge a named branch into current HEAD
695    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
708/// Commit builder for ergonomic commit creation
709pub 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    /// Create a new commit builder
719    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    /// Set parent commits
730    #[must_use]
731    pub fn parents(mut self, parents: Vec<Cid>) -> Self {
732        self.parents = parents;
733        self
734    }
735
736    /// Set root CID
737    #[must_use]
738    pub fn root(mut self, root: Cid) -> Self {
739        self.root = Some(root);
740        self
741    }
742
743    /// Set commit message
744    #[must_use]
745    pub fn message(mut self, message: String) -> Self {
746        self.message = Some(message);
747        self
748    }
749
750    /// Set author
751    #[must_use]
752    pub fn author(mut self, author: Author) -> Self {
753        self.author = Some(author);
754        self
755    }
756
757    /// Add metadata entry
758    #[must_use]
759    pub fn metadata(mut self, key: String, value: String) -> Self {
760        self.metadata.insert(key, value);
761        self
762    }
763
764    /// Build the commit
765    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
797    #[tokio::test]
798    async fn test_commit_creation() {
799        let author = Author {
800            name: "Test User".to_string(),
801            email: "test@example.com".to_string(),
802        };
803
804        let root_block = Block::new(Bytes::from("model weights"))
805            .expect("test: Block::new with valid bytes should succeed");
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
817            .finalize()
818            .expect("test: commit finalize should succeed");
819        assert!(commit.cid.is_some());
820        assert_eq!(commit.cid(), &commit_cid);
821        assert!(commit.is_initial());
822    }
823
824    #[tokio::test]
825    async fn test_commit_serialization() {
826        let author = Author {
827            name: "Test User".to_string(),
828            email: "test@example.com".to_string(),
829        };
830
831        let root_block = Block::new(Bytes::from("model weights"))
832            .expect("test: Block::new with valid bytes should succeed");
833        let root_cid = *root_block.cid();
834
835        let mut commit = Commit::new(
836            vec![],
837            root_cid,
838            "Initial commit".to_string(),
839            author.clone(),
840            HashMap::new(),
841        );
842
843        commit
844            .finalize()
845            .expect("test: commit finalize should succeed");
846        let commit_block = commit
847            .to_block()
848            .expect("test: commit to_block should succeed");
849        let deserialized =
850            Commit::from_block(&commit_block).expect("test: Commit from_block should succeed");
851
852        assert_eq!(commit, deserialized);
853    }
854
855    #[tokio::test]
856    async fn test_version_control_initial_commit() {
857        let config = BlockStoreConfig {
858            path: std::env::temp_dir().join("ipfrs-vcs-test-initial"),
859            cache_size: 10 * 1024 * 1024,
860        };
861        let _ = std::fs::remove_dir_all(&config.path);
862
863        let store = Arc::new(
864            SledBlockStore::new(config).expect("test: SledBlockStore::new should succeed"),
865        );
866        let vcs = VersionControl::new(store.clone());
867
868        // Create root block (model)
869        let model_block =
870            Block::new(Bytes::from("model v1")).expect("test: Block::new should succeed");
871        let model_cid = *model_block.cid();
872        store
873            .put(&model_block)
874            .await
875            .expect("test: store.put should succeed");
876
877        // Create initial commit
878        let author = Author {
879            name: "Test User".to_string(),
880            email: "test@example.com".to_string(),
881        };
882
883        let commit_cid = vcs
884            .commit(
885                model_cid,
886                "Initial commit".to_string(),
887                author,
888                HashMap::new(),
889            )
890            .await
891            .expect("test: vcs.commit should succeed");
892
893        // Verify HEAD is updated
894        assert_eq!(vcs.head(), Some(commit_cid));
895
896        // Verify we can load the commit
897        let commit_block = store
898            .get(&commit_cid)
899            .await
900            .expect("test: store.get should succeed")
901            .expect("test: block should exist");
902        let commit =
903            Commit::from_block(&commit_block).expect("test: Commit::from_block should succeed");
904        assert_eq!(commit.root, model_cid);
905        assert_eq!(commit.message, "Initial commit");
906        assert!(commit.is_initial());
907    }
908
909    #[tokio::test]
910    async fn test_version_control_multiple_commits() {
911        let config = BlockStoreConfig {
912            path: std::env::temp_dir().join("ipfrs-vcs-test-multiple"),
913            cache_size: 10 * 1024 * 1024,
914        };
915        let _ = std::fs::remove_dir_all(&config.path);
916
917        let store = Arc::new(
918            SledBlockStore::new(config).expect("test: SledBlockStore::new should succeed"),
919        );
920        let vcs = VersionControl::new(store.clone());
921
922        let author = Author {
923            name: "Test User".to_string(),
924            email: "test@example.com".to_string(),
925        };
926
927        // First commit
928        let model1 = Block::new(Bytes::from("model v1"))
929            .expect("test: Block::new for model1 should succeed");
930        store
931            .put(&model1)
932            .await
933            .expect("test: store.put model1 should succeed");
934        let commit1 = vcs
935            .commit(
936                *model1.cid(),
937                "First commit".to_string(),
938                author.clone(),
939                HashMap::new(),
940            )
941            .await
942            .expect("test: first commit should succeed");
943
944        // Second commit
945        let model2 = Block::new(Bytes::from("model v2"))
946            .expect("test: Block::new for model2 should succeed");
947        store
948            .put(&model2)
949            .await
950            .expect("test: store.put model2 should succeed");
951        let commit2 = vcs
952            .commit(
953                *model2.cid(),
954                "Second commit".to_string(),
955                author,
956                HashMap::new(),
957            )
958            .await
959            .expect("test: second commit should succeed");
960
961        // Verify HEAD is at second commit
962        assert_eq!(vcs.head(), Some(commit2));
963
964        // Load second commit and verify it has first commit as parent
965        let commit2_block = store
966            .get(&commit2)
967            .await
968            .expect("test: store.get commit2 should succeed")
969            .expect("test: commit2 block should exist");
970        let commit2_obj =
971            Commit::from_block(&commit2_block).expect("test: Commit::from_block should succeed");
972        assert_eq!(commit2_obj.parents, vec![commit1]);
973    }
974
975    #[tokio::test]
976    async fn test_checkout() {
977        let config = BlockStoreConfig {
978            path: std::env::temp_dir().join("ipfrs-vcs-test-checkout"),
979            cache_size: 10 * 1024 * 1024,
980        };
981        let _ = std::fs::remove_dir_all(&config.path);
982
983        let store = Arc::new(
984            SledBlockStore::new(config).expect("test: SledBlockStore::new should succeed"),
985        );
986        let vcs = VersionControl::new(store.clone());
987
988        let author = Author {
989            name: "Test User".to_string(),
990            email: "test@example.com".to_string(),
991        };
992
993        // Create two commits
994        let model1 = Block::new(Bytes::from("model v1"))
995            .expect("test: Block::new for model1 should succeed");
996        store
997            .put(&model1)
998            .await
999            .expect("test: store.put model1 should succeed");
1000        let commit1 = vcs
1001            .commit(
1002                *model1.cid(),
1003                "First".to_string(),
1004                author.clone(),
1005                HashMap::new(),
1006            )
1007            .await
1008            .expect("test: first commit should succeed");
1009
1010        let model2 = Block::new(Bytes::from("model v2"))
1011            .expect("test: Block::new for model2 should succeed");
1012        store
1013            .put(&model2)
1014            .await
1015            .expect("test: store.put model2 should succeed");
1016        let _commit2 = vcs
1017            .commit(*model2.cid(), "Second".to_string(), author, HashMap::new())
1018            .await
1019            .expect("test: second commit should succeed");
1020
1021        // Checkout to first commit
1022        let root = vcs
1023            .checkout(&commit1)
1024            .await
1025            .expect("test: checkout should succeed");
1026        assert_eq!(root, *model1.cid());
1027        assert_eq!(vcs.head(), Some(commit1));
1028    }
1029
1030    #[tokio::test]
1031    async fn test_commit_log() {
1032        let config = BlockStoreConfig {
1033            path: std::env::temp_dir().join("ipfrs-vcs-test-log"),
1034            cache_size: 10 * 1024 * 1024,
1035        };
1036        let _ = std::fs::remove_dir_all(&config.path);
1037
1038        let store = Arc::new(
1039            SledBlockStore::new(config).expect("test: SledBlockStore::new should succeed"),
1040        );
1041        let vcs = VersionControl::new(store.clone());
1042
1043        let author = Author {
1044            name: "Test User".to_string(),
1045            email: "test@example.com".to_string(),
1046        };
1047
1048        // Create three commits
1049        let mut commits = Vec::new();
1050        for i in 1..=3 {
1051            let model = Block::new(Bytes::from(format!("model v{}", i)))
1052                .expect("test: Block::new should succeed");
1053            store
1054                .put(&model)
1055                .await
1056                .expect("test: store.put should succeed");
1057            let commit = vcs
1058                .commit(
1059                    *model.cid(),
1060                    format!("Commit {}", i),
1061                    author.clone(),
1062                    HashMap::new(),
1063                )
1064                .await
1065                .expect("test: vcs.commit should succeed");
1066            commits.push(commit);
1067        }
1068
1069        // Get log from HEAD
1070        let log = vcs
1071            .log(&commits[2], 10)
1072            .await
1073            .expect("test: vcs.log should succeed");
1074        assert_eq!(log.len(), 3);
1075        assert_eq!(log[0].message, "Commit 3");
1076        assert_eq!(log[1].message, "Commit 2");
1077        assert_eq!(log[2].message, "Commit 1");
1078    }
1079
1080    #[test]
1081    fn test_commit_builder() {
1082        let author = Author {
1083            name: "Builder".to_string(),
1084            email: "builder@example.com".to_string(),
1085        };
1086
1087        let root_block = Block::new(Bytes::from("root")).expect("test: Block::new should succeed");
1088
1089        let commit = CommitBuilder::new()
1090            .root(*root_block.cid())
1091            .message("Test commit".to_string())
1092            .author(author.clone())
1093            .metadata("key1".to_string(), "value1".to_string())
1094            .build()
1095            .expect("test: CommitBuilder::build should succeed");
1096
1097        assert_eq!(commit.message, "Test commit");
1098        assert_eq!(commit.author, author);
1099        assert_eq!(
1100            commit
1101                .metadata
1102                .get("key1")
1103                .expect("test: metadata key1 should exist"),
1104            "value1"
1105        );
1106    }
1107}