1use std::future::Future;
4use std::path::PathBuf;
5use std::pin::Pin;
6
7use super::error::GitError;
8use super::merge::SquashMergeOutcome;
9use super::rebase::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, is_rebase_in_progress,
18 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 has_unmerged_paths(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>>;
142
143 fn list_staged_conflict_marker_files(
149 &self,
150 repo_path: PathBuf,
151 paths: Vec<String>,
152 ) -> GitFuture<Result<Vec<String>, GitError>>;
153
154 fn list_conflicted_files(&self, repo_path: PathBuf)
159 -> GitFuture<Result<Vec<String>, GitError>>;
160
161 fn commit_all(
168 &self,
169 repo_path: PathBuf,
170 message: String,
171 no_verify: bool,
172 ) -> GitFuture<Result<(), GitError>>;
173
174 fn commit_all_preserving_single_commit(
183 &self,
184 repo_path: PathBuf,
185 base_branch: String,
186 commit_message: String,
187 message_strategy: SingleCommitMessageStrategy,
188 no_verify: bool,
189 ) -> GitFuture<Result<(), GitError>>;
190
191 fn stage_all(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>>;
196
197 fn head_short_hash(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
202
203 fn head_hash(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
208
209 fn ref_hash(
214 &self,
215 repo_path: PathBuf,
216 reference: String,
217 ) -> GitFuture<Result<String, GitError>>;
218
219 fn head_commit_message(
225 &self,
226 repo_path: PathBuf,
227 ) -> GitFuture<Result<Option<String>, GitError>>;
228
229 fn delete_branch(
235 &self,
236 repo_path: PathBuf,
237 branch_name: String,
238 ) -> GitFuture<Result<(), GitError>>;
239
240 fn diff(&self, repo_path: PathBuf, base_branch: String) -> GitFuture<Result<String, GitError>>;
246
247 fn is_worktree_clean(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>>;
252
253 fn worktree_status(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
258
259 fn tracked_worktree_status(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
264
265 fn has_commits_since(
271 &self,
272 repo_path: PathBuf,
273 base_branch: String,
274 ) -> GitFuture<Result<bool, GitError>>;
275
276 fn pull_rebase(&self, repo_path: PathBuf) -> GitFuture<Result<PullRebaseResult, GitError>>;
281
282 fn push_current_branch(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
289
290 fn push_current_branch_to_remote_branch(
297 &self,
298 repo_path: PathBuf,
299 remote_branch_name: String,
300 ) -> GitFuture<Result<String, GitError>>;
301
302 fn remote_branch_exists(
308 &self,
309 repo_path: PathBuf,
310 remote_branch_name: String,
311 ) -> GitFuture<Result<bool, GitError>>;
312
313 fn current_upstream_reference(&self, repo_path: PathBuf)
318 -> GitFuture<Result<String, GitError>>;
319
320 fn fetch_remote(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>>;
325
326 fn get_ahead_behind(&self, repo_path: PathBuf) -> GitFuture<Result<(u32, u32), GitError>>;
331
332 fn get_ref_ahead_behind(
340 &self,
341 repo_path: PathBuf,
342 left_ref: String,
343 right_ref: String,
344 ) -> GitFuture<Result<(u32, u32), GitError>>;
345
346 fn branch_tracking_statuses(
355 &self,
356 repo_path: PathBuf,
357 ) -> GitFuture<Result<BranchTrackingMap, GitError>>;
358
359 fn list_upstream_commit_titles(
366 &self,
367 repo_path: PathBuf,
368 ) -> GitFuture<Result<Vec<String>, GitError>>;
369
370 fn list_local_commit_titles(
376 &self,
377 repo_path: PathBuf,
378 ) -> GitFuture<Result<Vec<String>, GitError>>;
379
380 fn repo_url(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
385
386 fn main_repo_root(&self, repo_path: PathBuf) -> GitFuture<Result<PathBuf, GitError>>;
391}
392
393pub struct RealGitClient;
395
396impl GitClient for RealGitClient {
397 fn detect_git_info(&self, dir: PathBuf) -> GitFuture<Option<String>> {
398 Box::pin(async move { detect_git_info(dir).await })
399 }
400
401 fn find_git_repo_root(&self, dir: PathBuf) -> GitFuture<Option<PathBuf>> {
402 Box::pin(async move { find_git_repo_root(dir).await })
403 }
404
405 fn create_worktree(
406 &self,
407 repo_path: PathBuf,
408 worktree_path: PathBuf,
409 branch_name: String,
410 start_ref: String,
411 ) -> GitFuture<Result<(), GitError>> {
412 Box::pin(
413 async move { create_worktree(repo_path, worktree_path, branch_name, start_ref).await },
414 )
415 }
416
417 fn remove_worktree(&self, worktree_path: PathBuf) -> GitFuture<Result<(), GitError>> {
418 Box::pin(async move { remove_worktree(worktree_path).await })
419 }
420
421 fn squash_merge_diff(
422 &self,
423 repo_path: PathBuf,
424 source_branch: String,
425 target_branch: String,
426 ) -> GitFuture<Result<String, GitError>> {
427 Box::pin(async move { squash_merge_diff(repo_path, source_branch, target_branch).await })
428 }
429
430 fn squash_merge(
431 &self,
432 repo_path: PathBuf,
433 source_branch: String,
434 target_branch: String,
435 commit_message: String,
436 ) -> GitFuture<Result<SquashMergeOutcome, GitError>> {
437 Box::pin(async move {
438 squash_merge(repo_path, source_branch, target_branch, commit_message).await
439 })
440 }
441
442 fn rebase(&self, repo_path: PathBuf, target_branch: String) -> GitFuture<Result<(), GitError>> {
443 Box::pin(async move { rebase::rebase(repo_path, target_branch).await })
444 }
445
446 fn rebase_start(
447 &self,
448 repo_path: PathBuf,
449 target_branch: String,
450 ) -> GitFuture<Result<RebaseStepResult, GitError>> {
451 Box::pin(async move { rebase_start(repo_path, target_branch).await })
452 }
453
454 fn rebase_onto_start(
455 &self,
456 repo_path: PathBuf,
457 new_base: String,
458 old_base: String,
459 ) -> GitFuture<Result<RebaseStepResult, GitError>> {
460 Box::pin(async move { rebase_onto_start(repo_path, new_base, old_base).await })
461 }
462
463 fn rebase_continue(&self, repo_path: PathBuf) -> GitFuture<Result<RebaseStepResult, GitError>> {
464 Box::pin(async move { rebase_continue(repo_path).await })
465 }
466
467 fn abort_rebase(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>> {
468 Box::pin(async move { abort_rebase(repo_path).await })
469 }
470
471 fn is_rebase_in_progress(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>> {
472 Box::pin(async move { is_rebase_in_progress(repo_path).await })
473 }
474
475 fn has_unmerged_paths(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>> {
476 Box::pin(async move { has_unmerged_paths(repo_path).await })
477 }
478
479 fn list_staged_conflict_marker_files(
480 &self,
481 repo_path: PathBuf,
482 paths: Vec<String>,
483 ) -> GitFuture<Result<Vec<String>, GitError>> {
484 Box::pin(async move { list_staged_conflict_marker_files(repo_path, paths).await })
485 }
486
487 fn list_conflicted_files(
488 &self,
489 repo_path: PathBuf,
490 ) -> GitFuture<Result<Vec<String>, GitError>> {
491 Box::pin(async move { list_conflicted_files(repo_path).await })
492 }
493
494 fn commit_all(
495 &self,
496 repo_path: PathBuf,
497 message: String,
498 no_verify: bool,
499 ) -> GitFuture<Result<(), GitError>> {
500 Box::pin(async move { commit_all(repo_path, message, no_verify).await })
501 }
502
503 fn commit_all_preserving_single_commit(
504 &self,
505 repo_path: PathBuf,
506 base_branch: String,
507 commit_message: String,
508 message_strategy: SingleCommitMessageStrategy,
509 no_verify: bool,
510 ) -> GitFuture<Result<(), GitError>> {
511 Box::pin(async move {
512 commit_all_preserving_single_commit(
513 repo_path,
514 base_branch,
515 commit_message,
516 message_strategy,
517 no_verify,
518 )
519 .await
520 })
521 }
522
523 fn stage_all(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>> {
524 Box::pin(async move { stage_all(repo_path).await })
525 }
526
527 fn head_short_hash(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
528 Box::pin(async move { head_short_hash(repo_path).await })
529 }
530
531 fn head_hash(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
532 Box::pin(async move { head_hash(repo_path).await })
533 }
534
535 fn ref_hash(
536 &self,
537 repo_path: PathBuf,
538 reference: String,
539 ) -> GitFuture<Result<String, GitError>> {
540 Box::pin(async move { ref_hash(repo_path, reference).await })
541 }
542
543 fn head_commit_message(
544 &self,
545 repo_path: PathBuf,
546 ) -> GitFuture<Result<Option<String>, GitError>> {
547 Box::pin(async move { head_commit_message(repo_path).await })
548 }
549
550 fn delete_branch(
551 &self,
552 repo_path: PathBuf,
553 branch_name: String,
554 ) -> GitFuture<Result<(), GitError>> {
555 Box::pin(async move { delete_branch(repo_path, branch_name).await })
556 }
557
558 fn diff(&self, repo_path: PathBuf, base_branch: String) -> GitFuture<Result<String, GitError>> {
559 Box::pin(async move { diff(repo_path, base_branch).await })
560 }
561
562 fn is_worktree_clean(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>> {
563 Box::pin(async move { is_worktree_clean(repo_path).await })
564 }
565
566 fn worktree_status(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
567 Box::pin(async move { worktree_status(repo_path).await })
568 }
569
570 fn tracked_worktree_status(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
571 Box::pin(async move { tracked_worktree_status(repo_path).await })
572 }
573
574 fn has_commits_since(
575 &self,
576 repo_path: PathBuf,
577 base_branch: String,
578 ) -> GitFuture<Result<bool, GitError>> {
579 Box::pin(async move { has_commits_since(repo_path, base_branch).await })
580 }
581
582 fn pull_rebase(&self, repo_path: PathBuf) -> GitFuture<Result<PullRebaseResult, GitError>> {
583 Box::pin(async move { pull_rebase(repo_path).await })
584 }
585
586 fn push_current_branch(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
587 Box::pin(async move { push_current_branch(repo_path).await })
588 }
589
590 fn push_current_branch_to_remote_branch(
591 &self,
592 repo_path: PathBuf,
593 remote_branch_name: String,
594 ) -> GitFuture<Result<String, GitError>> {
595 Box::pin(async move {
596 push_current_branch_to_remote_branch(repo_path, remote_branch_name).await
597 })
598 }
599
600 fn remote_branch_exists(
601 &self,
602 repo_path: PathBuf,
603 remote_branch_name: String,
604 ) -> GitFuture<Result<bool, GitError>> {
605 Box::pin(async move { remote_branch_exists(repo_path, remote_branch_name).await })
606 }
607
608 fn current_upstream_reference(
609 &self,
610 repo_path: PathBuf,
611 ) -> GitFuture<Result<String, GitError>> {
612 Box::pin(async move { current_upstream_reference(repo_path).await })
613 }
614
615 fn fetch_remote(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>> {
616 Box::pin(async move { fetch_remote(repo_path).await })
617 }
618
619 fn get_ahead_behind(&self, repo_path: PathBuf) -> GitFuture<Result<(u32, u32), GitError>> {
620 Box::pin(async move { get_ahead_behind(repo_path).await })
621 }
622
623 fn get_ref_ahead_behind(
624 &self,
625 repo_path: PathBuf,
626 left_ref: String,
627 right_ref: String,
628 ) -> GitFuture<Result<(u32, u32), GitError>> {
629 Box::pin(async move { get_ref_ahead_behind(repo_path, left_ref, right_ref).await })
630 }
631
632 fn branch_tracking_statuses(
633 &self,
634 repo_path: PathBuf,
635 ) -> GitFuture<Result<BranchTrackingMap, GitError>> {
636 Box::pin(async move { branch_tracking_statuses(repo_path).await })
637 }
638
639 fn list_upstream_commit_titles(
640 &self,
641 repo_path: PathBuf,
642 ) -> GitFuture<Result<Vec<String>, GitError>> {
643 Box::pin(async move { list_upstream_commit_titles(repo_path).await })
644 }
645
646 fn list_local_commit_titles(
647 &self,
648 repo_path: PathBuf,
649 ) -> GitFuture<Result<Vec<String>, GitError>> {
650 Box::pin(async move { list_local_commit_titles(repo_path).await })
651 }
652
653 fn repo_url(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
654 Box::pin(async move { repo_url(repo_path).await })
655 }
656
657 fn main_repo_root(&self, repo_path: PathBuf) -> GitFuture<Result<PathBuf, GitError>> {
658 Box::pin(async move { main_repo_root(repo_path).await })
659 }
660}
661
662#[cfg(test)]
663mod tests {
664 use std::path::{Path, PathBuf};
665 use std::process::Command;
666 use std::time::Duration;
667 use std::{fs, thread};
668
669 use tempfile::tempdir;
670
671 use super::*;
672
673 fn canonicalize_test_path(path: &Path) -> PathBuf {
676 fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
677 }
678
679 fn run_git_command(repo_path: &Path, args: &[&str]) {
680 let output = Command::new("git")
681 .args(args)
682 .current_dir(repo_path)
683 .output()
684 .expect("failed to run git command");
685
686 assert!(
687 output.status.success(),
688 "git command {:?} failed: {}",
689 args,
690 String::from_utf8_lossy(&output.stderr)
691 );
692 }
693
694 fn run_git_command_stdout(repo_path: &Path, args: &[&str]) -> String {
695 let output = Command::new("git")
696 .args(args)
697 .current_dir(repo_path)
698 .output()
699 .expect("failed to run git command");
700
701 assert!(
702 output.status.success(),
703 "git command {:?} failed: {}",
704 args,
705 String::from_utf8_lossy(&output.stderr)
706 );
707
708 String::from_utf8_lossy(&output.stdout).trim().to_string()
709 }
710
711 fn setup_test_git_repo(repo_path: &Path) {
712 run_git_command(repo_path, &["init", "-b", "main"]);
713 run_git_command(repo_path, &["config", "user.name", "Test User"]);
714 run_git_command(repo_path, &["config", "user.email", "test@example.com"]);
715
716 fs::write(repo_path.join("README.md"), "test repo").expect("failed to write file");
717 run_git_command(repo_path, &["add", "README.md"]);
718 run_git_command(repo_path, &["commit", "-m", "Initial commit"]);
719 }
720
721 #[tokio::test]
722 async fn test_squash_merge_returns_committed_when_changes_exist() {
723 let dir = tempdir().expect("failed to create temp dir");
725 setup_test_git_repo(dir.path());
726 run_git_command(dir.path(), &["checkout", "-b", "feature-branch"]);
727 fs::write(dir.path().join("feature.txt"), "feature content").expect("failed to write file");
728 run_git_command(dir.path(), &["add", "feature.txt"]);
729 run_git_command(dir.path(), &["commit", "-m", "Add feature"]);
730 run_git_command(dir.path(), &["checkout", "main"]);
731
732 let result = squash_merge(
734 dir.path().to_path_buf(),
735 "feature-branch".to_string(),
736 "main".to_string(),
737 "Squash merge feature".to_string(),
738 )
739 .await;
740
741 assert_eq!(
743 result.expect("squash merge should succeed"),
744 SquashMergeOutcome::Committed,
745 );
746 }
747
748 #[tokio::test]
749 async fn test_squash_merge_returns_already_present_when_changes_exist_in_target() {
750 let dir = tempdir().expect("failed to create temp dir");
752 setup_test_git_repo(dir.path());
753 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
754 fs::write(dir.path().join("session.txt"), "session change").expect("failed to write file");
755 run_git_command(dir.path(), &["add", "session.txt"]);
756 run_git_command(dir.path(), &["commit", "-m", "Session change"]);
757 run_git_command(dir.path(), &["checkout", "main"]);
758 fs::write(dir.path().join("session.txt"), "session change").expect("failed to write file");
759 run_git_command(dir.path(), &["add", "session.txt"]);
760 run_git_command(dir.path(), &["commit", "-m", "Apply same change on main"]);
761
762 let result = squash_merge(
764 dir.path().to_path_buf(),
765 "session-branch".to_string(),
766 "main".to_string(),
767 "Merge session".to_string(),
768 )
769 .await;
770
771 assert_eq!(
773 result.expect("squash merge should succeed"),
774 SquashMergeOutcome::AlreadyPresentInTarget,
775 );
776 }
777
778 #[tokio::test]
779 async fn test_commit_all_preserving_single_commit_creates_first_commit() {
780 let dir = tempdir().expect("failed to create temp dir");
782 setup_test_git_repo(dir.path());
783 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
784 let commit_message = "Session commit".to_string();
785 fs::write(dir.path().join("work.txt"), "first change").expect("failed to write file");
786
787 let result = commit_all_preserving_single_commit(
789 dir.path().to_path_buf(),
790 "main".to_string(),
791 commit_message.clone(),
792 SingleCommitMessageStrategy::Replace,
793 false,
794 )
795 .await;
796 let commit_count = run_git_command_stdout(dir.path(), &["rev-list", "--count", "HEAD"]);
797 let head_message = run_git_command_stdout(dir.path(), &["log", "-1", "--pretty=%B"]);
798
799 assert!(
801 result.is_ok(),
802 "commit_all_preserving_single_commit should succeed: {result:?}"
803 );
804 assert_eq!(commit_count, "2");
805 assert_eq!(head_message, commit_message);
806 }
807
808 #[tokio::test]
809 async fn test_commit_all_preserving_single_commit_amends_existing_session_commit() {
810 let dir = tempdir().expect("failed to create temp dir");
812 setup_test_git_repo(dir.path());
813 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
814 let commit_message = "Session commit".to_string();
815 fs::write(dir.path().join("work.txt"), "first change").expect("failed to write file");
816 commit_all_preserving_single_commit(
817 dir.path().to_path_buf(),
818 "main".to_string(),
819 commit_message.clone(),
820 SingleCommitMessageStrategy::Replace,
821 false,
822 )
823 .await
824 .expect("failed to create first session commit");
825 let first_hash = run_git_command_stdout(dir.path(), &["rev-parse", "HEAD"]);
826 let first_count = run_git_command_stdout(dir.path(), &["rev-list", "--count", "HEAD"]);
827
828 fs::write(dir.path().join("work.txt"), "second change").expect("failed to write file");
830 let result = commit_all_preserving_single_commit(
831 dir.path().to_path_buf(),
832 "main".to_string(),
833 commit_message.clone(),
834 SingleCommitMessageStrategy::Replace,
835 false,
836 )
837 .await;
838 let second_hash = run_git_command_stdout(dir.path(), &["rev-parse", "HEAD"]);
839 let second_count = run_git_command_stdout(dir.path(), &["rev-list", "--count", "HEAD"]);
840
841 assert!(result.is_ok(), "amend commit should succeed: {result:?}");
843 assert_ne!(first_hash, second_hash);
844 assert_eq!(first_count, second_count);
845 }
846
847 #[tokio::test]
848 async fn test_commit_all_preserving_single_commit_replaces_amended_message() {
849 let dir = tempdir().expect("failed to create temp dir");
851 setup_test_git_repo(dir.path());
852 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
853 fs::write(dir.path().join("work.txt"), "first change").expect("failed to write file");
854 commit_all_preserving_single_commit(
855 dir.path().to_path_buf(),
856 "main".to_string(),
857 "First session message".to_string(),
858 SingleCommitMessageStrategy::Replace,
859 false,
860 )
861 .await
862 .expect("failed to create first session commit");
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 "Refined session message".to_string(),
870 SingleCommitMessageStrategy::Replace,
871 false,
872 )
873 .await;
874 let head_message = run_git_command_stdout(dir.path(), &["log", "-1", "--pretty=%B"]);
875
876 assert!(
878 result.is_ok(),
879 "replace amended message should succeed: {result:?}"
880 );
881 assert_eq!(head_message, "Refined session message");
882 }
883
884 #[tokio::test]
885 async fn test_commit_all_preserving_single_commit_retries_index_lock_and_succeeds() {
886 let dir = tempdir().expect("failed to create temp dir");
888 setup_test_git_repo(dir.path());
889 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
890 let commit_message = "Session commit".to_string();
891 fs::write(dir.path().join("work.txt"), "locked change").expect("failed to write file");
892 let index_lock_path = dir.path().join(".git").join("index.lock");
893 fs::write(&index_lock_path, "stale lock").expect("failed to write lock file");
894 let lock_cleanup = thread::spawn(move || {
895 thread::sleep(Duration::from_millis(250));
896 let _ = fs::remove_file(index_lock_path);
897 });
898
899 let result = commit_all_preserving_single_commit(
901 dir.path().to_path_buf(),
902 "main".to_string(),
903 commit_message.clone(),
904 SingleCommitMessageStrategy::Replace,
905 false,
906 )
907 .await;
908 lock_cleanup
909 .join()
910 .expect("failed to join lock cleanup thread");
911 let head_message = run_git_command_stdout(dir.path(), &["log", "-1", "--pretty=%B"]);
912
913 assert!(
915 result.is_ok(),
916 "retry with index lock should succeed: {result:?}"
917 );
918 assert_eq!(head_message, commit_message);
919 }
920
921 #[tokio::test]
922 async fn test_diff_hides_leading_squash_merged_commit_for_non_rebased_session() {
923 let dir = tempdir().expect("failed to create temp dir");
925 setup_test_git_repo(dir.path());
926 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
927 fs::write(dir.path().join("merged.txt"), "already merged change")
928 .expect("failed to write merged file");
929 run_git_command(dir.path(), &["add", "merged.txt"]);
930 run_git_command(dir.path(), &["commit", "-m", "Session change"]);
931 run_git_command(dir.path(), &["checkout", "main"]);
932 run_git_command(dir.path(), &["merge", "--squash", "session-branch"]);
933 run_git_command(dir.path(), &["commit", "-m", "Squash merge session change"]);
934 run_git_command(dir.path(), &["checkout", "session-branch"]);
935
936 let diff_output = diff(dir.path().to_path_buf(), "main".to_string())
938 .await
939 .expect("failed to load diff");
940
941 assert!(
943 diff_output.trim().is_empty(),
944 "expected no diff, got: {diff_output}"
945 );
946 }
947
948 #[tokio::test]
949 async fn test_diff_keeps_new_commits_after_leading_squash_merged_commit() {
950 let dir = tempdir().expect("failed to create temp dir");
952 setup_test_git_repo(dir.path());
953 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
954 fs::write(dir.path().join("merged.txt"), "already merged change")
955 .expect("failed to write merged file");
956 run_git_command(dir.path(), &["add", "merged.txt"]);
957 run_git_command(dir.path(), &["commit", "-m", "Session change"]);
958 run_git_command(dir.path(), &["checkout", "main"]);
959 run_git_command(dir.path(), &["merge", "--squash", "session-branch"]);
960 run_git_command(dir.path(), &["commit", "-m", "Squash merge session change"]);
961 run_git_command(dir.path(), &["checkout", "session-branch"]);
962 fs::write(dir.path().join("new.txt"), "new session-only change")
963 .expect("failed to write new file");
964 run_git_command(dir.path(), &["add", "new.txt"]);
965 run_git_command(dir.path(), &["commit", "-m", "New session change"]);
966
967 let diff_output = diff(dir.path().to_path_buf(), "main".to_string())
969 .await
970 .expect("failed to load diff");
971
972 assert!(diff_output.contains("new.txt"));
974 assert!(!diff_output.contains("merged.txt"));
975 }
976
977 #[tokio::test]
978 async fn test_diff_does_not_include_base_only_commits() {
979 let dir = tempdir().expect("failed to create temp dir");
981 setup_test_git_repo(dir.path());
982 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
983 fs::write(dir.path().join("session.txt"), "session change").expect("failed to write file");
984 run_git_command(dir.path(), &["add", "session.txt"]);
985 run_git_command(dir.path(), &["commit", "-m", "Session change"]);
986 run_git_command(dir.path(), &["checkout", "main"]);
987 fs::write(dir.path().join("main-only.txt"), "base branch only")
988 .expect("failed to write base-only file");
989 run_git_command(dir.path(), &["add", "main-only.txt"]);
990 run_git_command(dir.path(), &["commit", "-m", "Main branch change"]);
991 run_git_command(dir.path(), &["checkout", "session-branch"]);
992
993 let diff_output = diff(dir.path().to_path_buf(), "main".to_string())
995 .await
996 .expect("failed to load diff");
997
998 assert!(diff_output.contains("session.txt"));
1000 assert!(!diff_output.contains("main-only.txt"));
1001 }
1002
1003 #[tokio::test]
1004 async fn test_is_worktree_clean_returns_true_for_clean_repo() {
1005 let dir = tempdir().expect("failed to create temp dir");
1007 setup_test_git_repo(dir.path());
1008
1009 let is_clean = is_worktree_clean(dir.path().to_path_buf())
1011 .await
1012 .expect("failed to check worktree cleanliness");
1013
1014 assert!(is_clean);
1016 }
1017
1018 #[tokio::test]
1019 async fn test_is_worktree_clean_returns_false_for_dirty_repo() {
1020 let dir = tempdir().expect("failed to create temp dir");
1022 setup_test_git_repo(dir.path());
1023 fs::write(dir.path().join("README.md"), "dirty change").expect("failed to write change");
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_worktree_status_reports_dirty_repo_paths() {
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 fs::write(dir.path().join("new-file.txt"), "new").expect("failed to write new file");
1041
1042 let status = worktree_status(dir.path().to_path_buf())
1044 .await
1045 .expect("failed to read worktree status");
1046
1047 assert!(status.contains("README.md"));
1049 assert!(status.contains("new-file.txt"));
1050 }
1051
1052 #[tokio::test]
1053 async fn test_tracked_worktree_status_ignores_untracked_repo_paths() {
1054 let dir = tempdir().expect("failed to create temp dir");
1056 setup_test_git_repo(dir.path());
1057 fs::write(dir.path().join("README.md"), "dirty change").expect("failed to write change");
1058 fs::write(dir.path().join("new-file.txt"), "new").expect("failed to write new file");
1059
1060 let status = tracked_worktree_status(dir.path().to_path_buf())
1062 .await
1063 .expect("failed to read tracked worktree status");
1064
1065 assert!(status.contains("README.md"));
1067 assert!(!status.contains("new-file.txt"));
1068 }
1069
1070 #[tokio::test]
1071 async fn test_main_repo_root_returns_repo_root_for_main_worktree() {
1072 let dir = tempdir().expect("failed to create temp dir");
1074 setup_test_git_repo(dir.path());
1075
1076 let repo_root = main_repo_root(dir.path().to_path_buf())
1078 .await
1079 .expect("failed to resolve main repo root");
1080
1081 assert_eq!(
1083 canonicalize_test_path(&repo_root),
1084 canonicalize_test_path(dir.path())
1085 );
1086 }
1087
1088 #[tokio::test]
1089 async fn test_main_repo_root_returns_shared_repo_root_for_linked_worktree() {
1090 let dir = tempdir().expect("failed to create temp dir");
1092 setup_test_git_repo(dir.path());
1093 let linked_worktree = dir.path().join("linked-worktree");
1094 create_worktree(
1095 dir.path().to_path_buf(),
1096 linked_worktree.clone(),
1097 "wt/main-repo-root-test".to_string(),
1098 "main".to_string(),
1099 )
1100 .await
1101 .expect("failed to create linked worktree");
1102
1103 let repo_root = main_repo_root(linked_worktree)
1105 .await
1106 .expect("failed to resolve shared repo root");
1107
1108 assert_eq!(
1110 canonicalize_test_path(&repo_root),
1111 canonicalize_test_path(dir.path())
1112 );
1113 }
1114
1115 #[tokio::test]
1116 async fn test_abort_rebase_cleans_stale_rebase_merge_metadata() {
1117 let dir = tempdir().expect("failed to create temp dir");
1119 setup_test_git_repo(dir.path());
1120 let stale_rebase_dir = dir.path().join(".git/rebase-merge");
1121 fs::create_dir_all(&stale_rebase_dir).expect("failed to create stale rebase metadata");
1122 fs::write(stale_rebase_dir.join("head-name"), "refs/heads/main")
1123 .expect("failed to write stale rebase metadata");
1124
1125 let result = abort_rebase(dir.path().to_path_buf()).await;
1127
1128 assert!(result.is_ok(), "abort_rebase should succeed: {result:?}");
1130 assert!(!stale_rebase_dir.exists());
1131 }
1132
1133 #[tokio::test]
1134 async fn test_abort_rebase_returns_error_without_rebase_state_or_stale_metadata() {
1135 let dir = tempdir().expect("failed to create temp dir");
1137 setup_test_git_repo(dir.path());
1138
1139 let result = abort_rebase(dir.path().to_path_buf()).await;
1141
1142 assert!(result.is_err());
1144 }
1145
1146 #[tokio::test]
1147 async fn test_ref_hash_resolves_branch_head() {
1148 let dir = tempdir().expect("failed to create temp dir");
1150 setup_test_git_repo(dir.path());
1151 let expected_hash = run_git_command_stdout(dir.path(), &["rev-parse", "main"]);
1152
1153 let resolved_hash = ref_hash(dir.path().to_path_buf(), "main".to_string())
1155 .await
1156 .expect("failed to resolve main hash");
1157
1158 assert_eq!(resolved_hash, expected_hash);
1160 }
1161
1162 #[tokio::test]
1163 async fn test_rebase_onto_start_replays_commits_after_old_base() {
1164 let dir = tempdir().expect("failed to create temp dir");
1166 setup_test_git_repo(dir.path());
1167 run_git_command(dir.path(), &["checkout", "-b", "parent"]);
1168 fs::write(dir.path().join("parent.txt"), "parent").expect("failed to write parent file");
1169 run_git_command(dir.path(), &["add", "parent.txt"]);
1170 run_git_command(dir.path(), &["commit", "-m", "Parent change"]);
1171 let parent_tip = run_git_command_stdout(dir.path(), &["rev-parse", "HEAD"]);
1172 run_git_command(dir.path(), &["checkout", "-b", "child"]);
1173 fs::write(dir.path().join("child.txt"), "child").expect("failed to write child file");
1174 run_git_command(dir.path(), &["add", "child.txt"]);
1175 run_git_command(dir.path(), &["commit", "-m", "Child change"]);
1176 run_git_command(dir.path(), &["checkout", "main"]);
1177 fs::write(dir.path().join("main.txt"), "main").expect("failed to write main file");
1178 run_git_command(dir.path(), &["add", "main.txt"]);
1179 run_git_command(dir.path(), &["commit", "-m", "Main change"]);
1180 run_git_command(dir.path(), &["checkout", "child"]);
1181
1182 let result = rebase_onto_start(dir.path().to_path_buf(), "main".to_string(), parent_tip)
1184 .await
1185 .expect("failed to start rebase --onto");
1186 let child_only_subjects = run_git_command_stdout(
1187 dir.path(),
1188 &["log", "--format=%s", "--reverse", "main..HEAD"],
1189 );
1190
1191 assert_eq!(result, RebaseStepResult::Completed);
1193 assert_eq!(child_only_subjects, "Child change");
1194 assert!(!dir.path().join("parent.txt").exists());
1195 assert!(dir.path().join("child.txt").exists());
1196 }
1197
1198 #[tokio::test]
1199 async fn test_pull_rebase_returns_error_without_upstream() {
1200 let dir = tempdir().expect("failed to create temp dir");
1202 setup_test_git_repo(dir.path());
1203
1204 let result = pull_rebase(dir.path().to_path_buf()).await;
1206
1207 assert!(result.is_err());
1209 }
1210
1211 #[tokio::test]
1212 async fn test_pull_rebase_targets_single_upstream_when_merge_targets_are_ambiguous() {
1213 let dir = tempdir().expect("failed to create temp dir");
1215 let remote_dir = tempdir().expect("failed to create remote temp dir");
1216 setup_test_git_repo(dir.path());
1217 run_git_command(remote_dir.path(), &["init", "--bare"]);
1218
1219 let remote_path = remote_dir.path().to_string_lossy().to_string();
1220 run_git_command(dir.path(), &["remote", "add", "origin", &remote_path]);
1221 run_git_command(dir.path(), &["push", "-u", "origin", "main"]);
1222
1223 run_git_command(dir.path(), &["checkout", "-b", "feature"]);
1224 fs::write(dir.path().join("feature.txt"), "feature change").expect("failed to write file");
1225 run_git_command(dir.path(), &["add", "feature.txt"]);
1226 run_git_command(dir.path(), &["commit", "-m", "Add feature branch"]);
1227 run_git_command(dir.path(), &["push", "-u", "origin", "feature"]);
1228 run_git_command(dir.path(), &["checkout", "main"]);
1229
1230 run_git_command(
1231 dir.path(),
1232 &["config", "--add", "branch.main.merge", "refs/heads/feature"],
1233 );
1234
1235 let pull_without_explicit_target = Command::new("git")
1236 .args(["pull", "--rebase"])
1237 .current_dir(dir.path())
1238 .output()
1239 .expect("failed to run pull --rebase");
1240
1241 assert!(
1242 !pull_without_explicit_target.status.success(),
1243 "expected plain pull --rebase to fail in ambiguous merge-target setup"
1244 );
1245 assert!(
1246 String::from_utf8_lossy(&pull_without_explicit_target.stderr)
1247 .contains("Cannot rebase onto multiple branches"),
1248 "expected ambiguous merge-target failure"
1249 );
1250
1251 let result = pull_rebase(dir.path().to_path_buf()).await;
1253
1254 assert!(
1256 matches!(result, Ok(PullRebaseResult::Completed)),
1257 "pull_rebase should complete: {result:?}"
1258 );
1259 }
1260
1261 #[tokio::test]
1262 async fn test_pull_rebase_targets_local_upstream_when_upstream_name_has_no_remote_prefix() {
1263 let dir = tempdir().expect("failed to create temp dir");
1265 setup_test_git_repo(dir.path());
1266
1267 run_git_command(dir.path(), &["checkout", "-b", "feature"]);
1268 fs::write(dir.path().join("feature.txt"), "feature change").expect("failed to write file");
1269 run_git_command(dir.path(), &["add", "feature.txt"]);
1270 run_git_command(dir.path(), &["commit", "-m", "Add feature branch"]);
1271 run_git_command(dir.path(), &["checkout", "main"]);
1272
1273 run_git_command(dir.path(), &["config", "branch.main.remote", "."]);
1274 run_git_command(
1275 dir.path(),
1276 &[
1277 "config",
1278 "--replace-all",
1279 "branch.main.merge",
1280 "refs/heads/main",
1281 ],
1282 );
1283 run_git_command(
1284 dir.path(),
1285 &["config", "--add", "branch.main.merge", "refs/heads/feature"],
1286 );
1287
1288 let pull_without_explicit_target = Command::new("git")
1289 .args(["pull", "--rebase"])
1290 .current_dir(dir.path())
1291 .output()
1292 .expect("failed to run pull --rebase");
1293
1294 assert!(
1295 !pull_without_explicit_target.status.success(),
1296 "expected plain pull --rebase to fail in ambiguous merge-target setup"
1297 );
1298 assert!(
1299 String::from_utf8_lossy(&pull_without_explicit_target.stderr)
1300 .contains("Cannot rebase onto multiple branches"),
1301 "expected ambiguous merge-target failure"
1302 );
1303
1304 let result = pull_rebase(dir.path().to_path_buf()).await;
1306
1307 assert!(
1309 matches!(result, Ok(PullRebaseResult::Completed)),
1310 "pull_rebase with local upstream should complete: {result:?}"
1311 );
1312 }
1313
1314 #[tokio::test]
1315 async fn test_list_upstream_commit_titles_returns_error_without_upstream() {
1316 let dir = tempdir().expect("failed to create temp dir");
1318 setup_test_git_repo(dir.path());
1319
1320 let result = list_upstream_commit_titles(dir.path().to_path_buf()).await;
1322
1323 assert!(result.is_err());
1325 }
1326
1327 #[tokio::test]
1328 async fn test_list_upstream_commit_titles_returns_new_upstream_commit_titles() {
1329 let dir = tempdir().expect("failed to create temp dir");
1331 let remote_dir = tempdir().expect("failed to create remote temp dir");
1332 let contributor_dir = tempdir().expect("failed to create contributor temp dir");
1333 let contributor_clone_path = contributor_dir.path().join("clone");
1334 setup_test_git_repo(dir.path());
1335 run_git_command(remote_dir.path(), &["init", "--bare"]);
1336
1337 let remote_path = remote_dir.path().to_string_lossy().to_string();
1338 let contributor_clone_path_text = contributor_clone_path.to_string_lossy().to_string();
1339 run_git_command(dir.path(), &["remote", "add", "origin", &remote_path]);
1340 run_git_command(dir.path(), &["push", "-u", "origin", "main"]);
1341
1342 run_git_command(
1343 contributor_dir.path(),
1344 &["clone", &remote_path, &contributor_clone_path_text],
1345 );
1346 run_git_command(
1347 &contributor_clone_path,
1348 &["config", "user.name", "Contributor User"],
1349 );
1350 run_git_command(
1351 &contributor_clone_path,
1352 &["config", "user.email", "contributor@example.com"],
1353 );
1354 run_git_command(
1355 &contributor_clone_path,
1356 &["checkout", "-B", "main", "origin/main"],
1357 );
1358 fs::write(contributor_clone_path.join("remote.txt"), "remote change")
1359 .expect("failed to write remote change");
1360 run_git_command(&contributor_clone_path, &["add", "remote.txt"]);
1361 run_git_command(
1362 &contributor_clone_path,
1363 &["commit", "-m", "Remote commit title"],
1364 );
1365 run_git_command(&contributor_clone_path, &["push", "origin", "main"]);
1366 run_git_command(dir.path(), &["fetch", "origin"]);
1367
1368 let titles = list_upstream_commit_titles(dir.path().to_path_buf())
1370 .await
1371 .expect("failed to list upstream commit titles");
1372
1373 assert_eq!(titles, vec!["Remote commit title".to_string()]);
1375 }
1376
1377 #[tokio::test]
1378 async fn test_list_local_commit_titles_returns_error_without_upstream() {
1379 let dir = tempdir().expect("failed to create temp dir");
1381 setup_test_git_repo(dir.path());
1382
1383 let result = list_local_commit_titles(dir.path().to_path_buf()).await;
1385
1386 assert!(result.is_err());
1388 }
1389
1390 #[tokio::test]
1391 async fn test_list_local_commit_titles_returns_new_local_commit_titles() {
1392 let dir = tempdir().expect("failed to create temp dir");
1394 let remote_dir = tempdir().expect("failed to create remote temp dir");
1395 setup_test_git_repo(dir.path());
1396 run_git_command(remote_dir.path(), &["init", "--bare"]);
1397
1398 let remote_path = remote_dir.path().to_string_lossy().to_string();
1399 run_git_command(dir.path(), &["remote", "add", "origin", &remote_path]);
1400 run_git_command(dir.path(), &["push", "-u", "origin", "main"]);
1401
1402 fs::write(dir.path().join("local_1.txt"), "local change 1")
1403 .expect("failed to write local change 1");
1404 run_git_command(dir.path(), &["add", "local_1.txt"]);
1405 run_git_command(dir.path(), &["commit", "-m", "Local commit title one"]);
1406
1407 fs::write(dir.path().join("local_2.txt"), "local change 2")
1408 .expect("failed to write local change 2");
1409 run_git_command(dir.path(), &["add", "local_2.txt"]);
1410 run_git_command(dir.path(), &["commit", "-m", "Local commit title two"]);
1411
1412 let titles = list_local_commit_titles(dir.path().to_path_buf())
1414 .await
1415 .expect("failed to list local commit titles");
1416
1417 assert_eq!(
1419 titles,
1420 vec![
1421 "Local commit title one".to_string(),
1422 "Local commit title two".to_string(),
1423 ]
1424 );
1425 }
1426
1427 #[tokio::test]
1428 async fn test_push_current_branch_returns_error_without_remote() {
1429 let dir = tempdir().expect("failed to create temp dir");
1431 setup_test_git_repo(dir.path());
1432
1433 let result = push_current_branch(dir.path().to_path_buf()).await;
1435
1436 assert!(result.is_err());
1438 }
1439
1440 #[tokio::test]
1441 async fn test_push_current_branch_returns_upstream_reference() {
1442 let dir = tempdir().expect("failed to create temp dir");
1444 let remote_dir = tempdir().expect("failed to create remote temp dir");
1445 setup_test_git_repo(dir.path());
1446 run_git_command(remote_dir.path(), &["init", "--bare"]);
1447 let remote_path = remote_dir.path().to_string_lossy().to_string();
1448 run_git_command(dir.path(), &["remote", "add", "origin", &remote_path]);
1449
1450 let upstream_reference = push_current_branch(dir.path().to_path_buf())
1452 .await
1453 .expect("push should set upstream");
1454
1455 assert_eq!(upstream_reference, "origin/main");
1457 }
1458
1459 #[tokio::test]
1460 async fn test_push_current_branch_to_remote_branch_returns_upstream_reference() {
1461 let dir = tempdir().expect("failed to create temp dir");
1463 let remote_dir = tempdir().expect("failed to create remote temp dir");
1464 setup_test_git_repo(dir.path());
1465 run_git_command(remote_dir.path(), &["init", "--bare"]);
1466 let remote_path = remote_dir.path().to_string_lossy().to_string();
1467 run_git_command(dir.path(), &["remote", "add", "origin", &remote_path]);
1468
1469 let upstream_reference = push_current_branch_to_remote_branch(
1471 dir.path().to_path_buf(),
1472 "review/custom-branch".to_string(),
1473 )
1474 .await
1475 .expect("push should set a custom upstream");
1476
1477 assert_eq!(upstream_reference, "origin/review/custom-branch");
1479 }
1480
1481 #[test]
1482 fn test_is_no_upstream_error_detects_upstream_hint() {
1483 let detail = "fatal: The current branch main has no upstream branch.";
1485
1486 let is_no_upstream = sync::is_no_upstream_error(detail);
1488
1489 assert!(is_no_upstream);
1491 }
1492
1493 #[test]
1494 fn test_is_rebase_conflict_detects_conflict_keyword() {
1495 let detail = "CONFLICT (content): Merge conflict in src/main.rs";
1497
1498 assert!(rebase::is_rebase_conflict(detail));
1500 }
1501
1502 #[test]
1503 fn test_is_rebase_conflict_detects_could_not_apply() {
1504 let detail = "error: could not apply abc1234... Update handler";
1506
1507 assert!(rebase::is_rebase_conflict(detail));
1509 }
1510
1511 #[test]
1512 fn test_is_rebase_conflict_detects_mark_as_resolved() {
1513 let detail = "hint: mark them as resolved using git add";
1515
1516 assert!(rebase::is_rebase_conflict(detail));
1518 }
1519
1520 #[test]
1521 fn test_is_rebase_conflict_detects_unresolved_conflict() {
1522 let detail = "fatal: Exiting because of an unresolved conflict.";
1524
1525 assert!(rebase::is_rebase_conflict(detail));
1527 }
1528
1529 #[test]
1530 fn test_is_rebase_conflict_detects_committing_not_possible() {
1531 let detail = "error: Committing is not possible because you have unmerged files.";
1533
1534 assert!(rebase::is_rebase_conflict(detail));
1536 }
1537
1538 #[test]
1539 fn test_is_rebase_conflict_returns_false_for_unrelated_error() {
1540 let detail = "fatal: not a git repository (or any parent up to mount point /)";
1542
1543 assert!(!rebase::is_rebase_conflict(detail));
1545 }
1546
1547 #[test]
1548 fn test_is_rebase_conflict_returns_false_for_index_lock_error() {
1549 let detail = "fatal: Unable to create '.git/index.lock': File exists.";
1551
1552 assert!(!rebase::is_rebase_conflict(detail));
1554 }
1555}