rustic_git/commands/
commit.rs1use crate::{Repository, Result, Hash};
2use crate::utils::git;
3
4impl Repository {
5 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 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 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 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 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 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 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_file(&repo, test_path, "test.txt", "test content");
127
128 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 let status = repo.status().unwrap();
138 assert!(status.is_clean());
139
140 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_file(&repo, test_path, "test.txt", "test content");
151
152 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 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_file(&repo, test_path, "test.txt", "test content");
170
171 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 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 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 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"); }
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_file(&repo, test_path, "test.txt", "test content");
225
226 let result = repo.commit_with_author("Test commit", "");
228 assert!(result.is_err());
229
230 fs::remove_dir_all(test_path).unwrap();
232 }
233}