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