1use crate::utils::git;
2use crate::{Repository, Result};
3use std::path::Path;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum ResetMode {
7 Soft,
8 Mixed,
9 Hard,
10}
11
12impl ResetMode {
13 pub const fn as_str(&self) -> &'static str {
14 match self {
15 ResetMode::Soft => "--soft",
16 ResetMode::Mixed => "--mixed",
17 ResetMode::Hard => "--hard",
18 }
19 }
20}
21
22pub fn reset<P: AsRef<Path>>(repo_path: P, mode: ResetMode, commit: &str) -> Result<()> {
23 let args = vec!["reset", mode.as_str(), commit];
24 git(&args, Some(repo_path.as_ref()))?;
25 Ok(())
26}
27
28impl Repository {
29 pub fn reset_soft(&self, commit: &str) -> Result<()> {
42 Self::ensure_git()?;
43 reset(self.repo_path(), ResetMode::Soft, commit)?;
44 Ok(())
45 }
46
47 pub fn reset_mixed(&self, commit: &str) -> Result<()> {
60 Self::ensure_git()?;
61 reset(self.repo_path(), ResetMode::Mixed, commit)?;
62 Ok(())
63 }
64
65 pub fn reset_hard(&self, commit: &str) -> Result<()> {
78 Self::ensure_git()?;
79 reset(self.repo_path(), ResetMode::Hard, commit)?;
80 Ok(())
81 }
82
83 pub fn reset_with_mode(&self, commit: &str, mode: ResetMode) -> Result<()> {
96 Self::ensure_git()?;
97 reset(self.repo_path(), mode, commit)?;
98 Ok(())
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::Repository;
106 use std::path::PathBuf;
107 use std::{env, fs};
108
109 fn create_test_repo(test_name: &str) -> (PathBuf, Repository) {
110 let temp_dir = env::temp_dir().join(format!("rustic_git_reset_test_{}", test_name));
111
112 if temp_dir.exists() {
114 fs::remove_dir_all(&temp_dir).unwrap();
115 }
116
117 let repo = Repository::init(&temp_dir, false).unwrap();
118
119 repo.config()
121 .set_user("Test User", "test@example.com")
122 .unwrap();
123
124 (temp_dir, repo)
125 }
126
127 fn create_file_and_commit(
128 repo: &Repository,
129 temp_dir: &Path,
130 filename: &str,
131 content: &str,
132 message: &str,
133 ) -> String {
134 let file_path = temp_dir.join(filename);
135 fs::write(&file_path, content).unwrap();
136 repo.add(&[filename]).unwrap();
137 repo.commit(message).unwrap().to_string()
138 }
139
140 #[test]
141 fn test_reset_mode_as_str() {
142 assert_eq!(ResetMode::Soft.as_str(), "--soft");
143 assert_eq!(ResetMode::Mixed.as_str(), "--mixed");
144 assert_eq!(ResetMode::Hard.as_str(), "--hard");
145 }
146
147 #[test]
148 fn test_reset_soft() {
149 let (temp_dir, repo) = create_test_repo("reset_soft");
150
151 let first_commit =
153 create_file_and_commit(&repo, &temp_dir, "file1.txt", "content1", "First commit");
154
155 let _second_commit =
157 create_file_and_commit(&repo, &temp_dir, "file2.txt", "content2", "Second commit");
158
159 reset(&temp_dir, ResetMode::Soft, &first_commit).unwrap();
161
162 let status = repo.status().unwrap();
164 assert_eq!(status.staged_files().count(), 1);
165 assert!(
166 status
167 .staged_files()
168 .any(|f| f.path.file_name().unwrap() == "file2.txt")
169 );
170
171 assert!(temp_dir.join("file2.txt").exists());
173
174 fs::remove_dir_all(&temp_dir).unwrap();
176 }
177
178 #[test]
179 fn test_reset_mixed() {
180 let (temp_dir, repo) = create_test_repo("reset_mixed");
181
182 let first_commit =
184 create_file_and_commit(&repo, &temp_dir, "file1.txt", "content1", "First commit");
185
186 let _second_commit =
188 create_file_and_commit(&repo, &temp_dir, "file2.txt", "content2", "Second commit");
189
190 reset(&temp_dir, ResetMode::Mixed, &first_commit).unwrap();
192
193 let status = repo.status().unwrap();
195 assert_eq!(status.staged_files().count(), 0);
196
197 assert!(temp_dir.join("file2.txt").exists());
199 assert!(
200 status
201 .untracked_entries()
202 .any(|f| f.path.file_name().unwrap() == "file2.txt")
203 );
204 }
205
206 #[test]
207 fn test_reset_hard() {
208 let (temp_dir, repo) = create_test_repo("reset_hard");
209
210 let first_commit =
212 create_file_and_commit(&repo, &temp_dir, "file1.txt", "content1", "First commit");
213
214 let _second_commit =
216 create_file_and_commit(&repo, &temp_dir, "file2.txt", "content2", "Second commit");
217
218 reset(&temp_dir, ResetMode::Hard, &first_commit).unwrap();
220
221 let status = repo.status().unwrap();
223 assert_eq!(status.staged_files().count(), 0);
224
225 assert!(!temp_dir.join("file2.txt").exists());
227 assert_eq!(status.untracked_entries().count(), 0);
228 }
229
230 #[test]
231 fn test_reset_invalid_commit() {
232 let (temp_dir, _repo) = create_test_repo("reset_invalid_commit");
233
234 let result = reset(&temp_dir, ResetMode::Mixed, "invalid_commit_hash");
235 assert!(result.is_err());
236 }
237
238 #[test]
239 fn test_reset_head() {
240 let (temp_dir, repo) = create_test_repo("reset_head");
241
242 create_file_and_commit(&repo, &temp_dir, "file1.txt", "content1", "Initial commit");
244
245 fs::write(temp_dir.join("file1.txt"), "modified").unwrap();
247 repo.add(&["file1.txt"]).unwrap();
248
249 reset(temp_dir, ResetMode::Mixed, "HEAD").unwrap();
251
252 let status = repo.status().unwrap();
254 assert_eq!(status.staged_files().count(), 0);
255 assert_eq!(status.unstaged_files().count(), 1);
256 }
257
258 #[test]
260 fn test_repository_reset_soft() {
261 let (temp_dir, repo) = create_test_repo("repository_reset_soft");
262
263 let first_commit =
265 create_file_and_commit(&repo, &temp_dir, "file1.txt", "content1", "First commit");
266
267 let _second_commit =
269 create_file_and_commit(&repo, &temp_dir, "file2.txt", "content2", "Second commit");
270
271 repo.reset_soft(&first_commit).unwrap();
273
274 let status = repo.status().unwrap();
276 assert_eq!(status.staged_files().count(), 1);
277 assert!(
278 status
279 .staged_files()
280 .any(|f| f.path.file_name().unwrap() == "file2.txt")
281 );
282 }
283
284 #[test]
285 fn test_repository_reset_mixed() {
286 let (temp_dir, repo) = create_test_repo("repository_reset_mixed");
287
288 let first_commit =
290 create_file_and_commit(&repo, &temp_dir, "file1.txt", "content1", "First commit");
291
292 let _second_commit =
294 create_file_and_commit(&repo, &temp_dir, "file2.txt", "content2", "Second commit");
295
296 repo.reset_mixed(&first_commit).unwrap();
298
299 let status = repo.status().unwrap();
301 assert_eq!(status.staged_files().count(), 0);
302 assert!(temp_dir.join("file2.txt").exists());
303 assert!(
304 status
305 .untracked_entries()
306 .any(|f| f.path.file_name().unwrap() == "file2.txt")
307 );
308 }
309
310 #[test]
311 fn test_repository_reset_hard() {
312 let (temp_dir, repo) = create_test_repo("repository_reset_hard");
313
314 let first_commit =
316 create_file_and_commit(&repo, &temp_dir, "file1.txt", "content1", "First commit");
317
318 let _second_commit =
320 create_file_and_commit(&repo, &temp_dir, "file2.txt", "content2", "Second commit");
321
322 repo.reset_hard(&first_commit).unwrap();
324
325 let status = repo.status().unwrap();
327 assert_eq!(status.staged_files().count(), 0);
328 assert!(!temp_dir.join("file2.txt").exists());
329 assert_eq!(status.untracked_entries().count(), 0);
330
331 fs::remove_dir_all(&temp_dir).unwrap();
333 }
334
335 #[test]
336 fn test_repository_reset_with_mode() {
337 let (temp_dir, repo) = create_test_repo("repository_reset_with_mode");
338
339 let first_commit =
341 create_file_and_commit(&repo, &temp_dir, "file1.txt", "content1", "First commit");
342
343 let _second_commit =
345 create_file_and_commit(&repo, &temp_dir, "file2.txt", "content2", "Second commit");
346
347 repo.reset_with_mode(&first_commit, ResetMode::Mixed)
349 .unwrap();
350
351 let status = repo.status().unwrap();
353 assert_eq!(status.staged_files().count(), 0);
354 assert!(temp_dir.join("file2.txt").exists());
355
356 fs::remove_dir_all(&temp_dir).unwrap();
358 }
359}