1use chrono::{DateTime, Local, TimeZone};
2use git2::{DiffOptions, Repository, Sort};
3use std::io::Write;
4
5use crate::config::FileConfig;
6use crate::error::{GcopError, Result};
7use crate::git::{CommitInfo, DiffStats, GitOperations};
8
9const DEFAULT_MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
11
12pub struct GitRepository {
14 pub(crate) repo: Repository,
15 max_file_size: u64,
16}
17
18impl GitRepository {
19 pub fn open(file_config: Option<&FileConfig>) -> Result<Self> {
24 let repo = Repository::discover(".")?;
25 let max_file_size = file_config
26 .map(|c| c.max_size)
27 .unwrap_or(DEFAULT_MAX_FILE_SIZE);
28 Ok(Self {
29 repo,
30 max_file_size,
31 })
32 }
33
34 fn diff_to_string(&self, diff: &git2::Diff) -> Result<String> {
36 let mut output = Vec::new();
37 diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
38 let origin = line.origin();
40
41 match origin {
43 '+' | '-' | ' ' => {
44 let _ = output.write_all(&[origin as u8]);
45 }
46 _ => {}
47 }
48
49 let _ = output.write_all(line.content());
51 true
52 })?;
53 Ok(String::from_utf8_lossy(&output).to_string())
54 }
55}
56
57impl GitOperations for GitRepository {
58 fn get_staged_diff(&self) -> Result<String> {
59 let index = self.repo.index()?;
61
62 if self.is_empty()? {
64 let mut opts = DiffOptions::new();
65 let diff = self
66 .repo
67 .diff_tree_to_index(None, Some(&index), Some(&mut opts))?;
68 return self.diff_to_string(&diff);
69 }
70
71 let head = self.repo.head()?;
73 let head_tree = head.peel_to_tree()?;
74
75 let mut opts = DiffOptions::new();
77 let diff = self
78 .repo
79 .diff_tree_to_index(Some(&head_tree), Some(&index), Some(&mut opts))?;
80
81 self.diff_to_string(&diff)
82 }
83
84 fn get_uncommitted_diff(&self) -> Result<String> {
85 let index = self.repo.index()?;
87
88 let mut opts = DiffOptions::new();
90 let diff = self
91 .repo
92 .diff_index_to_workdir(Some(&index), Some(&mut opts))?;
93
94 self.diff_to_string(&diff)
95 }
96
97 fn get_commit_diff(&self, commit_hash: &str) -> Result<String> {
98 let commit = self
100 .repo
101 .revparse_single(commit_hash)
102 .and_then(|obj| obj.peel_to_commit())
103 .map_err(|_| {
104 GcopError::InvalidInput(
105 rust_i18n::t!("git.invalid_commit_hash", hash = commit_hash).to_string(),
106 )
107 })?;
108
109 let commit_tree = commit.tree()?;
110
111 let parent_tree = if commit.parent_count() > 0 {
113 Some(commit.parent(0)?.tree()?)
114 } else {
115 None
116 };
117
118 let mut opts = DiffOptions::new();
120 let diff = self.repo.diff_tree_to_tree(
121 parent_tree.as_ref(),
122 Some(&commit_tree),
123 Some(&mut opts),
124 )?;
125
126 self.diff_to_string(&diff)
127 }
128
129 fn get_range_diff(&self, range: &str) -> Result<String> {
130 let parts: Vec<&str> = range.split("..").collect();
132 if parts.len() != 2 {
133 return Err(GcopError::InvalidInput(
134 rust_i18n::t!("git.invalid_range_format", range = range).to_string(),
135 ));
136 }
137
138 let base_commit = self.repo.revparse_single(parts[0])?.peel_to_commit()?;
139 let head_commit = self.repo.revparse_single(parts[1])?.peel_to_commit()?;
140
141 let base_tree = base_commit.tree()?;
142 let head_tree = head_commit.tree()?;
143
144 let mut opts = DiffOptions::new();
145 let diff =
146 self.repo
147 .diff_tree_to_tree(Some(&base_tree), Some(&head_tree), Some(&mut opts))?;
148
149 self.diff_to_string(&diff)
150 }
151
152 fn get_file_content(&self, path: &str) -> Result<String> {
153 let metadata = std::fs::metadata(path)?;
154 if metadata.len() > self.max_file_size {
155 return Err(GcopError::InvalidInput(
156 rust_i18n::t!(
157 "git.file_too_large",
158 size = metadata.len(),
159 max = self.max_file_size
160 )
161 .to_string(),
162 ));
163 }
164
165 let content = std::fs::read_to_string(path)?;
166 Ok(content)
167 }
168
169 fn commit(&self, message: &str) -> Result<()> {
170 crate::git::commit::commit_changes(message)
171 }
172
173 fn commit_amend(&self, message: &str) -> Result<()> {
174 crate::git::commit::commit_amend_changes(message)
175 }
176
177 fn get_current_branch(&self) -> Result<Option<String>> {
178 if self.is_empty()? {
180 return Ok(None);
181 }
182
183 let head = self.repo.head()?;
184
185 if head.is_branch() {
186 let branch_name = head.shorthand().map(|s| s.to_string());
188 Ok(branch_name)
189 } else {
190 Ok(None)
192 }
193 }
194
195 fn get_diff_stats(&self, diff: &str) -> Result<DiffStats> {
196 crate::git::diff::parse_diff_stats(diff)
197 }
198
199 fn has_staged_changes(&self) -> Result<bool> {
200 let diff = self.get_staged_diff()?;
201 Ok(!diff.trim().is_empty())
202 }
203
204 fn get_commit_history(&self) -> Result<Vec<CommitInfo>> {
205 if self.is_empty()? {
207 return Ok(Vec::new());
208 }
209
210 let mut revwalk = self.repo.revwalk()?;
211 revwalk.push_head()?;
212 revwalk.set_sorting(Sort::TIME)?;
213
214 let mut commits = Vec::new();
215
216 for oid in revwalk {
217 let oid = oid?;
218 let commit = self.repo.find_commit(oid)?;
219
220 let hash = oid.to_string();
221 let parent_count = commit.parent_count();
222 let author = commit.author();
223 let author_name = author.name().unwrap_or("Unknown").to_string();
224 let author_email = author.email().unwrap_or("").to_string();
225
226 let git_time = commit.time();
228 let timestamp: DateTime<Local> = Local
229 .timestamp_opt(git_time.seconds(), 0)
230 .single()
231 .unwrap_or_else(|| {
232 tracing::warn!(
233 "Invalid git timestamp {} for commit {}",
234 git_time.seconds(),
235 commit.id()
236 );
237 Local::now()
238 });
239
240 let message = commit
241 .message()
242 .unwrap_or("")
243 .lines()
244 .next()
245 .unwrap_or("")
246 .to_string();
247
248 commits.push(CommitInfo {
249 hash,
250 parent_count,
251 author_name,
252 author_email,
253 timestamp,
254 message,
255 });
256 }
257
258 Ok(commits)
259 }
260
261 fn get_commit_line_stats(&self, hash: &str) -> Result<(usize, usize)> {
262 let commit = self
263 .repo
264 .revparse_single(hash)
265 .and_then(|obj| obj.peel_to_commit())
266 .map_err(|_| {
267 GcopError::InvalidInput(
268 rust_i18n::t!("git.invalid_commit_hash", hash = hash).to_string(),
269 )
270 })?;
271
272 let commit_tree = commit.tree()?;
273 let parent_tree = if commit.parent_count() > 0 {
274 Some(commit.parent(0)?.tree()?)
275 } else {
276 None
277 };
278
279 let mut opts = DiffOptions::new();
280 let diff = self.repo.diff_tree_to_tree(
281 parent_tree.as_ref(),
282 Some(&commit_tree),
283 Some(&mut opts),
284 )?;
285
286 let stats = diff.stats()?;
287 Ok((stats.insertions(), stats.deletions()))
288 }
289
290 fn is_empty(&self) -> Result<bool> {
291 match self.repo.head() {
293 Ok(_) => Ok(false),
294 Err(e) if e.code() == git2::ErrorCode::UnbornBranch => Ok(true),
295 Err(e) => Err(e.into()),
296 }
297 }
298
299 fn get_staged_files(&self) -> Result<Vec<String>> {
300 let mut index = self.repo.index()?;
301 index.read(true)?;
304 let tree = if self.is_empty()? {
305 None
306 } else {
307 let head = self.repo.head()?;
308 Some(head.peel_to_tree()?)
309 };
310 let mut opts = DiffOptions::new();
311 let diff = self
312 .repo
313 .diff_tree_to_index(tree.as_ref(), Some(&index), Some(&mut opts))?;
314
315 Ok(diff
316 .deltas()
317 .filter_map(|delta| delta.new_file().path())
318 .map(|p| p.to_string_lossy().into_owned())
319 .collect())
320 }
321
322 fn unstage_all(&self) -> Result<()> {
323 use std::process::Command;
324
325 let workdir = self.get_workdir()?;
326
327 if self.is_empty()? {
328 let output = Command::new("git")
330 .current_dir(workdir)
331 .args(["rm", "--cached", "-r", "."])
332 .output()?;
333 if !output.status.success() {
334 let stderr = String::from_utf8_lossy(&output.stderr);
335 return Err(crate::error::GcopError::GitCommand(
336 stderr.trim().to_string(),
337 ));
338 }
339 } else {
340 let output = Command::new("git")
341 .current_dir(workdir)
342 .args(["reset", "HEAD"])
343 .output()?;
344 if !output.status.success() {
345 let stderr = String::from_utf8_lossy(&output.stderr);
346 return Err(crate::error::GcopError::GitCommand(
347 stderr.trim().to_string(),
348 ));
349 }
350 }
351 Ok(())
352 }
353
354 fn stage_files(&self, files: &[String]) -> Result<()> {
355 use std::process::Command;
356
357 if files.is_empty() {
358 return Ok(());
359 }
360
361 let workdir = self.get_workdir()?;
362
363 let output = Command::new("git")
364 .current_dir(workdir)
365 .env("GIT_LITERAL_PATHSPECS", "1")
366 .arg("add")
367 .args(files)
368 .output()?;
369
370 if !output.status.success() {
371 let stderr = String::from_utf8_lossy(&output.stderr);
372 return Err(crate::error::GcopError::GitCommand(
373 stderr.trim().to_string(),
374 ));
375 }
376 Ok(())
377 }
378
379 fn get_workdir(&self) -> Result<std::path::PathBuf> {
380 self.repo
381 .workdir()
382 .ok_or_else(|| crate::error::GcopError::GitCommand("bare repository".to_string()))
383 .map(|p| p.to_path_buf())
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use std::fs;
391 use std::path::Path;
392 use tempfile::TempDir;
393
394 fn create_test_repo() -> (TempDir, GitRepository) {
396 let dir = TempDir::new().unwrap();
397 let repo = Repository::init(dir.path()).unwrap();
398
399 let mut config = repo.config().unwrap();
401 config.set_str("user.name", "Test User").unwrap();
402 config.set_str("user.email", "test@example.com").unwrap();
403
404 let git_repo = GitRepository {
405 repo,
406 max_file_size: DEFAULT_MAX_FILE_SIZE,
407 };
408
409 (dir, git_repo)
410 }
411
412 fn create_file(dir: &Path, name: &str, content: &str) {
414 let file_path = dir.join(name);
415 fs::write(&file_path, content).unwrap();
416 }
417
418 fn stage_file(repo: &Repository, name: &str) {
420 let mut index = repo.index().unwrap();
421 index.add_path(Path::new(name)).unwrap();
422 index.write().unwrap();
423 }
424
425 fn create_commit(repo: &Repository, message: &str) {
427 let mut index = repo.index().unwrap();
428 let oid = index.write_tree().unwrap();
429 let tree = repo.find_tree(oid).unwrap();
430 let sig = repo.signature().unwrap();
431
432 let parent_commit = repo.head().ok().and_then(|h| h.peel_to_commit().ok());
433
434 if let Some(parent) = parent_commit {
435 repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&parent])
436 .unwrap();
437 } else {
438 repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[])
439 .unwrap();
440 }
441 }
442
443 #[test]
446 fn test_is_empty_true_for_new_repo() {
447 let (_dir, git_repo) = create_test_repo();
448 assert!(git_repo.is_empty().unwrap());
449 }
450
451 #[test]
452 fn test_is_empty_false_after_commit() {
453 let (dir, git_repo) = create_test_repo();
454 create_file(dir.path(), "test.txt", "hello");
455 stage_file(&git_repo.repo, "test.txt");
456 create_commit(&git_repo.repo, "Initial commit");
457
458 assert!(!git_repo.is_empty().unwrap());
459 }
460
461 #[test]
464 fn test_get_current_branch_empty_repo() {
465 let (_dir, git_repo) = create_test_repo();
466 assert_eq!(git_repo.get_current_branch().unwrap(), None);
467 }
468
469 #[test]
470 fn test_get_current_branch_normal() {
471 let (dir, git_repo) = create_test_repo();
472 create_file(dir.path(), "test.txt", "hello");
473 stage_file(&git_repo.repo, "test.txt");
474 create_commit(&git_repo.repo, "Initial commit");
475
476 let branch = git_repo.get_current_branch().unwrap();
477 assert!(branch.is_some());
478 let branch_name = branch.unwrap();
480 assert!(branch_name == "master" || branch_name == "main");
481 }
482
483 #[test]
484 fn test_get_current_branch_detached_head() {
485 let (dir, git_repo) = create_test_repo();
486 create_file(dir.path(), "test.txt", "hello");
487 stage_file(&git_repo.repo, "test.txt");
488 create_commit(&git_repo.repo, "Initial commit");
489
490 let head = git_repo.repo.head().unwrap();
492 let commit = head.peel_to_commit().unwrap();
493 git_repo.repo.set_head_detached(commit.id()).unwrap();
494
495 assert_eq!(git_repo.get_current_branch().unwrap(), None);
496 }
497
498 #[test]
501 fn test_has_staged_changes_false_empty_repo() {
502 let (_dir, git_repo) = create_test_repo();
503 assert!(!git_repo.has_staged_changes().unwrap());
504 }
505
506 #[test]
507 fn test_has_staged_changes_true() {
508 let (dir, git_repo) = create_test_repo();
509 create_file(dir.path(), "test.txt", "hello");
510 stage_file(&git_repo.repo, "test.txt");
511
512 assert!(git_repo.has_staged_changes().unwrap());
513 }
514
515 #[test]
516 fn test_has_staged_changes_false_after_commit() {
517 let (dir, git_repo) = create_test_repo();
518 create_file(dir.path(), "test.txt", "hello");
519 stage_file(&git_repo.repo, "test.txt");
520 create_commit(&git_repo.repo, "Initial commit");
521
522 assert!(!git_repo.has_staged_changes().unwrap());
523 }
524
525 #[test]
528 fn test_get_staged_diff_empty_repo() {
529 let (dir, git_repo) = create_test_repo();
530 create_file(dir.path(), "test.txt", "hello world");
531 stage_file(&git_repo.repo, "test.txt");
532
533 let diff = git_repo.get_staged_diff().unwrap();
534 assert!(diff.contains("hello world"));
535 assert!(diff.contains("+hello world"));
536 }
537
538 #[test]
539 fn test_get_staged_diff_normal() {
540 let (dir, git_repo) = create_test_repo();
541 create_file(dir.path(), "test.txt", "hello");
542 stage_file(&git_repo.repo, "test.txt");
543 create_commit(&git_repo.repo, "Initial commit");
544
545 create_file(dir.path(), "test.txt", "hello world");
547 stage_file(&git_repo.repo, "test.txt");
548
549 let diff = git_repo.get_staged_diff().unwrap();
550 assert!(diff.contains("-hello"));
551 assert!(diff.contains("+hello world"));
552 }
553
554 #[test]
557 fn test_get_uncommitted_diff() {
558 let (dir, git_repo) = create_test_repo();
559 create_file(dir.path(), "test.txt", "hello");
560 stage_file(&git_repo.repo, "test.txt");
561 create_commit(&git_repo.repo, "Initial commit");
562
563 create_file(dir.path(), "test.txt", "hello world");
565
566 let diff = git_repo.get_uncommitted_diff().unwrap();
567 assert!(diff.contains("-hello"));
568 assert!(diff.contains("+hello world"));
569 }
570
571 #[test]
574 fn test_get_commit_diff_initial_commit() {
575 let (dir, git_repo) = create_test_repo();
576 create_file(dir.path(), "test.txt", "hello");
577 stage_file(&git_repo.repo, "test.txt");
578 create_commit(&git_repo.repo, "Initial commit");
579
580 let head = git_repo.repo.head().unwrap();
581 let commit = head.peel_to_commit().unwrap();
582 let hash = commit.id().to_string();
583
584 let diff = git_repo.get_commit_diff(&hash).unwrap();
585 assert!(diff.contains("+hello"));
586 }
587
588 #[test]
589 fn test_get_commit_diff_normal() {
590 let (dir, git_repo) = create_test_repo();
591 create_file(dir.path(), "test.txt", "hello");
592 stage_file(&git_repo.repo, "test.txt");
593 create_commit(&git_repo.repo, "Initial commit");
594
595 create_file(dir.path(), "test.txt", "hello world");
597 stage_file(&git_repo.repo, "test.txt");
598 create_commit(&git_repo.repo, "Second commit");
599
600 let head = git_repo.repo.head().unwrap();
601 let commit = head.peel_to_commit().unwrap();
602 let hash = commit.id().to_string();
603
604 let diff = git_repo.get_commit_diff(&hash).unwrap();
605 assert!(diff.contains("-hello"));
606 assert!(diff.contains("+hello world"));
607 }
608
609 #[test]
610 fn test_get_commit_diff_invalid_hash() {
611 let (_dir, git_repo) = create_test_repo();
612 let result = git_repo.get_commit_diff("invalid_hash");
613 assert!(result.is_err());
614 }
615
616 #[test]
619 fn test_get_range_diff() {
620 let (dir, git_repo) = create_test_repo();
621 create_file(dir.path(), "test.txt", "version1");
622 stage_file(&git_repo.repo, "test.txt");
623 create_commit(&git_repo.repo, "First commit");
624
625 let first_commit = git_repo.repo.head().unwrap().peel_to_commit().unwrap();
626
627 create_file(dir.path(), "test.txt", "version2");
628 stage_file(&git_repo.repo, "test.txt");
629 create_commit(&git_repo.repo, "Second commit");
630
631 let second_commit = git_repo.repo.head().unwrap().peel_to_commit().unwrap();
632
633 let range = format!("{}..{}", first_commit.id(), second_commit.id());
634 let diff = git_repo.get_range_diff(&range).unwrap();
635
636 assert!(diff.contains("-version1"));
637 assert!(diff.contains("+version2"));
638 }
639
640 #[test]
641 fn test_get_range_diff_invalid_format() {
642 let (dir, git_repo) = create_test_repo();
643 create_file(dir.path(), "test.txt", "hello");
644 stage_file(&git_repo.repo, "test.txt");
645 create_commit(&git_repo.repo, "Initial commit");
646
647 let result = git_repo.get_range_diff("invalid_range");
648 assert!(result.is_err());
649 }
650
651 #[test]
654 fn test_get_file_content() {
655 let (dir, git_repo) = create_test_repo();
656 let file_path = dir.path().join("test.txt");
657 fs::write(&file_path, "hello world").unwrap();
658
659 let content = git_repo
660 .get_file_content(file_path.to_str().unwrap())
661 .unwrap();
662 assert_eq!(content, "hello world");
663 }
664
665 #[test]
666 fn test_get_file_content_too_large() {
667 let (dir, git_repo) = create_test_repo();
668 let file_path = dir.path().join("large.txt");
669
670 let large_content = "x".repeat((DEFAULT_MAX_FILE_SIZE + 1) as usize);
672 fs::write(&file_path, large_content).unwrap();
673
674 let result = git_repo.get_file_content(file_path.to_str().unwrap());
675 assert!(result.is_err());
676 }
677
678 #[test]
681 fn test_get_commit_history_empty_repo() {
682 let (_dir, git_repo) = create_test_repo();
683 let commits = git_repo.get_commit_history().unwrap();
684 assert!(commits.is_empty());
685 }
686
687 #[test]
688 fn test_get_commit_history() {
689 let (dir, git_repo) = create_test_repo();
690
691 create_file(dir.path(), "test.txt", "v1");
692 stage_file(&git_repo.repo, "test.txt");
693 create_commit(&git_repo.repo, "First commit");
694
695 create_file(dir.path(), "test.txt", "v2");
696 stage_file(&git_repo.repo, "test.txt");
697 create_commit(&git_repo.repo, "Second commit");
698
699 let commits = git_repo.get_commit_history().unwrap();
700 assert_eq!(commits.len(), 2);
701 assert_eq!(commits[0].message, "Second commit");
702 assert_eq!(commits[1].message, "First commit");
703 assert_eq!(commits[0].author_name, "Test User");
704 assert_eq!(commits[0].author_email, "test@example.com");
705 }
706
707 #[test]
710 fn test_get_diff_stats() {
711 let (_dir, git_repo) = create_test_repo();
712 let diff = r#"
713diff --git a/test.txt b/test.txt
714index 1234567..abcdefg 100644
715--- a/test.txt
716+++ b/test.txt
717@@ -1,1 +1,2 @@
718 hello
719+world
720"#;
721 let stats = git_repo.get_diff_stats(diff).unwrap();
722 assert_eq!(stats.files_changed.len(), 1);
723 assert_eq!(stats.insertions, 1);
724 assert_eq!(stats.deletions, 0);
725 }
726
727 #[test]
730 fn test_stage_files_literal_glob_path() {
731 let (dir, git_repo) = create_test_repo();
735
736 create_file(dir.path(), "init.txt", "init");
738 stage_file(&git_repo.repo, "init.txt");
739 create_commit(&git_repo.repo, "initial");
740
741 let bracket_dir = dir.path().join("[locale]");
743 let sibling_dir = dir.path().join("l");
744 fs::create_dir_all(&bracket_dir).unwrap();
745 fs::create_dir_all(&sibling_dir).unwrap();
746
747 fs::write(bracket_dir.join("page.tsx"), "bracket content").unwrap();
748 fs::write(sibling_dir.join("page.tsx"), "sibling content").unwrap();
749
750 let mut index = git_repo.repo.index().unwrap();
752 index
753 .add_path(std::path::Path::new("[locale]/page.tsx"))
754 .unwrap();
755 index.write().unwrap();
756
757 git_repo.unstage_all().unwrap();
759
760 git_repo
761 .stage_files(&["[locale]/page.tsx".to_string()])
762 .unwrap();
763
764 let staged = git_repo.get_staged_files().unwrap();
766 assert!(
767 staged.contains(&"[locale]/page.tsx".to_string()),
768 "expected [locale]/page.tsx to be staged"
769 );
770 assert!(
771 !staged.contains(&"l/page.tsx".to_string()),
772 "l/page.tsx should NOT be staged (glob expansion guard)"
773 );
774 }
775
776 #[test]
777 fn test_stage_files_glob_path_missing_literal_errors_not_sibling() {
778 let (dir, git_repo) = create_test_repo();
781
782 create_file(dir.path(), "init.txt", "init");
783 stage_file(&git_repo.repo, "init.txt");
784 create_commit(&git_repo.repo, "initial");
785
786 let sibling_dir = dir.path().join("l");
788 fs::create_dir_all(&sibling_dir).unwrap();
789 fs::write(sibling_dir.join("page.tsx"), "sibling").unwrap();
790
791 let result = git_repo.stage_files(&["[locale]/page.tsx".to_string()]);
793 assert!(
794 result.is_err(),
795 "staging a non-existent literal path should fail"
796 );
797
798 let staged = git_repo.get_staged_files().unwrap();
800 assert!(
801 !staged.contains(&"l/page.tsx".to_string()),
802 "l/page.tsx must NOT be staged as a glob side-effect"
803 );
804 }
805
806 #[test]
807 fn test_unstage_all_then_stage_subset_does_not_touch_unstaged_file() {
808 let (dir, git_repo) = create_test_repo();
812
813 create_file(dir.path(), "a.rs", "v1");
815 create_file(dir.path(), "b.rs", "v1");
816 create_file(dir.path(), "c.rs", "v1");
817 stage_file(&git_repo.repo, "a.rs");
818 stage_file(&git_repo.repo, "b.rs");
819 stage_file(&git_repo.repo, "c.rs");
820 create_commit(&git_repo.repo, "initial");
821
822 create_file(dir.path(), "a.rs", "v2");
824 create_file(dir.path(), "b.rs", "v2");
825 create_file(dir.path(), "c.rs", "v2");
826 stage_file(&git_repo.repo, "a.rs");
827 stage_file(&git_repo.repo, "b.rs");
828 let staged_before = git_repo.get_staged_files().unwrap();
831 assert!(staged_before.contains(&"a.rs".to_string()));
832 assert!(staged_before.contains(&"b.rs".to_string()));
833 assert!(!staged_before.contains(&"c.rs".to_string()));
834
835 git_repo.unstage_all().unwrap();
837 git_repo.stage_files(&["a.rs".to_string()]).unwrap();
838
839 let staged_after = git_repo.get_staged_files().unwrap();
840 assert!(
841 staged_after.contains(&"a.rs".to_string()),
842 "a.rs should be staged"
843 );
844 assert!(
845 !staged_after.contains(&"b.rs".to_string()),
846 "b.rs should NOT be staged (belongs to a different group)"
847 );
848 assert!(
849 !staged_after.contains(&"c.rs".to_string()),
850 "c.rs should NOT be staged (was never in the staging area)"
851 );
852 }
853}