rustic_git/commands/
commit.rs

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