rustic_git/commands/
commit.rs

1use crate::{Repository, Result, Hash};
2use crate::utils::git;
3
4impl Repository {
5    /// Create a commit with the given message.
6    ///
7    /// # Arguments
8    ///
9    /// * `message` - The commit message
10    ///
11    /// # Returns
12    ///
13    /// A `Result` containing the `Hash` of the new commit or a `GitError`.
14    pub fn commit(&self, message: &str) -> Result<Hash> {
15        Self::ensure_git()?;
16
17        if message.trim().is_empty() {
18            return Err(crate::error::GitError::CommandFailed(
19                "Commit message cannot be empty".to_string()
20            ));
21        }
22
23        // Check if there are staged changes
24        let status = self.status()?;
25        let has_staged = status.files.iter().any(|(file_status, _)| {
26            matches!(file_status, 
27                crate::FileStatus::Added | 
28                crate::FileStatus::Modified | 
29                crate::FileStatus::Deleted
30            )
31        });
32
33        if !has_staged {
34            return Err(crate::error::GitError::CommandFailed(
35                "No changes staged for commit".to_string()
36            ));
37        }
38
39        let _stdout = git(&["commit", "-m", message], Some(self.repo_path()))?;
40        
41        // Get the commit hash of the just-created commit
42        let hash_output = git(&["rev-parse", "HEAD"], Some(self.repo_path()))?;
43        let commit_hash = hash_output.trim().to_string();
44        
45        Ok(Hash(commit_hash))
46    }
47
48    /// Create a commit with the given message and author.
49    ///
50    /// # Arguments
51    ///
52    /// * `message` - The commit message
53    /// * `author` - The author in format "Name <email@example.com>"
54    ///
55    /// # Returns
56    ///
57    /// A `Result` containing the `Hash` of the new commit or a `GitError`.
58    pub fn commit_with_author(&self, message: &str, author: &str) -> Result<Hash> {
59        Self::ensure_git()?;
60
61        if message.trim().is_empty() {
62            return Err(crate::error::GitError::CommandFailed(
63                "Commit message cannot be empty".to_string()
64            ));
65        }
66
67        if author.trim().is_empty() {
68            return Err(crate::error::GitError::CommandFailed(
69                "Author cannot be empty".to_string()
70            ));
71        }
72
73        // Check if there are staged changes
74        let status = self.status()?;
75        let has_staged = status.files.iter().any(|(file_status, _)| {
76            matches!(file_status, 
77                crate::FileStatus::Added | 
78                crate::FileStatus::Modified | 
79                crate::FileStatus::Deleted
80            )
81        });
82
83        if !has_staged {
84            return Err(crate::error::GitError::CommandFailed(
85                "No changes staged for commit".to_string()
86            ));
87        }
88
89        let _stdout = git(&["commit", "-m", message, "--author", author], Some(self.repo_path()))?;
90        
91        // Get the commit hash of the just-created commit
92        let hash_output = git(&["rev-parse", "HEAD"], Some(self.repo_path()))?;
93        let commit_hash = hash_output.trim().to_string();
94        
95        Ok(Hash(commit_hash))
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use std::fs;
103    use std::path::Path;
104
105    fn create_test_repo(path: &str) -> Repository {
106        // Clean up if exists
107        if Path::new(path).exists() {
108            fs::remove_dir_all(path).unwrap();
109        }
110        
111        Repository::init(path, false).unwrap()
112    }
113
114    fn create_and_stage_file(repo: &Repository, repo_path: &str, filename: &str, content: &str) {
115        let file_path = format!("{}/{}", repo_path, filename);
116        fs::write(file_path, content).unwrap();
117        repo.add(&[filename]).unwrap();
118    }
119
120    #[test]
121    fn test_commit_basic() {
122        let test_path = "/tmp/test_commit_repo";
123        let repo = create_test_repo(test_path);
124
125        // Create and stage a file
126        create_and_stage_file(&repo, test_path, "test.txt", "test content");
127
128        // Commit the changes
129        let result = repo.commit("Initial commit");
130        assert!(result.is_ok());
131        
132        let hash = result.unwrap();
133        assert!(!hash.as_str().is_empty());
134        assert_eq!(hash.short().len(), 7);
135
136        // Verify repository is now clean
137        let status = repo.status().unwrap();
138        assert!(status.is_clean());
139
140        // Clean up
141        fs::remove_dir_all(test_path).unwrap();
142    }
143
144    #[test]
145    fn test_commit_with_author() {
146        let test_path = "/tmp/test_commit_author_repo";
147        let repo = create_test_repo(test_path);
148
149        // Create and stage a file
150        create_and_stage_file(&repo, test_path, "test.txt", "test content");
151
152        // Commit with author
153        let result = repo.commit_with_author("Test commit", "Test User <test@example.com>");
154        assert!(result.is_ok());
155        
156        let hash = result.unwrap();
157        assert!(!hash.as_str().is_empty());
158
159        // Clean up
160        fs::remove_dir_all(test_path).unwrap();
161    }
162
163    #[test]
164    fn test_commit_empty_message() {
165        let test_path = "/tmp/test_commit_empty_msg_repo";
166        let repo = create_test_repo(test_path);
167
168        // Create and stage a file
169        create_and_stage_file(&repo, test_path, "test.txt", "test content");
170
171        // Try to commit with empty message
172        let result = repo.commit("");
173        assert!(result.is_err());
174        
175        if let Err(crate::error::GitError::CommandFailed(msg)) = result {
176            assert!(msg.contains("empty"));
177        } else {
178            panic!("Expected CommandFailed error");
179        }
180
181        // Clean up
182        fs::remove_dir_all(test_path).unwrap();
183    }
184
185    #[test]
186    fn test_commit_no_staged_changes() {
187        let test_path = "/tmp/test_commit_no_changes_repo";
188        let repo = create_test_repo(test_path);
189
190        // Try to commit without staging anything
191        let result = repo.commit("Test commit");
192        assert!(result.is_err());
193        
194        if let Err(crate::error::GitError::CommandFailed(msg)) = result {
195            assert!(msg.contains("No changes staged"));
196        } else {
197            panic!("Expected CommandFailed error");
198        }
199
200        // Clean up
201        fs::remove_dir_all(test_path).unwrap();
202    }
203
204    #[test]
205    fn test_hash_display() {
206        let hash = Hash("abc123def456".to_string());
207        assert_eq!(hash.as_str(), "abc123def456");
208        assert_eq!(hash.short(), "abc123d");
209        assert_eq!(format!("{}", hash), "abc123def456");
210    }
211
212    #[test]
213    fn test_hash_short_hash() {
214        let hash = Hash("abc".to_string());
215        assert_eq!(hash.short(), "abc"); // Less than 7 chars, returns full hash
216    }
217
218    #[test]
219    fn test_commit_with_author_empty_author() {
220        let test_path = "/tmp/test_commit_empty_author_repo";
221        let repo = create_test_repo(test_path);
222
223        // Create and stage a file
224        create_and_stage_file(&repo, test_path, "test.txt", "test content");
225
226        // Try to commit with empty author
227        let result = repo.commit_with_author("Test commit", "");
228        assert!(result.is_err());
229
230        // Clean up
231        fs::remove_dir_all(test_path).unwrap();
232    }
233}