1use std::future::Future;
4use std::path::PathBuf;
5use std::pin::Pin;
6
7use super::error::GitError;
8use super::merge::SquashMergeOutcome;
9use super::rebase::{InProgressGitOperation, RebaseStepResult};
10#[cfg(test)]
11use super::sync;
12use super::sync::{BranchTrackingMap, PullRebaseResult, SingleCommitMessageStrategy};
13use super::{
14 abort_rebase, branch_tracking_statuses, commit_all, commit_all_preserving_single_commit,
15 create_worktree, current_upstream_reference, delete_branch, detect_git_info, diff,
16 fetch_remote, find_git_repo_root, get_ahead_behind, get_ref_ahead_behind, has_commits_since,
17 has_unmerged_paths, head_commit_message, head_hash, head_short_hash, in_progress_operation,
18 is_rebase_in_progress, is_worktree_clean, list_conflicted_files, list_local_commit_titles,
19 list_staged_conflict_marker_files, list_upstream_commit_titles, main_checkout_working_tree,
20 main_repo_root, pull_rebase, push_current_branch, push_current_branch_to_remote_branch, rebase,
21 rebase_continue, rebase_onto_start, rebase_start, ref_hash, remote_branch_exists,
22 remove_worktree, repo_url, squash_merge, squash_merge_diff, stage_all, tracked_worktree_status,
23 worktree_status,
24};
25
26pub type GitFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
28
29#[cfg_attr(any(test, feature = "test-utils"), mockall::automock)]
34pub trait GitClient: Send + Sync {
35 fn detect_git_info(&self, dir: PathBuf) -> GitFuture<Option<String>>;
40
41 fn find_git_repo_root(&self, dir: PathBuf) -> GitFuture<Option<PathBuf>>;
45
46 fn create_worktree(
53 &self,
54 repo_path: PathBuf,
55 worktree_path: PathBuf,
56 branch_name: String,
57 start_ref: String,
58 ) -> GitFuture<Result<(), GitError>>;
59
60 fn remove_worktree(&self, worktree_path: PathBuf) -> GitFuture<Result<(), GitError>>;
66
67 fn squash_merge_diff(
73 &self,
74 repo_path: PathBuf,
75 source_branch: String,
76 target_branch: String,
77 ) -> GitFuture<Result<String, GitError>>;
78
79 fn squash_merge(
85 &self,
86 repo_path: PathBuf,
87 source_branch: String,
88 target_branch: String,
89 commit_message: String,
90 ) -> GitFuture<Result<SquashMergeOutcome, GitError>>;
91
92 fn rebase(&self, repo_path: PathBuf, target_branch: String) -> GitFuture<Result<(), GitError>>;
97
98 fn rebase_start(
104 &self,
105 repo_path: PathBuf,
106 target_branch: String,
107 ) -> GitFuture<Result<RebaseStepResult, GitError>>;
108
109 fn rebase_onto_start(
114 &self,
115 repo_path: PathBuf,
116 new_base: String,
117 old_base: String,
118 ) -> GitFuture<Result<RebaseStepResult, GitError>>;
119
120 fn rebase_continue(&self, repo_path: PathBuf) -> GitFuture<Result<RebaseStepResult, GitError>>;
125
126 fn abort_rebase(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>>;
131
132 fn is_rebase_in_progress(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>>;
137
138 fn in_progress_operation(
143 &self,
144 repo_path: PathBuf,
145 ) -> GitFuture<Result<Option<InProgressGitOperation>, GitError>>;
146
147 fn has_unmerged_paths(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>>;
152
153 fn list_staged_conflict_marker_files(
159 &self,
160 repo_path: PathBuf,
161 paths: Vec<String>,
162 ) -> GitFuture<Result<Vec<String>, GitError>>;
163
164 fn list_conflicted_files(&self, repo_path: PathBuf)
169 -> GitFuture<Result<Vec<String>, GitError>>;
170
171 fn commit_all(
178 &self,
179 repo_path: PathBuf,
180 message: String,
181 no_verify: bool,
182 ) -> GitFuture<Result<(), GitError>>;
183
184 fn commit_all_preserving_single_commit(
193 &self,
194 repo_path: PathBuf,
195 base_branch: String,
196 commit_message: String,
197 message_strategy: SingleCommitMessageStrategy,
198 no_verify: bool,
199 ) -> GitFuture<Result<(), GitError>>;
200
201 fn stage_all(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>>;
206
207 fn head_short_hash(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
212
213 fn head_hash(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
218
219 fn ref_hash(
224 &self,
225 repo_path: PathBuf,
226 reference: String,
227 ) -> GitFuture<Result<String, GitError>>;
228
229 fn head_commit_message(
235 &self,
236 repo_path: PathBuf,
237 ) -> GitFuture<Result<Option<String>, GitError>>;
238
239 fn delete_branch(
245 &self,
246 repo_path: PathBuf,
247 branch_name: String,
248 ) -> GitFuture<Result<(), GitError>>;
249
250 fn diff(&self, repo_path: PathBuf, base_branch: String) -> GitFuture<Result<String, GitError>>;
256
257 fn is_worktree_clean(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>>;
262
263 fn worktree_status(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
268
269 fn tracked_worktree_status(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
274
275 fn has_commits_since(
281 &self,
282 repo_path: PathBuf,
283 base_branch: String,
284 ) -> GitFuture<Result<bool, GitError>>;
285
286 fn pull_rebase(&self, repo_path: PathBuf) -> GitFuture<Result<PullRebaseResult, GitError>>;
291
292 fn push_current_branch(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
299
300 fn push_current_branch_to_remote_branch(
307 &self,
308 repo_path: PathBuf,
309 remote_branch_name: String,
310 ) -> GitFuture<Result<String, GitError>>;
311
312 fn remote_branch_exists(
318 &self,
319 repo_path: PathBuf,
320 remote_branch_name: String,
321 ) -> GitFuture<Result<bool, GitError>>;
322
323 fn current_upstream_reference(&self, repo_path: PathBuf)
328 -> GitFuture<Result<String, GitError>>;
329
330 fn fetch_remote(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>>;
335
336 fn get_ahead_behind(&self, repo_path: PathBuf) -> GitFuture<Result<(u32, u32), GitError>>;
341
342 fn get_ref_ahead_behind(
350 &self,
351 repo_path: PathBuf,
352 left_ref: String,
353 right_ref: String,
354 ) -> GitFuture<Result<(u32, u32), GitError>>;
355
356 fn branch_tracking_statuses(
365 &self,
366 repo_path: PathBuf,
367 ) -> GitFuture<Result<BranchTrackingMap, GitError>>;
368
369 fn list_upstream_commit_titles(
376 &self,
377 repo_path: PathBuf,
378 ) -> GitFuture<Result<Vec<String>, GitError>>;
379
380 fn list_local_commit_titles(
386 &self,
387 repo_path: PathBuf,
388 ) -> GitFuture<Result<Vec<String>, GitError>>;
389
390 fn repo_url(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
395
396 fn main_repo_root(&self, repo_path: PathBuf) -> GitFuture<Result<PathBuf, GitError>>;
401
402 fn main_checkout_working_tree(
410 &self,
411 repo_path: PathBuf,
412 ) -> GitFuture<Result<Option<PathBuf>, GitError>>;
413}
414
415pub struct RealGitClient;
417
418impl GitClient for RealGitClient {
419 fn detect_git_info(&self, dir: PathBuf) -> GitFuture<Option<String>> {
420 Box::pin(async move { detect_git_info(dir).await })
421 }
422
423 fn find_git_repo_root(&self, dir: PathBuf) -> GitFuture<Option<PathBuf>> {
424 Box::pin(async move { find_git_repo_root(dir).await })
425 }
426
427 fn create_worktree(
428 &self,
429 repo_path: PathBuf,
430 worktree_path: PathBuf,
431 branch_name: String,
432 start_ref: String,
433 ) -> GitFuture<Result<(), GitError>> {
434 Box::pin(
435 async move { create_worktree(repo_path, worktree_path, branch_name, start_ref).await },
436 )
437 }
438
439 fn remove_worktree(&self, worktree_path: PathBuf) -> GitFuture<Result<(), GitError>> {
440 Box::pin(async move { remove_worktree(worktree_path).await })
441 }
442
443 fn squash_merge_diff(
444 &self,
445 repo_path: PathBuf,
446 source_branch: String,
447 target_branch: String,
448 ) -> GitFuture<Result<String, GitError>> {
449 Box::pin(async move { squash_merge_diff(repo_path, source_branch, target_branch).await })
450 }
451
452 fn squash_merge(
453 &self,
454 repo_path: PathBuf,
455 source_branch: String,
456 target_branch: String,
457 commit_message: String,
458 ) -> GitFuture<Result<SquashMergeOutcome, GitError>> {
459 Box::pin(async move {
460 squash_merge(repo_path, source_branch, target_branch, commit_message).await
461 })
462 }
463
464 fn rebase(&self, repo_path: PathBuf, target_branch: String) -> GitFuture<Result<(), GitError>> {
465 Box::pin(async move { rebase::rebase(repo_path, target_branch).await })
466 }
467
468 fn rebase_start(
469 &self,
470 repo_path: PathBuf,
471 target_branch: String,
472 ) -> GitFuture<Result<RebaseStepResult, GitError>> {
473 Box::pin(async move { rebase_start(repo_path, target_branch).await })
474 }
475
476 fn rebase_onto_start(
477 &self,
478 repo_path: PathBuf,
479 new_base: String,
480 old_base: String,
481 ) -> GitFuture<Result<RebaseStepResult, GitError>> {
482 Box::pin(async move { rebase_onto_start(repo_path, new_base, old_base).await })
483 }
484
485 fn rebase_continue(&self, repo_path: PathBuf) -> GitFuture<Result<RebaseStepResult, GitError>> {
486 Box::pin(async move { rebase_continue(repo_path).await })
487 }
488
489 fn abort_rebase(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>> {
490 Box::pin(async move { abort_rebase(repo_path).await })
491 }
492
493 fn is_rebase_in_progress(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>> {
494 Box::pin(async move { is_rebase_in_progress(repo_path).await })
495 }
496
497 fn in_progress_operation(
498 &self,
499 repo_path: PathBuf,
500 ) -> GitFuture<Result<Option<InProgressGitOperation>, GitError>> {
501 Box::pin(async move { in_progress_operation(repo_path).await })
502 }
503
504 fn has_unmerged_paths(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>> {
505 Box::pin(async move { has_unmerged_paths(repo_path).await })
506 }
507
508 fn list_staged_conflict_marker_files(
509 &self,
510 repo_path: PathBuf,
511 paths: Vec<String>,
512 ) -> GitFuture<Result<Vec<String>, GitError>> {
513 Box::pin(async move { list_staged_conflict_marker_files(repo_path, paths).await })
514 }
515
516 fn list_conflicted_files(
517 &self,
518 repo_path: PathBuf,
519 ) -> GitFuture<Result<Vec<String>, GitError>> {
520 Box::pin(async move { list_conflicted_files(repo_path).await })
521 }
522
523 fn commit_all(
524 &self,
525 repo_path: PathBuf,
526 message: String,
527 no_verify: bool,
528 ) -> GitFuture<Result<(), GitError>> {
529 Box::pin(async move { commit_all(repo_path, message, no_verify).await })
530 }
531
532 fn commit_all_preserving_single_commit(
533 &self,
534 repo_path: PathBuf,
535 base_branch: String,
536 commit_message: String,
537 message_strategy: SingleCommitMessageStrategy,
538 no_verify: bool,
539 ) -> GitFuture<Result<(), GitError>> {
540 Box::pin(async move {
541 commit_all_preserving_single_commit(
542 repo_path,
543 base_branch,
544 commit_message,
545 message_strategy,
546 no_verify,
547 )
548 .await
549 })
550 }
551
552 fn stage_all(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>> {
553 Box::pin(async move { stage_all(repo_path).await })
554 }
555
556 fn head_short_hash(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
557 Box::pin(async move { head_short_hash(repo_path).await })
558 }
559
560 fn head_hash(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
561 Box::pin(async move { head_hash(repo_path).await })
562 }
563
564 fn ref_hash(
565 &self,
566 repo_path: PathBuf,
567 reference: String,
568 ) -> GitFuture<Result<String, GitError>> {
569 Box::pin(async move { ref_hash(repo_path, reference).await })
570 }
571
572 fn head_commit_message(
573 &self,
574 repo_path: PathBuf,
575 ) -> GitFuture<Result<Option<String>, GitError>> {
576 Box::pin(async move { head_commit_message(repo_path).await })
577 }
578
579 fn delete_branch(
580 &self,
581 repo_path: PathBuf,
582 branch_name: String,
583 ) -> GitFuture<Result<(), GitError>> {
584 Box::pin(async move { delete_branch(repo_path, branch_name).await })
585 }
586
587 fn diff(&self, repo_path: PathBuf, base_branch: String) -> GitFuture<Result<String, GitError>> {
588 Box::pin(async move { diff(repo_path, base_branch).await })
589 }
590
591 fn is_worktree_clean(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>> {
592 Box::pin(async move { is_worktree_clean(repo_path).await })
593 }
594
595 fn worktree_status(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
596 Box::pin(async move { worktree_status(repo_path).await })
597 }
598
599 fn tracked_worktree_status(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
600 Box::pin(async move { tracked_worktree_status(repo_path).await })
601 }
602
603 fn has_commits_since(
604 &self,
605 repo_path: PathBuf,
606 base_branch: String,
607 ) -> GitFuture<Result<bool, GitError>> {
608 Box::pin(async move { has_commits_since(repo_path, base_branch).await })
609 }
610
611 fn pull_rebase(&self, repo_path: PathBuf) -> GitFuture<Result<PullRebaseResult, GitError>> {
612 Box::pin(async move { pull_rebase(repo_path).await })
613 }
614
615 fn push_current_branch(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
616 Box::pin(async move { push_current_branch(repo_path).await })
617 }
618
619 fn push_current_branch_to_remote_branch(
620 &self,
621 repo_path: PathBuf,
622 remote_branch_name: String,
623 ) -> GitFuture<Result<String, GitError>> {
624 Box::pin(async move {
625 push_current_branch_to_remote_branch(repo_path, remote_branch_name).await
626 })
627 }
628
629 fn remote_branch_exists(
630 &self,
631 repo_path: PathBuf,
632 remote_branch_name: String,
633 ) -> GitFuture<Result<bool, GitError>> {
634 Box::pin(async move { remote_branch_exists(repo_path, remote_branch_name).await })
635 }
636
637 fn current_upstream_reference(
638 &self,
639 repo_path: PathBuf,
640 ) -> GitFuture<Result<String, GitError>> {
641 Box::pin(async move { current_upstream_reference(repo_path).await })
642 }
643
644 fn fetch_remote(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>> {
645 Box::pin(async move { fetch_remote(repo_path).await })
646 }
647
648 fn get_ahead_behind(&self, repo_path: PathBuf) -> GitFuture<Result<(u32, u32), GitError>> {
649 Box::pin(async move { get_ahead_behind(repo_path).await })
650 }
651
652 fn get_ref_ahead_behind(
653 &self,
654 repo_path: PathBuf,
655 left_ref: String,
656 right_ref: String,
657 ) -> GitFuture<Result<(u32, u32), GitError>> {
658 Box::pin(async move { get_ref_ahead_behind(repo_path, left_ref, right_ref).await })
659 }
660
661 fn branch_tracking_statuses(
662 &self,
663 repo_path: PathBuf,
664 ) -> GitFuture<Result<BranchTrackingMap, GitError>> {
665 Box::pin(async move { branch_tracking_statuses(repo_path).await })
666 }
667
668 fn list_upstream_commit_titles(
669 &self,
670 repo_path: PathBuf,
671 ) -> GitFuture<Result<Vec<String>, GitError>> {
672 Box::pin(async move { list_upstream_commit_titles(repo_path).await })
673 }
674
675 fn list_local_commit_titles(
676 &self,
677 repo_path: PathBuf,
678 ) -> GitFuture<Result<Vec<String>, GitError>> {
679 Box::pin(async move { list_local_commit_titles(repo_path).await })
680 }
681
682 fn repo_url(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
683 Box::pin(async move { repo_url(repo_path).await })
684 }
685
686 fn main_repo_root(&self, repo_path: PathBuf) -> GitFuture<Result<PathBuf, GitError>> {
687 Box::pin(async move { main_repo_root(repo_path).await })
688 }
689
690 fn main_checkout_working_tree(
691 &self,
692 repo_path: PathBuf,
693 ) -> GitFuture<Result<Option<PathBuf>, GitError>> {
694 Box::pin(async move { main_checkout_working_tree(repo_path).await })
695 }
696}
697
698#[cfg(test)]
699mod tests {
700 use std::path::{Path, PathBuf};
701 use std::process::Command;
702 use std::time::Duration;
703 use std::{fs, thread};
704
705 use tempfile::tempdir;
706
707 use super::*;
708
709 fn canonicalize_test_path(path: &Path) -> PathBuf {
712 fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
713 }
714
715 fn run_git_command(repo_path: &Path, args: &[&str]) {
716 let output = Command::new("git")
717 .args(args)
718 .current_dir(repo_path)
719 .output()
720 .expect("failed to run git command");
721
722 assert!(
723 output.status.success(),
724 "git command {:?} failed: {}",
725 args,
726 String::from_utf8_lossy(&output.stderr)
727 );
728 }
729
730 fn run_git_command_stdout(repo_path: &Path, args: &[&str]) -> String {
731 let output = Command::new("git")
732 .args(args)
733 .current_dir(repo_path)
734 .output()
735 .expect("failed to run git command");
736
737 assert!(
738 output.status.success(),
739 "git command {:?} failed: {}",
740 args,
741 String::from_utf8_lossy(&output.stderr)
742 );
743
744 String::from_utf8_lossy(&output.stdout).trim().to_string()
745 }
746
747 fn setup_test_git_repo(repo_path: &Path) {
748 run_git_command(repo_path, &["init", "-b", "main"]);
749 run_git_command(repo_path, &["config", "user.name", "Test User"]);
750 run_git_command(repo_path, &["config", "user.email", "test@example.com"]);
751
752 fs::write(repo_path.join("README.md"), "test repo").expect("failed to write file");
753 run_git_command(repo_path, &["add", "README.md"]);
754 run_git_command(repo_path, &["commit", "-m", "Initial commit"]);
755 }
756
757 #[tokio::test]
758 async fn test_squash_merge_returns_committed_when_changes_exist() {
759 let dir = tempdir().expect("failed to create temp dir");
761 setup_test_git_repo(dir.path());
762 run_git_command(dir.path(), &["checkout", "-b", "feature-branch"]);
763 fs::write(dir.path().join("feature.txt"), "feature content").expect("failed to write file");
764 run_git_command(dir.path(), &["add", "feature.txt"]);
765 run_git_command(dir.path(), &["commit", "-m", "Add feature"]);
766 run_git_command(dir.path(), &["checkout", "main"]);
767
768 let result = squash_merge(
770 dir.path().to_path_buf(),
771 "feature-branch".to_string(),
772 "main".to_string(),
773 "Squash merge feature".to_string(),
774 )
775 .await;
776
777 assert_eq!(
779 result.expect("squash merge should succeed"),
780 SquashMergeOutcome::Committed,
781 );
782 }
783
784 #[tokio::test]
785 async fn test_squash_merge_returns_already_present_when_changes_exist_in_target() {
786 let dir = tempdir().expect("failed to create temp dir");
788 setup_test_git_repo(dir.path());
789 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
790 fs::write(dir.path().join("session.txt"), "session change").expect("failed to write file");
791 run_git_command(dir.path(), &["add", "session.txt"]);
792 run_git_command(dir.path(), &["commit", "-m", "Session change"]);
793 run_git_command(dir.path(), &["checkout", "main"]);
794 fs::write(dir.path().join("session.txt"), "session change").expect("failed to write file");
795 run_git_command(dir.path(), &["add", "session.txt"]);
796 run_git_command(dir.path(), &["commit", "-m", "Apply same change on main"]);
797
798 let result = squash_merge(
800 dir.path().to_path_buf(),
801 "session-branch".to_string(),
802 "main".to_string(),
803 "Merge session".to_string(),
804 )
805 .await;
806
807 assert_eq!(
809 result.expect("squash merge should succeed"),
810 SquashMergeOutcome::AlreadyPresentInTarget,
811 );
812 }
813
814 #[tokio::test]
815 async fn test_commit_all_preserving_single_commit_creates_first_commit() {
816 let dir = tempdir().expect("failed to create temp dir");
818 setup_test_git_repo(dir.path());
819 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
820 let commit_message = "Session commit".to_string();
821 fs::write(dir.path().join("work.txt"), "first change").expect("failed to write file");
822
823 let result = commit_all_preserving_single_commit(
825 dir.path().to_path_buf(),
826 "main".to_string(),
827 commit_message.clone(),
828 SingleCommitMessageStrategy::Replace,
829 false,
830 )
831 .await;
832 let commit_count = run_git_command_stdout(dir.path(), &["rev-list", "--count", "HEAD"]);
833 let head_message = run_git_command_stdout(dir.path(), &["log", "-1", "--pretty=%B"]);
834
835 assert!(
837 result.is_ok(),
838 "commit_all_preserving_single_commit should succeed: {result:?}"
839 );
840 assert_eq!(commit_count, "2");
841 assert_eq!(head_message, commit_message);
842 }
843
844 #[tokio::test]
845 async fn test_commit_all_preserving_single_commit_amends_existing_session_commit() {
846 let dir = tempdir().expect("failed to create temp dir");
848 setup_test_git_repo(dir.path());
849 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
850 let commit_message = "Session commit".to_string();
851 fs::write(dir.path().join("work.txt"), "first change").expect("failed to write file");
852 commit_all_preserving_single_commit(
853 dir.path().to_path_buf(),
854 "main".to_string(),
855 commit_message.clone(),
856 SingleCommitMessageStrategy::Replace,
857 false,
858 )
859 .await
860 .expect("failed to create first session commit");
861 let first_hash = run_git_command_stdout(dir.path(), &["rev-parse", "HEAD"]);
862 let first_count = run_git_command_stdout(dir.path(), &["rev-list", "--count", "HEAD"]);
863
864 fs::write(dir.path().join("work.txt"), "second change").expect("failed to write file");
866 let result = commit_all_preserving_single_commit(
867 dir.path().to_path_buf(),
868 "main".to_string(),
869 commit_message.clone(),
870 SingleCommitMessageStrategy::Replace,
871 false,
872 )
873 .await;
874 let second_hash = run_git_command_stdout(dir.path(), &["rev-parse", "HEAD"]);
875 let second_count = run_git_command_stdout(dir.path(), &["rev-list", "--count", "HEAD"]);
876
877 assert!(result.is_ok(), "amend commit should succeed: {result:?}");
879 assert_ne!(first_hash, second_hash);
880 assert_eq!(first_count, second_count);
881 }
882
883 #[tokio::test]
884 async fn test_commit_all_preserving_single_commit_replaces_amended_message() {
885 let dir = tempdir().expect("failed to create temp dir");
887 setup_test_git_repo(dir.path());
888 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
889 fs::write(dir.path().join("work.txt"), "first change").expect("failed to write file");
890 commit_all_preserving_single_commit(
891 dir.path().to_path_buf(),
892 "main".to_string(),
893 "First session message".to_string(),
894 SingleCommitMessageStrategy::Replace,
895 false,
896 )
897 .await
898 .expect("failed to create first session commit");
899
900 fs::write(dir.path().join("work.txt"), "second change").expect("failed to write file");
902 let result = commit_all_preserving_single_commit(
903 dir.path().to_path_buf(),
904 "main".to_string(),
905 "Refined session message".to_string(),
906 SingleCommitMessageStrategy::Replace,
907 false,
908 )
909 .await;
910 let head_message = run_git_command_stdout(dir.path(), &["log", "-1", "--pretty=%B"]);
911
912 assert!(
914 result.is_ok(),
915 "replace amended message should succeed: {result:?}"
916 );
917 assert_eq!(head_message, "Refined session message");
918 }
919
920 #[tokio::test]
921 async fn test_commit_all_preserving_single_commit_retries_index_lock_and_succeeds() {
922 let dir = tempdir().expect("failed to create temp dir");
924 setup_test_git_repo(dir.path());
925 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
926 let commit_message = "Session commit".to_string();
927 fs::write(dir.path().join("work.txt"), "locked change").expect("failed to write file");
928 let index_lock_path = dir.path().join(".git").join("index.lock");
929 fs::write(&index_lock_path, "stale lock").expect("failed to write lock file");
930 let lock_cleanup = thread::spawn(move || {
931 thread::sleep(Duration::from_millis(250));
932 let _ = fs::remove_file(index_lock_path);
933 });
934
935 let result = commit_all_preserving_single_commit(
937 dir.path().to_path_buf(),
938 "main".to_string(),
939 commit_message.clone(),
940 SingleCommitMessageStrategy::Replace,
941 false,
942 )
943 .await;
944 lock_cleanup
945 .join()
946 .expect("failed to join lock cleanup thread");
947 let head_message = run_git_command_stdout(dir.path(), &["log", "-1", "--pretty=%B"]);
948
949 assert!(
951 result.is_ok(),
952 "retry with index lock should succeed: {result:?}"
953 );
954 assert_eq!(head_message, commit_message);
955 }
956
957 #[tokio::test]
958 async fn test_diff_hides_leading_squash_merged_commit_for_non_rebased_session() {
959 let dir = tempdir().expect("failed to create temp dir");
961 setup_test_git_repo(dir.path());
962 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
963 fs::write(dir.path().join("merged.txt"), "already merged change")
964 .expect("failed to write merged file");
965 run_git_command(dir.path(), &["add", "merged.txt"]);
966 run_git_command(dir.path(), &["commit", "-m", "Session change"]);
967 run_git_command(dir.path(), &["checkout", "main"]);
968 run_git_command(dir.path(), &["merge", "--squash", "session-branch"]);
969 run_git_command(dir.path(), &["commit", "-m", "Squash merge session change"]);
970 run_git_command(dir.path(), &["checkout", "session-branch"]);
971
972 let diff_output = diff(dir.path().to_path_buf(), "main".to_string())
974 .await
975 .expect("failed to load diff");
976
977 assert!(
979 diff_output.trim().is_empty(),
980 "expected no diff, got: {diff_output}"
981 );
982 }
983
984 #[tokio::test]
985 async fn test_diff_keeps_new_commits_after_leading_squash_merged_commit() {
986 let dir = tempdir().expect("failed to create temp dir");
988 setup_test_git_repo(dir.path());
989 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
990 fs::write(dir.path().join("merged.txt"), "already merged change")
991 .expect("failed to write merged file");
992 run_git_command(dir.path(), &["add", "merged.txt"]);
993 run_git_command(dir.path(), &["commit", "-m", "Session change"]);
994 run_git_command(dir.path(), &["checkout", "main"]);
995 run_git_command(dir.path(), &["merge", "--squash", "session-branch"]);
996 run_git_command(dir.path(), &["commit", "-m", "Squash merge session change"]);
997 run_git_command(dir.path(), &["checkout", "session-branch"]);
998 fs::write(dir.path().join("new.txt"), "new session-only change")
999 .expect("failed to write new file");
1000 run_git_command(dir.path(), &["add", "new.txt"]);
1001 run_git_command(dir.path(), &["commit", "-m", "New session change"]);
1002
1003 let diff_output = diff(dir.path().to_path_buf(), "main".to_string())
1005 .await
1006 .expect("failed to load diff");
1007
1008 assert!(diff_output.contains("new.txt"));
1010 assert!(!diff_output.contains("merged.txt"));
1011 }
1012
1013 #[tokio::test]
1014 async fn test_diff_does_not_include_base_only_commits() {
1015 let dir = tempdir().expect("failed to create temp dir");
1017 setup_test_git_repo(dir.path());
1018 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
1019 fs::write(dir.path().join("session.txt"), "session change").expect("failed to write file");
1020 run_git_command(dir.path(), &["add", "session.txt"]);
1021 run_git_command(dir.path(), &["commit", "-m", "Session change"]);
1022 run_git_command(dir.path(), &["checkout", "main"]);
1023 fs::write(dir.path().join("main-only.txt"), "base branch only")
1024 .expect("failed to write base-only file");
1025 run_git_command(dir.path(), &["add", "main-only.txt"]);
1026 run_git_command(dir.path(), &["commit", "-m", "Main branch change"]);
1027 run_git_command(dir.path(), &["checkout", "session-branch"]);
1028
1029 let diff_output = diff(dir.path().to_path_buf(), "main".to_string())
1031 .await
1032 .expect("failed to load diff");
1033
1034 assert!(diff_output.contains("session.txt"));
1036 assert!(!diff_output.contains("main-only.txt"));
1037 }
1038
1039 #[tokio::test]
1040 async fn test_is_worktree_clean_returns_true_for_clean_repo() {
1041 let dir = tempdir().expect("failed to create temp dir");
1043 setup_test_git_repo(dir.path());
1044
1045 let is_clean = is_worktree_clean(dir.path().to_path_buf())
1047 .await
1048 .expect("failed to check worktree cleanliness");
1049
1050 assert!(is_clean);
1052 }
1053
1054 #[tokio::test]
1055 async fn test_is_worktree_clean_returns_false_for_dirty_repo() {
1056 let dir = tempdir().expect("failed to create temp dir");
1058 setup_test_git_repo(dir.path());
1059 fs::write(dir.path().join("README.md"), "dirty change").expect("failed to write change");
1060
1061 let is_clean = is_worktree_clean(dir.path().to_path_buf())
1063 .await
1064 .expect("failed to check worktree cleanliness");
1065
1066 assert!(!is_clean);
1068 }
1069
1070 #[tokio::test]
1071 async fn test_worktree_status_reports_dirty_repo_paths() {
1072 let dir = tempdir().expect("failed to create temp dir");
1074 setup_test_git_repo(dir.path());
1075 fs::write(dir.path().join("README.md"), "dirty change").expect("failed to write change");
1076 fs::write(dir.path().join("new-file.txt"), "new").expect("failed to write new file");
1077
1078 let status = worktree_status(dir.path().to_path_buf())
1080 .await
1081 .expect("failed to read worktree status");
1082
1083 assert!(status.contains("README.md"));
1085 assert!(status.contains("new-file.txt"));
1086 }
1087
1088 #[tokio::test]
1089 async fn test_tracked_worktree_status_ignores_untracked_repo_paths() {
1090 let dir = tempdir().expect("failed to create temp dir");
1092 setup_test_git_repo(dir.path());
1093 fs::write(dir.path().join("README.md"), "dirty change").expect("failed to write change");
1094 fs::write(dir.path().join("new-file.txt"), "new").expect("failed to write new file");
1095
1096 let status = tracked_worktree_status(dir.path().to_path_buf())
1098 .await
1099 .expect("failed to read tracked worktree status");
1100
1101 assert!(status.contains("README.md"));
1103 assert!(!status.contains("new-file.txt"));
1104 }
1105
1106 #[tokio::test]
1107 async fn test_main_repo_root_returns_repo_root_for_main_worktree() {
1108 let dir = tempdir().expect("failed to create temp dir");
1110 setup_test_git_repo(dir.path());
1111
1112 let repo_root = main_repo_root(dir.path().to_path_buf())
1114 .await
1115 .expect("failed to resolve main repo root");
1116
1117 assert_eq!(
1119 canonicalize_test_path(&repo_root),
1120 canonicalize_test_path(dir.path())
1121 );
1122 }
1123
1124 #[tokio::test]
1125 async fn test_main_repo_root_returns_shared_repo_root_for_linked_worktree() {
1126 let dir = tempdir().expect("failed to create temp dir");
1128 setup_test_git_repo(dir.path());
1129 let linked_worktree = dir.path().join("linked-worktree");
1130 create_worktree(
1131 dir.path().to_path_buf(),
1132 linked_worktree.clone(),
1133 "wt/main-repo-root-test".to_string(),
1134 "main".to_string(),
1135 )
1136 .await
1137 .expect("failed to create linked worktree");
1138
1139 let repo_root = main_repo_root(linked_worktree)
1141 .await
1142 .expect("failed to resolve shared repo root");
1143
1144 assert_eq!(
1146 canonicalize_test_path(&repo_root),
1147 canonicalize_test_path(dir.path())
1148 );
1149 }
1150
1151 #[tokio::test]
1152 async fn test_abort_rebase_cleans_stale_rebase_merge_metadata() {
1153 let dir = tempdir().expect("failed to create temp dir");
1155 setup_test_git_repo(dir.path());
1156 let stale_rebase_dir = dir.path().join(".git/rebase-merge");
1157 fs::create_dir_all(&stale_rebase_dir).expect("failed to create stale rebase metadata");
1158 fs::write(stale_rebase_dir.join("head-name"), "refs/heads/main")
1159 .expect("failed to write stale rebase metadata");
1160
1161 let result = abort_rebase(dir.path().to_path_buf()).await;
1163
1164 assert!(result.is_ok(), "abort_rebase should succeed: {result:?}");
1166 assert!(!stale_rebase_dir.exists());
1167 }
1168
1169 #[tokio::test]
1170 async fn test_abort_rebase_returns_error_without_rebase_state_or_stale_metadata() {
1171 let dir = tempdir().expect("failed to create temp dir");
1173 setup_test_git_repo(dir.path());
1174
1175 let result = abort_rebase(dir.path().to_path_buf()).await;
1177
1178 assert!(result.is_err());
1180 }
1181
1182 #[tokio::test]
1183 async fn test_ref_hash_resolves_branch_head() {
1184 let dir = tempdir().expect("failed to create temp dir");
1186 setup_test_git_repo(dir.path());
1187 let expected_hash = run_git_command_stdout(dir.path(), &["rev-parse", "main"]);
1188
1189 let resolved_hash = ref_hash(dir.path().to_path_buf(), "main".to_string())
1191 .await
1192 .expect("failed to resolve main hash");
1193
1194 assert_eq!(resolved_hash, expected_hash);
1196 }
1197
1198 #[tokio::test]
1199 async fn test_rebase_onto_start_replays_commits_after_old_base() {
1200 let dir = tempdir().expect("failed to create temp dir");
1202 setup_test_git_repo(dir.path());
1203 run_git_command(dir.path(), &["checkout", "-b", "parent"]);
1204 fs::write(dir.path().join("parent.txt"), "parent").expect("failed to write parent file");
1205 run_git_command(dir.path(), &["add", "parent.txt"]);
1206 run_git_command(dir.path(), &["commit", "-m", "Parent change"]);
1207 let parent_tip = run_git_command_stdout(dir.path(), &["rev-parse", "HEAD"]);
1208 run_git_command(dir.path(), &["checkout", "-b", "child"]);
1209 fs::write(dir.path().join("child.txt"), "child").expect("failed to write child file");
1210 run_git_command(dir.path(), &["add", "child.txt"]);
1211 run_git_command(dir.path(), &["commit", "-m", "Child change"]);
1212 run_git_command(dir.path(), &["checkout", "main"]);
1213 fs::write(dir.path().join("main.txt"), "main").expect("failed to write main file");
1214 run_git_command(dir.path(), &["add", "main.txt"]);
1215 run_git_command(dir.path(), &["commit", "-m", "Main change"]);
1216 run_git_command(dir.path(), &["checkout", "child"]);
1217
1218 let result = rebase_onto_start(dir.path().to_path_buf(), "main".to_string(), parent_tip)
1220 .await
1221 .expect("failed to start rebase --onto");
1222 let child_only_subjects = run_git_command_stdout(
1223 dir.path(),
1224 &["log", "--format=%s", "--reverse", "main..HEAD"],
1225 );
1226
1227 assert_eq!(result, RebaseStepResult::Completed);
1229 assert_eq!(child_only_subjects, "Child change");
1230 assert!(!dir.path().join("parent.txt").exists());
1231 assert!(dir.path().join("child.txt").exists());
1232 }
1233
1234 #[tokio::test]
1235 async fn test_pull_rebase_returns_error_without_upstream() {
1236 let dir = tempdir().expect("failed to create temp dir");
1238 setup_test_git_repo(dir.path());
1239
1240 let result = pull_rebase(dir.path().to_path_buf()).await;
1242
1243 assert!(result.is_err());
1245 }
1246
1247 #[tokio::test]
1248 async fn test_pull_rebase_targets_single_upstream_when_merge_targets_are_ambiguous() {
1249 let dir = tempdir().expect("failed to create temp dir");
1251 let remote_dir = tempdir().expect("failed to create remote temp dir");
1252 setup_test_git_repo(dir.path());
1253 run_git_command(remote_dir.path(), &["init", "--bare"]);
1254
1255 let remote_path = remote_dir.path().to_string_lossy().to_string();
1256 run_git_command(dir.path(), &["remote", "add", "origin", &remote_path]);
1257 run_git_command(dir.path(), &["push", "-u", "origin", "main"]);
1258
1259 run_git_command(dir.path(), &["checkout", "-b", "feature"]);
1260 fs::write(dir.path().join("feature.txt"), "feature change").expect("failed to write file");
1261 run_git_command(dir.path(), &["add", "feature.txt"]);
1262 run_git_command(dir.path(), &["commit", "-m", "Add feature branch"]);
1263 run_git_command(dir.path(), &["push", "-u", "origin", "feature"]);
1264 run_git_command(dir.path(), &["checkout", "main"]);
1265
1266 run_git_command(
1267 dir.path(),
1268 &["config", "--add", "branch.main.merge", "refs/heads/feature"],
1269 );
1270
1271 let pull_without_explicit_target = Command::new("git")
1272 .args(["pull", "--rebase"])
1273 .current_dir(dir.path())
1274 .output()
1275 .expect("failed to run pull --rebase");
1276
1277 assert!(
1278 !pull_without_explicit_target.status.success(),
1279 "expected plain pull --rebase to fail in ambiguous merge-target setup"
1280 );
1281 assert!(
1282 String::from_utf8_lossy(&pull_without_explicit_target.stderr)
1283 .contains("Cannot rebase onto multiple branches"),
1284 "expected ambiguous merge-target failure"
1285 );
1286
1287 let result = pull_rebase(dir.path().to_path_buf()).await;
1289
1290 assert!(
1292 matches!(result, Ok(PullRebaseResult::Completed)),
1293 "pull_rebase should complete: {result:?}"
1294 );
1295 }
1296
1297 #[tokio::test]
1298 async fn test_pull_rebase_targets_local_upstream_when_upstream_name_has_no_remote_prefix() {
1299 let dir = tempdir().expect("failed to create temp dir");
1301 setup_test_git_repo(dir.path());
1302
1303 run_git_command(dir.path(), &["checkout", "-b", "feature"]);
1304 fs::write(dir.path().join("feature.txt"), "feature change").expect("failed to write file");
1305 run_git_command(dir.path(), &["add", "feature.txt"]);
1306 run_git_command(dir.path(), &["commit", "-m", "Add feature branch"]);
1307 run_git_command(dir.path(), &["checkout", "main"]);
1308
1309 run_git_command(dir.path(), &["config", "branch.main.remote", "."]);
1310 run_git_command(
1311 dir.path(),
1312 &[
1313 "config",
1314 "--replace-all",
1315 "branch.main.merge",
1316 "refs/heads/main",
1317 ],
1318 );
1319 run_git_command(
1320 dir.path(),
1321 &["config", "--add", "branch.main.merge", "refs/heads/feature"],
1322 );
1323
1324 let pull_without_explicit_target = Command::new("git")
1325 .args(["pull", "--rebase"])
1326 .current_dir(dir.path())
1327 .output()
1328 .expect("failed to run pull --rebase");
1329
1330 assert!(
1331 !pull_without_explicit_target.status.success(),
1332 "expected plain pull --rebase to fail in ambiguous merge-target setup"
1333 );
1334 assert!(
1335 String::from_utf8_lossy(&pull_without_explicit_target.stderr)
1336 .contains("Cannot rebase onto multiple branches"),
1337 "expected ambiguous merge-target failure"
1338 );
1339
1340 let result = pull_rebase(dir.path().to_path_buf()).await;
1342
1343 assert!(
1345 matches!(result, Ok(PullRebaseResult::Completed)),
1346 "pull_rebase with local upstream should complete: {result:?}"
1347 );
1348 }
1349
1350 #[tokio::test]
1351 async fn test_list_upstream_commit_titles_returns_error_without_upstream() {
1352 let dir = tempdir().expect("failed to create temp dir");
1354 setup_test_git_repo(dir.path());
1355
1356 let result = list_upstream_commit_titles(dir.path().to_path_buf()).await;
1358
1359 assert!(result.is_err());
1361 }
1362
1363 #[tokio::test]
1364 async fn test_list_upstream_commit_titles_returns_new_upstream_commit_titles() {
1365 let dir = tempdir().expect("failed to create temp dir");
1367 let remote_dir = tempdir().expect("failed to create remote temp dir");
1368 let contributor_dir = tempdir().expect("failed to create contributor temp dir");
1369 let contributor_clone_path = contributor_dir.path().join("clone");
1370 setup_test_git_repo(dir.path());
1371 run_git_command(remote_dir.path(), &["init", "--bare"]);
1372
1373 let remote_path = remote_dir.path().to_string_lossy().to_string();
1374 let contributor_clone_path_text = contributor_clone_path.to_string_lossy().to_string();
1375 run_git_command(dir.path(), &["remote", "add", "origin", &remote_path]);
1376 run_git_command(dir.path(), &["push", "-u", "origin", "main"]);
1377
1378 run_git_command(
1379 contributor_dir.path(),
1380 &["clone", &remote_path, &contributor_clone_path_text],
1381 );
1382 run_git_command(
1383 &contributor_clone_path,
1384 &["config", "user.name", "Contributor User"],
1385 );
1386 run_git_command(
1387 &contributor_clone_path,
1388 &["config", "user.email", "contributor@example.com"],
1389 );
1390 run_git_command(
1391 &contributor_clone_path,
1392 &["checkout", "-B", "main", "origin/main"],
1393 );
1394 fs::write(contributor_clone_path.join("remote.txt"), "remote change")
1395 .expect("failed to write remote change");
1396 run_git_command(&contributor_clone_path, &["add", "remote.txt"]);
1397 run_git_command(
1398 &contributor_clone_path,
1399 &["commit", "-m", "Remote commit title"],
1400 );
1401 run_git_command(&contributor_clone_path, &["push", "origin", "main"]);
1402 run_git_command(dir.path(), &["fetch", "origin"]);
1403
1404 let titles = list_upstream_commit_titles(dir.path().to_path_buf())
1406 .await
1407 .expect("failed to list upstream commit titles");
1408
1409 assert_eq!(titles, vec!["Remote commit title".to_string()]);
1411 }
1412
1413 #[tokio::test]
1414 async fn test_list_local_commit_titles_returns_error_without_upstream() {
1415 let dir = tempdir().expect("failed to create temp dir");
1417 setup_test_git_repo(dir.path());
1418
1419 let result = list_local_commit_titles(dir.path().to_path_buf()).await;
1421
1422 assert!(result.is_err());
1424 }
1425
1426 #[tokio::test]
1427 async fn test_list_local_commit_titles_returns_new_local_commit_titles() {
1428 let dir = tempdir().expect("failed to create temp dir");
1430 let remote_dir = tempdir().expect("failed to create remote temp dir");
1431 setup_test_git_repo(dir.path());
1432 run_git_command(remote_dir.path(), &["init", "--bare"]);
1433
1434 let remote_path = remote_dir.path().to_string_lossy().to_string();
1435 run_git_command(dir.path(), &["remote", "add", "origin", &remote_path]);
1436 run_git_command(dir.path(), &["push", "-u", "origin", "main"]);
1437
1438 fs::write(dir.path().join("local_1.txt"), "local change 1")
1439 .expect("failed to write local change 1");
1440 run_git_command(dir.path(), &["add", "local_1.txt"]);
1441 run_git_command(dir.path(), &["commit", "-m", "Local commit title one"]);
1442
1443 fs::write(dir.path().join("local_2.txt"), "local change 2")
1444 .expect("failed to write local change 2");
1445 run_git_command(dir.path(), &["add", "local_2.txt"]);
1446 run_git_command(dir.path(), &["commit", "-m", "Local commit title two"]);
1447
1448 let titles = list_local_commit_titles(dir.path().to_path_buf())
1450 .await
1451 .expect("failed to list local commit titles");
1452
1453 assert_eq!(
1455 titles,
1456 vec![
1457 "Local commit title one".to_string(),
1458 "Local commit title two".to_string(),
1459 ]
1460 );
1461 }
1462
1463 #[tokio::test]
1464 async fn test_push_current_branch_returns_error_without_remote() {
1465 let dir = tempdir().expect("failed to create temp dir");
1467 setup_test_git_repo(dir.path());
1468
1469 let result = push_current_branch(dir.path().to_path_buf()).await;
1471
1472 assert!(result.is_err());
1474 }
1475
1476 #[tokio::test]
1477 async fn test_push_current_branch_returns_upstream_reference() {
1478 let dir = tempdir().expect("failed to create temp dir");
1480 let remote_dir = tempdir().expect("failed to create remote temp dir");
1481 setup_test_git_repo(dir.path());
1482 run_git_command(remote_dir.path(), &["init", "--bare"]);
1483 let remote_path = remote_dir.path().to_string_lossy().to_string();
1484 run_git_command(dir.path(), &["remote", "add", "origin", &remote_path]);
1485
1486 let upstream_reference = push_current_branch(dir.path().to_path_buf())
1488 .await
1489 .expect("push should set upstream");
1490
1491 assert_eq!(upstream_reference, "origin/main");
1493 }
1494
1495 #[tokio::test]
1496 async fn test_push_current_branch_to_remote_branch_returns_upstream_reference() {
1497 let dir = tempdir().expect("failed to create temp dir");
1499 let remote_dir = tempdir().expect("failed to create remote temp dir");
1500 setup_test_git_repo(dir.path());
1501 run_git_command(remote_dir.path(), &["init", "--bare"]);
1502 let remote_path = remote_dir.path().to_string_lossy().to_string();
1503 run_git_command(dir.path(), &["remote", "add", "origin", &remote_path]);
1504
1505 let upstream_reference = push_current_branch_to_remote_branch(
1507 dir.path().to_path_buf(),
1508 "review/custom-branch".to_string(),
1509 )
1510 .await
1511 .expect("push should set a custom upstream");
1512
1513 assert_eq!(upstream_reference, "origin/review/custom-branch");
1515 }
1516
1517 #[test]
1518 fn test_is_no_upstream_error_detects_upstream_hint() {
1519 let detail = "fatal: The current branch main has no upstream branch.";
1521
1522 let is_no_upstream = sync::is_no_upstream_error(detail);
1524
1525 assert!(is_no_upstream);
1527 }
1528
1529 #[test]
1530 fn test_is_rebase_conflict_detects_conflict_keyword() {
1531 let detail = "CONFLICT (content): Merge conflict in src/main.rs";
1533
1534 assert!(rebase::is_rebase_conflict(detail));
1536 }
1537
1538 #[test]
1539 fn test_is_rebase_conflict_detects_could_not_apply() {
1540 let detail = "error: could not apply abc1234... Update handler";
1542
1543 assert!(rebase::is_rebase_conflict(detail));
1545 }
1546
1547 #[test]
1548 fn test_is_rebase_conflict_detects_mark_as_resolved() {
1549 let detail = "hint: mark them as resolved using git add";
1551
1552 assert!(rebase::is_rebase_conflict(detail));
1554 }
1555
1556 #[test]
1557 fn test_is_rebase_conflict_detects_unresolved_conflict() {
1558 let detail = "fatal: Exiting because of an unresolved conflict.";
1560
1561 assert!(rebase::is_rebase_conflict(detail));
1563 }
1564
1565 #[test]
1566 fn test_is_rebase_conflict_detects_committing_not_possible() {
1567 let detail = "error: Committing is not possible because you have unmerged files.";
1569
1570 assert!(rebase::is_rebase_conflict(detail));
1572 }
1573
1574 #[test]
1575 fn test_is_rebase_conflict_returns_false_for_unrelated_error() {
1576 let detail = "fatal: not a git repository (or any parent up to mount point /)";
1578
1579 assert!(!rebase::is_rebase_conflict(detail));
1581 }
1582
1583 #[test]
1584 fn test_is_rebase_conflict_returns_false_for_index_lock_error() {
1585 let detail = "fatal: Unable to create '.git/index.lock': File exists.";
1587
1588 assert!(!rebase::is_rebase_conflict(detail));
1590 }
1591}