pocket_cli/vcs/
shove.rs

1//! Shove (commit) functionality for Pocket VCS
2//!
3//! Handles the creation and management of shoves (commits).
4
5use std::path::{Path, PathBuf};
6use std::fs;
7use chrono::{DateTime, Utc};
8use serde::{Serialize, Deserialize};
9use anyhow::Result;
10use uuid::Uuid;
11use std::collections::HashMap;
12
13use crate::vcs::{ObjectId, Pile, FileChange, ChangeType, Repository, Tree};
14use crate::vcs::objects::EntryType;
15
16/// A unique identifier for a shove
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub struct ShoveId(String);
19
20impl ShoveId {
21    /// Create a new random shove ID
22    pub fn new() -> Self {
23        Self(Uuid::new_v4().to_string())
24    }
25    
26    /// Parse a shove ID from a string
27    pub fn from_str(s: &str) -> Result<Self> {
28        Ok(Self(s.to_string()))
29    }
30    
31    /// Get the string representation
32    pub fn as_str(&self) -> &str {
33        &self.0
34    }
35}
36
37/// Author information for a shove
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Author {
40    pub name: String,
41    pub email: String,
42    pub timestamp: DateTime<Utc>,
43}
44
45/// A shove (commit) in the repository
46#[derive(Debug, Serialize, Deserialize)]
47pub struct Shove {
48    /// Unique identifier for this shove
49    pub id: ShoveId,
50    
51    /// Parent shove IDs (multiple for merges)
52    pub parent_ids: Vec<ShoveId>,
53    
54    /// Author information
55    pub author: Author,
56    
57    /// When the shove was created
58    pub timestamp: DateTime<Utc>,
59    
60    /// Commit message
61    pub message: String,
62    
63    /// ID of the root tree object
64    pub root_tree_id: ObjectId,
65}
66
67impl Shove {
68    /// Create a new shove from a pile
69    pub fn new(
70        pile: &Pile,
71        parent_ids: Vec<ShoveId>,
72        author: Author,
73        message: &str,
74        root_tree_id: ObjectId,
75    ) -> Self {
76        Self {
77            id: ShoveId::new(),
78            parent_ids,
79            author,
80            timestamp: Utc::now(),
81            message: message.to_string(),
82            root_tree_id,
83        }
84    }
85    
86    /// Load a shove from a file
87    pub fn load(path: &Path) -> Result<Self> {
88        let content = fs::read_to_string(path)?;
89        let shove: Self = toml::from_str(&content)?;
90        Ok(shove)
91    }
92    
93    /// Save the shove to a file
94    pub fn save(&self, path: &Path) -> Result<()> {
95        let content = toml::to_string_pretty(self)?;
96        fs::write(path, content)?;
97        Ok(())
98    }
99    
100    /// Get the changes introduced by this shove
101    pub fn get_changes(&self, repo: &Repository) -> Result<Vec<FileChange>> {
102        let mut changes = Vec::new();
103        
104        // If this is the first shove, all files are considered added
105        if self.parent_ids.is_empty() {
106            // Get the tree for this shove
107            let tree_path = repo.path.join(".pocket").join("objects").join(self.root_tree_id.as_str());
108            let tree_content = fs::read_to_string(&tree_path)?;
109            let tree: Tree = toml::from_str(&tree_content)?;
110            
111            // All files in the tree are considered added
112            for entry in tree.entries {
113                if entry.entry_type == EntryType::File {
114                    changes.push(FileChange {
115                        path: PathBuf::from(&entry.name),
116                        change_type: ChangeType::Added,
117                        old_id: None,
118                        new_id: Some(entry.id),
119                    });
120                }
121            }
122            
123            return Ok(changes);
124        }
125        
126        // Get the parent shove
127        let parent_id = &self.parent_ids[0]; // For simplicity, just use the first parent
128        let parent_path = repo.path.join(".pocket").join("shoves").join(format!("{}.toml", parent_id.as_str()));
129        let parent_content = fs::read_to_string(&parent_path)?;
130        let parent: Shove = toml::from_str(&parent_content)?;
131        
132        // Get the trees for both shoves
133        let parent_tree_path = repo.path.join(".pocket").join("objects").join(parent.root_tree_id.as_str());
134        let current_tree_path = repo.path.join(".pocket").join("objects").join(self.root_tree_id.as_str());
135        
136        let parent_tree_content = fs::read_to_string(&parent_tree_path)?;
137        let current_tree_content = fs::read_to_string(&current_tree_path)?;
138        
139        let parent_tree: Tree = toml::from_str(&parent_tree_content)?;
140        let current_tree: Tree = toml::from_str(&current_tree_content)?;
141        
142        // Create maps for easier lookup
143        let mut parent_entries = HashMap::new();
144        for entry in parent_tree.entries {
145            parent_entries.insert(entry.name.clone(), entry);
146        }
147        
148        // Find added and modified files
149        for entry in &current_tree.entries {
150            if entry.entry_type == EntryType::File {
151                if let Some(parent_entry) = parent_entries.get(&entry.name) {
152                    // File exists in both trees, check if modified
153                    if parent_entry.id != entry.id {
154                        changes.push(FileChange {
155                            path: PathBuf::from(&entry.name),
156                            change_type: ChangeType::Modified,
157                            old_id: Some(parent_entry.id.clone()),
158                            new_id: Some(entry.id.clone()),
159                        });
160                    }
161                } else {
162                    // File only exists in current tree, it's added
163                    changes.push(FileChange {
164                        path: PathBuf::from(&entry.name),
165                        change_type: ChangeType::Added,
166                        old_id: None,
167                        new_id: Some(entry.id.clone()),
168                    });
169                }
170            }
171        }
172        
173        // Find deleted files
174        for (name, entry) in parent_entries {
175            if entry.entry_type == EntryType::File {
176                let exists = current_tree.entries.iter().any(|e| e.name == name);
177                if !exists {
178                    changes.push(FileChange {
179                        path: PathBuf::from(&name),
180                        change_type: ChangeType::Deleted,
181                        old_id: Some(entry.id),
182                        new_id: None,
183                    });
184                }
185            }
186        }
187        
188        Ok(changes)
189    }
190    
191    // Additional methods would be implemented here:
192    // - get_diff: Get the diff between this shove and another
193    // - get_files: Get all files in this shove
194    // - etc.
195}