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, check_pre_commit_hook_ready, commit_all,
15 commit_all_preserving_single_commit, create_worktree, current_upstream_reference,
16 delete_branch, detect_git_info, diff, fetch_remote, find_git_repo_root, get_ahead_behind,
17 get_ref_ahead_behind, has_commits_since, has_unmerged_paths, head_commit_message, head_hash,
18 head_short_hash, in_progress_operation, is_rebase_in_progress, is_worktree_clean,
19 list_conflicted_files, list_local_commit_titles, list_staged_conflict_marker_files,
20 list_upstream_commit_titles, main_checkout_working_tree, main_repo_root, pull_rebase,
21 push_current_branch, push_current_branch_to_remote_branch, rebase, rebase_continue,
22 rebase_onto_start, rebase_start, ref_hash, remote_branch_exists, remove_worktree, repo_url,
23 squash_merge, squash_merge_diff, stage_all, tracked_worktree_status, 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 check_pre_commit_hook_ready(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>>;
52
53 fn create_worktree(
60 &self,
61 repo_path: PathBuf,
62 worktree_path: PathBuf,
63 branch_name: String,
64 start_ref: String,
65 ) -> GitFuture<Result<(), GitError>>;
66
67 fn remove_worktree(&self, worktree_path: PathBuf) -> GitFuture<Result<(), GitError>>;
73
74 fn squash_merge_diff(
80 &self,
81 repo_path: PathBuf,
82 source_branch: String,
83 target_branch: String,
84 ) -> GitFuture<Result<String, GitError>>;
85
86 fn squash_merge(
92 &self,
93 repo_path: PathBuf,
94 source_branch: String,
95 target_branch: String,
96 commit_message: String,
97 ) -> GitFuture<Result<SquashMergeOutcome, GitError>>;
98
99 fn rebase(&self, repo_path: PathBuf, target_branch: String) -> GitFuture<Result<(), GitError>>;
104
105 fn rebase_start(
111 &self,
112 repo_path: PathBuf,
113 target_branch: String,
114 ) -> GitFuture<Result<RebaseStepResult, GitError>>;
115
116 fn rebase_onto_start(
121 &self,
122 repo_path: PathBuf,
123 new_base: String,
124 old_base: String,
125 ) -> GitFuture<Result<RebaseStepResult, GitError>>;
126
127 fn rebase_continue(&self, repo_path: PathBuf) -> GitFuture<Result<RebaseStepResult, GitError>>;
132
133 fn abort_rebase(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>>;
138
139 fn is_rebase_in_progress(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>>;
144
145 fn in_progress_operation(
150 &self,
151 repo_path: PathBuf,
152 ) -> GitFuture<Result<Option<InProgressGitOperation>, GitError>>;
153
154 fn has_unmerged_paths(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>>;
159
160 fn list_staged_conflict_marker_files(
166 &self,
167 repo_path: PathBuf,
168 paths: Vec<String>,
169 ) -> GitFuture<Result<Vec<String>, GitError>>;
170
171 fn list_conflicted_files(&self, repo_path: PathBuf)
176 -> GitFuture<Result<Vec<String>, GitError>>;
177
178 fn commit_all(
185 &self,
186 repo_path: PathBuf,
187 message: String,
188 no_verify: bool,
189 ) -> GitFuture<Result<(), GitError>>;
190
191 fn commit_all_preserving_single_commit(
200 &self,
201 repo_path: PathBuf,
202 base_branch: String,
203 commit_message: String,
204 message_strategy: SingleCommitMessageStrategy,
205 no_verify: bool,
206 ) -> GitFuture<Result<(), GitError>>;
207
208 fn stage_all(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>>;
213
214 fn head_short_hash(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
219
220 fn head_hash(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
225
226 fn ref_hash(
231 &self,
232 repo_path: PathBuf,
233 reference: String,
234 ) -> GitFuture<Result<String, GitError>>;
235
236 fn head_commit_message(
242 &self,
243 repo_path: PathBuf,
244 ) -> GitFuture<Result<Option<String>, GitError>>;
245
246 fn delete_branch(
252 &self,
253 repo_path: PathBuf,
254 branch_name: String,
255 ) -> GitFuture<Result<(), GitError>>;
256
257 fn diff(&self, repo_path: PathBuf, base_branch: String) -> GitFuture<Result<String, GitError>>;
263
264 fn is_worktree_clean(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>>;
269
270 fn worktree_status(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
275
276 fn tracked_worktree_status(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
281
282 fn has_commits_since(
288 &self,
289 repo_path: PathBuf,
290 base_branch: String,
291 ) -> GitFuture<Result<bool, GitError>>;
292
293 fn pull_rebase(&self, repo_path: PathBuf) -> GitFuture<Result<PullRebaseResult, GitError>>;
298
299 fn push_current_branch(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
306
307 fn push_current_branch_to_remote_branch(
314 &self,
315 repo_path: PathBuf,
316 remote_branch_name: String,
317 ) -> GitFuture<Result<String, GitError>>;
318
319 fn remote_branch_exists(
325 &self,
326 repo_path: PathBuf,
327 remote_branch_name: String,
328 ) -> GitFuture<Result<bool, GitError>>;
329
330 fn current_upstream_reference(&self, repo_path: PathBuf)
335 -> GitFuture<Result<String, GitError>>;
336
337 fn fetch_remote(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>>;
342
343 fn get_ahead_behind(&self, repo_path: PathBuf) -> GitFuture<Result<(u32, u32), GitError>>;
348
349 fn get_ref_ahead_behind(
357 &self,
358 repo_path: PathBuf,
359 left_ref: String,
360 right_ref: String,
361 ) -> GitFuture<Result<(u32, u32), GitError>>;
362
363 fn branch_tracking_statuses(
372 &self,
373 repo_path: PathBuf,
374 ) -> GitFuture<Result<BranchTrackingMap, GitError>>;
375
376 fn list_upstream_commit_titles(
383 &self,
384 repo_path: PathBuf,
385 ) -> GitFuture<Result<Vec<String>, GitError>>;
386
387 fn list_local_commit_titles(
393 &self,
394 repo_path: PathBuf,
395 ) -> GitFuture<Result<Vec<String>, GitError>>;
396
397 fn repo_url(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
402
403 fn main_repo_root(&self, repo_path: PathBuf) -> GitFuture<Result<PathBuf, GitError>>;
408
409 fn main_checkout_working_tree(
417 &self,
418 repo_path: PathBuf,
419 ) -> GitFuture<Result<Option<PathBuf>, GitError>>;
420}
421
422pub struct RealGitClient;
424
425impl GitClient for RealGitClient {
426 fn detect_git_info(&self, dir: PathBuf) -> GitFuture<Option<String>> {
427 Box::pin(async move { detect_git_info(dir).await })
428 }
429
430 fn find_git_repo_root(&self, dir: PathBuf) -> GitFuture<Option<PathBuf>> {
431 Box::pin(async move { find_git_repo_root(dir).await })
432 }
433
434 fn check_pre_commit_hook_ready(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>> {
435 Box::pin(async move { check_pre_commit_hook_ready(repo_path).await })
436 }
437
438 fn create_worktree(
439 &self,
440 repo_path: PathBuf,
441 worktree_path: PathBuf,
442 branch_name: String,
443 start_ref: String,
444 ) -> GitFuture<Result<(), GitError>> {
445 Box::pin(
446 async move { create_worktree(repo_path, worktree_path, branch_name, start_ref).await },
447 )
448 }
449
450 fn remove_worktree(&self, worktree_path: PathBuf) -> GitFuture<Result<(), GitError>> {
451 Box::pin(async move { remove_worktree(worktree_path).await })
452 }
453
454 fn squash_merge_diff(
455 &self,
456 repo_path: PathBuf,
457 source_branch: String,
458 target_branch: String,
459 ) -> GitFuture<Result<String, GitError>> {
460 Box::pin(async move { squash_merge_diff(repo_path, source_branch, target_branch).await })
461 }
462
463 fn squash_merge(
464 &self,
465 repo_path: PathBuf,
466 source_branch: String,
467 target_branch: String,
468 commit_message: String,
469 ) -> GitFuture<Result<SquashMergeOutcome, GitError>> {
470 Box::pin(async move {
471 squash_merge(repo_path, source_branch, target_branch, commit_message).await
472 })
473 }
474
475 fn rebase(&self, repo_path: PathBuf, target_branch: String) -> GitFuture<Result<(), GitError>> {
476 Box::pin(async move { rebase::rebase(repo_path, target_branch).await })
477 }
478
479 fn rebase_start(
480 &self,
481 repo_path: PathBuf,
482 target_branch: String,
483 ) -> GitFuture<Result<RebaseStepResult, GitError>> {
484 Box::pin(async move { rebase_start(repo_path, target_branch).await })
485 }
486
487 fn rebase_onto_start(
488 &self,
489 repo_path: PathBuf,
490 new_base: String,
491 old_base: String,
492 ) -> GitFuture<Result<RebaseStepResult, GitError>> {
493 Box::pin(async move { rebase_onto_start(repo_path, new_base, old_base).await })
494 }
495
496 fn rebase_continue(&self, repo_path: PathBuf) -> GitFuture<Result<RebaseStepResult, GitError>> {
497 Box::pin(async move { rebase_continue(repo_path).await })
498 }
499
500 fn abort_rebase(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>> {
501 Box::pin(async move { abort_rebase(repo_path).await })
502 }
503
504 fn is_rebase_in_progress(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>> {
505 Box::pin(async move { is_rebase_in_progress(repo_path).await })
506 }
507
508 fn in_progress_operation(
509 &self,
510 repo_path: PathBuf,
511 ) -> GitFuture<Result<Option<InProgressGitOperation>, GitError>> {
512 Box::pin(async move { in_progress_operation(repo_path).await })
513 }
514
515 fn has_unmerged_paths(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>> {
516 Box::pin(async move { has_unmerged_paths(repo_path).await })
517 }
518
519 fn list_staged_conflict_marker_files(
520 &self,
521 repo_path: PathBuf,
522 paths: Vec<String>,
523 ) -> GitFuture<Result<Vec<String>, GitError>> {
524 Box::pin(async move { list_staged_conflict_marker_files(repo_path, paths).await })
525 }
526
527 fn list_conflicted_files(
528 &self,
529 repo_path: PathBuf,
530 ) -> GitFuture<Result<Vec<String>, GitError>> {
531 Box::pin(async move { list_conflicted_files(repo_path).await })
532 }
533
534 fn commit_all(
535 &self,
536 repo_path: PathBuf,
537 message: String,
538 no_verify: bool,
539 ) -> GitFuture<Result<(), GitError>> {
540 Box::pin(async move { commit_all(repo_path, message, no_verify).await })
541 }
542
543 fn commit_all_preserving_single_commit(
544 &self,
545 repo_path: PathBuf,
546 base_branch: String,
547 commit_message: String,
548 message_strategy: SingleCommitMessageStrategy,
549 no_verify: bool,
550 ) -> GitFuture<Result<(), GitError>> {
551 Box::pin(async move {
552 commit_all_preserving_single_commit(
553 repo_path,
554 base_branch,
555 commit_message,
556 message_strategy,
557 no_verify,
558 )
559 .await
560 })
561 }
562
563 fn stage_all(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>> {
564 Box::pin(async move { stage_all(repo_path).await })
565 }
566
567 fn head_short_hash(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
568 Box::pin(async move { head_short_hash(repo_path).await })
569 }
570
571 fn head_hash(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
572 Box::pin(async move { head_hash(repo_path).await })
573 }
574
575 fn ref_hash(
576 &self,
577 repo_path: PathBuf,
578 reference: String,
579 ) -> GitFuture<Result<String, GitError>> {
580 Box::pin(async move { ref_hash(repo_path, reference).await })
581 }
582
583 fn head_commit_message(
584 &self,
585 repo_path: PathBuf,
586 ) -> GitFuture<Result<Option<String>, GitError>> {
587 Box::pin(async move { head_commit_message(repo_path).await })
588 }
589
590 fn delete_branch(
591 &self,
592 repo_path: PathBuf,
593 branch_name: String,
594 ) -> GitFuture<Result<(), GitError>> {
595 Box::pin(async move { delete_branch(repo_path, branch_name).await })
596 }
597
598 fn diff(&self, repo_path: PathBuf, base_branch: String) -> GitFuture<Result<String, GitError>> {
599 Box::pin(async move { diff(repo_path, base_branch).await })
600 }
601
602 fn is_worktree_clean(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>> {
603 Box::pin(async move { is_worktree_clean(repo_path).await })
604 }
605
606 fn worktree_status(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
607 Box::pin(async move { worktree_status(repo_path).await })
608 }
609
610 fn tracked_worktree_status(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
611 Box::pin(async move { tracked_worktree_status(repo_path).await })
612 }
613
614 fn has_commits_since(
615 &self,
616 repo_path: PathBuf,
617 base_branch: String,
618 ) -> GitFuture<Result<bool, GitError>> {
619 Box::pin(async move { has_commits_since(repo_path, base_branch).await })
620 }
621
622 fn pull_rebase(&self, repo_path: PathBuf) -> GitFuture<Result<PullRebaseResult, GitError>> {
623 Box::pin(async move { pull_rebase(repo_path).await })
624 }
625
626 fn push_current_branch(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
627 Box::pin(async move { push_current_branch(repo_path).await })
628 }
629
630 fn push_current_branch_to_remote_branch(
631 &self,
632 repo_path: PathBuf,
633 remote_branch_name: String,
634 ) -> GitFuture<Result<String, GitError>> {
635 Box::pin(async move {
636 push_current_branch_to_remote_branch(repo_path, remote_branch_name).await
637 })
638 }
639
640 fn remote_branch_exists(
641 &self,
642 repo_path: PathBuf,
643 remote_branch_name: String,
644 ) -> GitFuture<Result<bool, GitError>> {
645 Box::pin(async move { remote_branch_exists(repo_path, remote_branch_name).await })
646 }
647
648 fn current_upstream_reference(
649 &self,
650 repo_path: PathBuf,
651 ) -> GitFuture<Result<String, GitError>> {
652 Box::pin(async move { current_upstream_reference(repo_path).await })
653 }
654
655 fn fetch_remote(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>> {
656 Box::pin(async move { fetch_remote(repo_path).await })
657 }
658
659 fn get_ahead_behind(&self, repo_path: PathBuf) -> GitFuture<Result<(u32, u32), GitError>> {
660 Box::pin(async move { get_ahead_behind(repo_path).await })
661 }
662
663 fn get_ref_ahead_behind(
664 &self,
665 repo_path: PathBuf,
666 left_ref: String,
667 right_ref: String,
668 ) -> GitFuture<Result<(u32, u32), GitError>> {
669 Box::pin(async move { get_ref_ahead_behind(repo_path, left_ref, right_ref).await })
670 }
671
672 fn branch_tracking_statuses(
673 &self,
674 repo_path: PathBuf,
675 ) -> GitFuture<Result<BranchTrackingMap, GitError>> {
676 Box::pin(async move { branch_tracking_statuses(repo_path).await })
677 }
678
679 fn list_upstream_commit_titles(
680 &self,
681 repo_path: PathBuf,
682 ) -> GitFuture<Result<Vec<String>, GitError>> {
683 Box::pin(async move { list_upstream_commit_titles(repo_path).await })
684 }
685
686 fn list_local_commit_titles(
687 &self,
688 repo_path: PathBuf,
689 ) -> GitFuture<Result<Vec<String>, GitError>> {
690 Box::pin(async move { list_local_commit_titles(repo_path).await })
691 }
692
693 fn repo_url(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
694 Box::pin(async move { repo_url(repo_path).await })
695 }
696
697 fn main_repo_root(&self, repo_path: PathBuf) -> GitFuture<Result<PathBuf, GitError>> {
698 Box::pin(async move { main_repo_root(repo_path).await })
699 }
700
701 fn main_checkout_working_tree(
702 &self,
703 repo_path: PathBuf,
704 ) -> GitFuture<Result<Option<PathBuf>, GitError>> {
705 Box::pin(async move { main_checkout_working_tree(repo_path).await })
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use std::path::{Path, PathBuf};
712 use std::process::Command;
713 use std::time::Duration;
714 use std::{fs, thread};
715
716 use tempfile::tempdir;
717
718 use super::*;
719
720 fn canonicalize_test_path(path: &Path) -> PathBuf {
723 fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
724 }
725
726 fn run_git_command(repo_path: &Path, args: &[&str]) {
727 let output = Command::new("git")
728 .args(args)
729 .current_dir(repo_path)
730 .output()
731 .expect("failed to run git command");
732
733 assert!(
734 output.status.success(),
735 "git command {:?} failed: {}",
736 args,
737 String::from_utf8_lossy(&output.stderr)
738 );
739 }
740
741 fn run_git_command_stdout(repo_path: &Path, args: &[&str]) -> String {
742 let output = Command::new("git")
743 .args(args)
744 .current_dir(repo_path)
745 .output()
746 .expect("failed to run git command");
747
748 assert!(
749 output.status.success(),
750 "git command {:?} failed: {}",
751 args,
752 String::from_utf8_lossy(&output.stderr)
753 );
754
755 String::from_utf8_lossy(&output.stdout).trim().to_string()
756 }
757
758 fn setup_test_git_repo(repo_path: &Path) {
759 run_git_command(repo_path, &["init", "-b", "main"]);
760 run_git_command(repo_path, &["config", "user.name", "Test User"]);
761 run_git_command(repo_path, &["config", "user.email", "test@example.com"]);
762
763 fs::write(repo_path.join("README.md"), "test repo").expect("failed to write file");
764 run_git_command(repo_path, &["add", "README.md"]);
765 run_git_command(repo_path, &["commit", "-m", "Initial commit"]);
766 }
767
768 #[tokio::test]
769 async fn test_squash_merge_returns_committed_when_changes_exist() {
770 let dir = tempdir().expect("failed to create temp dir");
772 setup_test_git_repo(dir.path());
773 run_git_command(dir.path(), &["checkout", "-b", "feature-branch"]);
774 fs::write(dir.path().join("feature.txt"), "feature content").expect("failed to write file");
775 run_git_command(dir.path(), &["add", "feature.txt"]);
776 run_git_command(dir.path(), &["commit", "-m", "Add feature"]);
777 run_git_command(dir.path(), &["checkout", "main"]);
778
779 let result = squash_merge(
781 dir.path().to_path_buf(),
782 "feature-branch".to_string(),
783 "main".to_string(),
784 "Squash merge feature".to_string(),
785 )
786 .await;
787
788 assert_eq!(
790 result.expect("squash merge should succeed"),
791 SquashMergeOutcome::Committed,
792 );
793 }
794
795 #[tokio::test]
796 async fn test_squash_merge_returns_already_present_when_changes_exist_in_target() {
797 let dir = tempdir().expect("failed to create temp dir");
799 setup_test_git_repo(dir.path());
800 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
801 fs::write(dir.path().join("session.txt"), "session change").expect("failed to write file");
802 run_git_command(dir.path(), &["add", "session.txt"]);
803 run_git_command(dir.path(), &["commit", "-m", "Session change"]);
804 run_git_command(dir.path(), &["checkout", "main"]);
805 fs::write(dir.path().join("session.txt"), "session change").expect("failed to write file");
806 run_git_command(dir.path(), &["add", "session.txt"]);
807 run_git_command(dir.path(), &["commit", "-m", "Apply same change on main"]);
808
809 let result = squash_merge(
811 dir.path().to_path_buf(),
812 "session-branch".to_string(),
813 "main".to_string(),
814 "Merge session".to_string(),
815 )
816 .await;
817
818 assert_eq!(
820 result.expect("squash merge should succeed"),
821 SquashMergeOutcome::AlreadyPresentInTarget,
822 );
823 }
824
825 #[tokio::test]
826 async fn test_commit_all_preserving_single_commit_creates_first_commit() {
827 let dir = tempdir().expect("failed to create temp dir");
829 setup_test_git_repo(dir.path());
830 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
831 let commit_message = "Session commit".to_string();
832 fs::write(dir.path().join("work.txt"), "first change").expect("failed to write file");
833
834 let result = commit_all_preserving_single_commit(
836 dir.path().to_path_buf(),
837 "main".to_string(),
838 commit_message.clone(),
839 SingleCommitMessageStrategy::Replace,
840 false,
841 )
842 .await;
843 let commit_count = run_git_command_stdout(dir.path(), &["rev-list", "--count", "HEAD"]);
844 let head_message = run_git_command_stdout(dir.path(), &["log", "-1", "--pretty=%B"]);
845
846 assert!(
848 result.is_ok(),
849 "commit_all_preserving_single_commit should succeed: {result:?}"
850 );
851 assert_eq!(commit_count, "2");
852 assert_eq!(head_message, commit_message);
853 }
854
855 #[tokio::test]
856 async fn test_commit_all_preserving_single_commit_amends_existing_session_commit() {
857 let dir = tempdir().expect("failed to create temp dir");
859 setup_test_git_repo(dir.path());
860 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
861 let commit_message = "Session commit".to_string();
862 fs::write(dir.path().join("work.txt"), "first change").expect("failed to write file");
863 commit_all_preserving_single_commit(
864 dir.path().to_path_buf(),
865 "main".to_string(),
866 commit_message.clone(),
867 SingleCommitMessageStrategy::Replace,
868 false,
869 )
870 .await
871 .expect("failed to create first session commit");
872 let first_hash = run_git_command_stdout(dir.path(), &["rev-parse", "HEAD"]);
873 let first_count = run_git_command_stdout(dir.path(), &["rev-list", "--count", "HEAD"]);
874
875 fs::write(dir.path().join("work.txt"), "second change").expect("failed to write file");
877 let result = commit_all_preserving_single_commit(
878 dir.path().to_path_buf(),
879 "main".to_string(),
880 commit_message.clone(),
881 SingleCommitMessageStrategy::Replace,
882 false,
883 )
884 .await;
885 let second_hash = run_git_command_stdout(dir.path(), &["rev-parse", "HEAD"]);
886 let second_count = run_git_command_stdout(dir.path(), &["rev-list", "--count", "HEAD"]);
887
888 assert!(result.is_ok(), "amend commit should succeed: {result:?}");
890 assert_ne!(first_hash, second_hash);
891 assert_eq!(first_count, second_count);
892 }
893
894 #[tokio::test]
895 async fn test_commit_all_preserving_single_commit_replaces_amended_message() {
896 let dir = tempdir().expect("failed to create temp dir");
898 setup_test_git_repo(dir.path());
899 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
900 fs::write(dir.path().join("work.txt"), "first change").expect("failed to write file");
901 commit_all_preserving_single_commit(
902 dir.path().to_path_buf(),
903 "main".to_string(),
904 "First session message".to_string(),
905 SingleCommitMessageStrategy::Replace,
906 false,
907 )
908 .await
909 .expect("failed to create first session commit");
910
911 fs::write(dir.path().join("work.txt"), "second change").expect("failed to write file");
913 let result = commit_all_preserving_single_commit(
914 dir.path().to_path_buf(),
915 "main".to_string(),
916 "Refined session message".to_string(),
917 SingleCommitMessageStrategy::Replace,
918 false,
919 )
920 .await;
921 let head_message = run_git_command_stdout(dir.path(), &["log", "-1", "--pretty=%B"]);
922
923 assert!(
925 result.is_ok(),
926 "replace amended message should succeed: {result:?}"
927 );
928 assert_eq!(head_message, "Refined session message");
929 }
930
931 #[tokio::test]
932 async fn test_commit_all_preserving_single_commit_retries_index_lock_and_succeeds() {
933 let dir = tempdir().expect("failed to create temp dir");
935 setup_test_git_repo(dir.path());
936 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
937 let commit_message = "Session commit".to_string();
938 fs::write(dir.path().join("work.txt"), "locked change").expect("failed to write file");
939 let index_lock_path = dir.path().join(".git").join("index.lock");
940 fs::write(&index_lock_path, "stale lock").expect("failed to write lock file");
941 let lock_cleanup = thread::spawn(move || {
942 thread::sleep(Duration::from_millis(250));
943 let _ = fs::remove_file(index_lock_path);
944 });
945
946 let result = commit_all_preserving_single_commit(
948 dir.path().to_path_buf(),
949 "main".to_string(),
950 commit_message.clone(),
951 SingleCommitMessageStrategy::Replace,
952 false,
953 )
954 .await;
955 lock_cleanup
956 .join()
957 .expect("failed to join lock cleanup thread");
958 let head_message = run_git_command_stdout(dir.path(), &["log", "-1", "--pretty=%B"]);
959
960 assert!(
962 result.is_ok(),
963 "retry with index lock should succeed: {result:?}"
964 );
965 assert_eq!(head_message, commit_message);
966 }
967
968 #[tokio::test]
969 async fn test_diff_hides_leading_squash_merged_commit_for_non_rebased_session() {
970 let dir = tempdir().expect("failed to create temp dir");
972 setup_test_git_repo(dir.path());
973 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
974 fs::write(dir.path().join("merged.txt"), "already merged change")
975 .expect("failed to write merged file");
976 run_git_command(dir.path(), &["add", "merged.txt"]);
977 run_git_command(dir.path(), &["commit", "-m", "Session change"]);
978 run_git_command(dir.path(), &["checkout", "main"]);
979 run_git_command(dir.path(), &["merge", "--squash", "session-branch"]);
980 run_git_command(dir.path(), &["commit", "-m", "Squash merge session change"]);
981 run_git_command(dir.path(), &["checkout", "session-branch"]);
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!(
990 diff_output.trim().is_empty(),
991 "expected no diff, got: {diff_output}"
992 );
993 }
994
995 #[tokio::test]
996 async fn test_diff_keeps_new_commits_after_leading_squash_merged_commit() {
997 let dir = tempdir().expect("failed to create temp dir");
999 setup_test_git_repo(dir.path());
1000 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
1001 fs::write(dir.path().join("merged.txt"), "already merged change")
1002 .expect("failed to write merged file");
1003 run_git_command(dir.path(), &["add", "merged.txt"]);
1004 run_git_command(dir.path(), &["commit", "-m", "Session change"]);
1005 run_git_command(dir.path(), &["checkout", "main"]);
1006 run_git_command(dir.path(), &["merge", "--squash", "session-branch"]);
1007 run_git_command(dir.path(), &["commit", "-m", "Squash merge session change"]);
1008 run_git_command(dir.path(), &["checkout", "session-branch"]);
1009 fs::write(dir.path().join("new.txt"), "new session-only change")
1010 .expect("failed to write new file");
1011 run_git_command(dir.path(), &["add", "new.txt"]);
1012 run_git_command(dir.path(), &["commit", "-m", "New session change"]);
1013
1014 let diff_output = diff(dir.path().to_path_buf(), "main".to_string())
1016 .await
1017 .expect("failed to load diff");
1018
1019 assert!(diff_output.contains("new.txt"));
1021 assert!(!diff_output.contains("merged.txt"));
1022 }
1023
1024 #[tokio::test]
1025 async fn test_diff_does_not_include_base_only_commits() {
1026 let dir = tempdir().expect("failed to create temp dir");
1028 setup_test_git_repo(dir.path());
1029 run_git_command(dir.path(), &["checkout", "-b", "session-branch"]);
1030 fs::write(dir.path().join("session.txt"), "session change").expect("failed to write file");
1031 run_git_command(dir.path(), &["add", "session.txt"]);
1032 run_git_command(dir.path(), &["commit", "-m", "Session change"]);
1033 run_git_command(dir.path(), &["checkout", "main"]);
1034 fs::write(dir.path().join("main-only.txt"), "base branch only")
1035 .expect("failed to write base-only file");
1036 run_git_command(dir.path(), &["add", "main-only.txt"]);
1037 run_git_command(dir.path(), &["commit", "-m", "Main branch change"]);
1038 run_git_command(dir.path(), &["checkout", "session-branch"]);
1039
1040 let diff_output = diff(dir.path().to_path_buf(), "main".to_string())
1042 .await
1043 .expect("failed to load diff");
1044
1045 assert!(diff_output.contains("session.txt"));
1047 assert!(!diff_output.contains("main-only.txt"));
1048 }
1049
1050 #[tokio::test]
1051 async fn test_is_worktree_clean_returns_true_for_clean_repo() {
1052 let dir = tempdir().expect("failed to create temp dir");
1054 setup_test_git_repo(dir.path());
1055
1056 let is_clean = is_worktree_clean(dir.path().to_path_buf())
1058 .await
1059 .expect("failed to check worktree cleanliness");
1060
1061 assert!(is_clean);
1063 }
1064
1065 #[tokio::test]
1066 async fn test_is_worktree_clean_returns_false_for_dirty_repo() {
1067 let dir = tempdir().expect("failed to create temp dir");
1069 setup_test_git_repo(dir.path());
1070 fs::write(dir.path().join("README.md"), "dirty change").expect("failed to write change");
1071
1072 let is_clean = is_worktree_clean(dir.path().to_path_buf())
1074 .await
1075 .expect("failed to check worktree cleanliness");
1076
1077 assert!(!is_clean);
1079 }
1080
1081 #[tokio::test]
1082 async fn test_worktree_status_reports_dirty_repo_paths() {
1083 let dir = tempdir().expect("failed to create temp dir");
1085 setup_test_git_repo(dir.path());
1086 fs::write(dir.path().join("README.md"), "dirty change").expect("failed to write change");
1087 fs::write(dir.path().join("new-file.txt"), "new").expect("failed to write new file");
1088
1089 let status = worktree_status(dir.path().to_path_buf())
1091 .await
1092 .expect("failed to read worktree status");
1093
1094 assert!(status.contains("README.md"));
1096 assert!(status.contains("new-file.txt"));
1097 }
1098
1099 #[tokio::test]
1100 async fn test_tracked_worktree_status_ignores_untracked_repo_paths() {
1101 let dir = tempdir().expect("failed to create temp dir");
1103 setup_test_git_repo(dir.path());
1104 fs::write(dir.path().join("README.md"), "dirty change").expect("failed to write change");
1105 fs::write(dir.path().join("new-file.txt"), "new").expect("failed to write new file");
1106
1107 let status = tracked_worktree_status(dir.path().to_path_buf())
1109 .await
1110 .expect("failed to read tracked worktree status");
1111
1112 assert!(status.contains("README.md"));
1114 assert!(!status.contains("new-file.txt"));
1115 }
1116
1117 #[tokio::test]
1118 async fn test_main_repo_root_returns_repo_root_for_main_worktree() {
1119 let dir = tempdir().expect("failed to create temp dir");
1121 setup_test_git_repo(dir.path());
1122
1123 let repo_root = main_repo_root(dir.path().to_path_buf())
1125 .await
1126 .expect("failed to resolve main repo root");
1127
1128 assert_eq!(
1130 canonicalize_test_path(&repo_root),
1131 canonicalize_test_path(dir.path())
1132 );
1133 }
1134
1135 #[tokio::test]
1136 async fn test_main_repo_root_returns_shared_repo_root_for_linked_worktree() {
1137 let dir = tempdir().expect("failed to create temp dir");
1139 setup_test_git_repo(dir.path());
1140 let linked_worktree = dir.path().join("linked-worktree");
1141 create_worktree(
1142 dir.path().to_path_buf(),
1143 linked_worktree.clone(),
1144 "wt/main-repo-root-test".to_string(),
1145 "main".to_string(),
1146 )
1147 .await
1148 .expect("failed to create linked worktree");
1149
1150 let repo_root = main_repo_root(linked_worktree)
1152 .await
1153 .expect("failed to resolve shared repo root");
1154
1155 assert_eq!(
1157 canonicalize_test_path(&repo_root),
1158 canonicalize_test_path(dir.path())
1159 );
1160 }
1161
1162 #[tokio::test]
1163 async fn test_abort_rebase_cleans_stale_rebase_merge_metadata() {
1164 let dir = tempdir().expect("failed to create temp dir");
1166 setup_test_git_repo(dir.path());
1167 let stale_rebase_dir = dir.path().join(".git/rebase-merge");
1168 fs::create_dir_all(&stale_rebase_dir).expect("failed to create stale rebase metadata");
1169 fs::write(stale_rebase_dir.join("head-name"), "refs/heads/main")
1170 .expect("failed to write stale rebase metadata");
1171
1172 let result = abort_rebase(dir.path().to_path_buf()).await;
1174
1175 assert!(result.is_ok(), "abort_rebase should succeed: {result:?}");
1177 assert!(!stale_rebase_dir.exists());
1178 }
1179
1180 #[tokio::test]
1181 async fn test_abort_rebase_returns_error_without_rebase_state_or_stale_metadata() {
1182 let dir = tempdir().expect("failed to create temp dir");
1184 setup_test_git_repo(dir.path());
1185
1186 let result = abort_rebase(dir.path().to_path_buf()).await;
1188
1189 assert!(result.is_err());
1191 }
1192
1193 #[tokio::test]
1194 async fn test_ref_hash_resolves_branch_head() {
1195 let dir = tempdir().expect("failed to create temp dir");
1197 setup_test_git_repo(dir.path());
1198 let expected_hash = run_git_command_stdout(dir.path(), &["rev-parse", "main"]);
1199
1200 let resolved_hash = ref_hash(dir.path().to_path_buf(), "main".to_string())
1202 .await
1203 .expect("failed to resolve main hash");
1204
1205 assert_eq!(resolved_hash, expected_hash);
1207 }
1208
1209 #[tokio::test]
1210 async fn test_rebase_onto_start_replays_commits_after_old_base() {
1211 let dir = tempdir().expect("failed to create temp dir");
1213 setup_test_git_repo(dir.path());
1214 run_git_command(dir.path(), &["checkout", "-b", "parent"]);
1215 fs::write(dir.path().join("parent.txt"), "parent").expect("failed to write parent file");
1216 run_git_command(dir.path(), &["add", "parent.txt"]);
1217 run_git_command(dir.path(), &["commit", "-m", "Parent change"]);
1218 let parent_tip = run_git_command_stdout(dir.path(), &["rev-parse", "HEAD"]);
1219 run_git_command(dir.path(), &["checkout", "-b", "child"]);
1220 fs::write(dir.path().join("child.txt"), "child").expect("failed to write child file");
1221 run_git_command(dir.path(), &["add", "child.txt"]);
1222 run_git_command(dir.path(), &["commit", "-m", "Child change"]);
1223 run_git_command(dir.path(), &["checkout", "main"]);
1224 fs::write(dir.path().join("main.txt"), "main").expect("failed to write main file");
1225 run_git_command(dir.path(), &["add", "main.txt"]);
1226 run_git_command(dir.path(), &["commit", "-m", "Main change"]);
1227 run_git_command(dir.path(), &["checkout", "child"]);
1228
1229 let result = rebase_onto_start(dir.path().to_path_buf(), "main".to_string(), parent_tip)
1231 .await
1232 .expect("failed to start rebase --onto");
1233 let child_only_subjects = run_git_command_stdout(
1234 dir.path(),
1235 &["log", "--format=%s", "--reverse", "main..HEAD"],
1236 );
1237
1238 assert_eq!(result, RebaseStepResult::Completed);
1240 assert_eq!(child_only_subjects, "Child change");
1241 assert!(!dir.path().join("parent.txt").exists());
1242 assert!(dir.path().join("child.txt").exists());
1243 }
1244
1245 #[tokio::test]
1246 async fn test_pull_rebase_returns_error_without_upstream() {
1247 let dir = tempdir().expect("failed to create temp dir");
1249 setup_test_git_repo(dir.path());
1250
1251 let result = pull_rebase(dir.path().to_path_buf()).await;
1253
1254 assert!(result.is_err());
1256 }
1257
1258 #[tokio::test]
1259 async fn test_pull_rebase_targets_single_upstream_when_merge_targets_are_ambiguous() {
1260 let dir = tempdir().expect("failed to create temp dir");
1262 let remote_dir = tempdir().expect("failed to create remote temp dir");
1263 setup_test_git_repo(dir.path());
1264 run_git_command(remote_dir.path(), &["init", "--bare"]);
1265
1266 let remote_path = remote_dir.path().to_string_lossy().to_string();
1267 run_git_command(dir.path(), &["remote", "add", "origin", &remote_path]);
1268 run_git_command(dir.path(), &["push", "-u", "origin", "main"]);
1269
1270 run_git_command(dir.path(), &["checkout", "-b", "feature"]);
1271 fs::write(dir.path().join("feature.txt"), "feature change").expect("failed to write file");
1272 run_git_command(dir.path(), &["add", "feature.txt"]);
1273 run_git_command(dir.path(), &["commit", "-m", "Add feature branch"]);
1274 run_git_command(dir.path(), &["push", "-u", "origin", "feature"]);
1275 run_git_command(dir.path(), &["checkout", "main"]);
1276
1277 run_git_command(
1278 dir.path(),
1279 &["config", "--add", "branch.main.merge", "refs/heads/feature"],
1280 );
1281
1282 let pull_without_explicit_target = Command::new("git")
1283 .args(["pull", "--rebase"])
1284 .current_dir(dir.path())
1285 .output()
1286 .expect("failed to run pull --rebase");
1287
1288 assert!(
1289 !pull_without_explicit_target.status.success(),
1290 "expected plain pull --rebase to fail in ambiguous merge-target setup"
1291 );
1292 assert!(
1293 String::from_utf8_lossy(&pull_without_explicit_target.stderr)
1294 .contains("Cannot rebase onto multiple branches"),
1295 "expected ambiguous merge-target failure"
1296 );
1297
1298 let result = pull_rebase(dir.path().to_path_buf()).await;
1300
1301 assert!(
1303 matches!(result, Ok(PullRebaseResult::Completed)),
1304 "pull_rebase should complete: {result:?}"
1305 );
1306 }
1307
1308 #[tokio::test]
1309 async fn test_pull_rebase_targets_local_upstream_when_upstream_name_has_no_remote_prefix() {
1310 let dir = tempdir().expect("failed to create temp dir");
1312 setup_test_git_repo(dir.path());
1313
1314 run_git_command(dir.path(), &["checkout", "-b", "feature"]);
1315 fs::write(dir.path().join("feature.txt"), "feature change").expect("failed to write file");
1316 run_git_command(dir.path(), &["add", "feature.txt"]);
1317 run_git_command(dir.path(), &["commit", "-m", "Add feature branch"]);
1318 run_git_command(dir.path(), &["checkout", "main"]);
1319
1320 run_git_command(dir.path(), &["config", "branch.main.remote", "."]);
1321 run_git_command(
1322 dir.path(),
1323 &[
1324 "config",
1325 "--replace-all",
1326 "branch.main.merge",
1327 "refs/heads/main",
1328 ],
1329 );
1330 run_git_command(
1331 dir.path(),
1332 &["config", "--add", "branch.main.merge", "refs/heads/feature"],
1333 );
1334
1335 let pull_without_explicit_target = Command::new("git")
1336 .args(["pull", "--rebase"])
1337 .current_dir(dir.path())
1338 .output()
1339 .expect("failed to run pull --rebase");
1340
1341 assert!(
1342 !pull_without_explicit_target.status.success(),
1343 "expected plain pull --rebase to fail in ambiguous merge-target setup"
1344 );
1345 assert!(
1346 String::from_utf8_lossy(&pull_without_explicit_target.stderr)
1347 .contains("Cannot rebase onto multiple branches"),
1348 "expected ambiguous merge-target failure"
1349 );
1350
1351 let result = pull_rebase(dir.path().to_path_buf()).await;
1353
1354 assert!(
1356 matches!(result, Ok(PullRebaseResult::Completed)),
1357 "pull_rebase with local upstream should complete: {result:?}"
1358 );
1359 }
1360
1361 #[tokio::test]
1362 async fn test_list_upstream_commit_titles_returns_error_without_upstream() {
1363 let dir = tempdir().expect("failed to create temp dir");
1365 setup_test_git_repo(dir.path());
1366
1367 let result = list_upstream_commit_titles(dir.path().to_path_buf()).await;
1369
1370 assert!(result.is_err());
1372 }
1373
1374 #[tokio::test]
1375 async fn test_list_upstream_commit_titles_returns_new_upstream_commit_titles() {
1376 let dir = tempdir().expect("failed to create temp dir");
1378 let remote_dir = tempdir().expect("failed to create remote temp dir");
1379 let contributor_dir = tempdir().expect("failed to create contributor temp dir");
1380 let contributor_clone_path = contributor_dir.path().join("clone");
1381 setup_test_git_repo(dir.path());
1382 run_git_command(remote_dir.path(), &["init", "--bare"]);
1383
1384 let remote_path = remote_dir.path().to_string_lossy().to_string();
1385 let contributor_clone_path_text = contributor_clone_path.to_string_lossy().to_string();
1386 run_git_command(dir.path(), &["remote", "add", "origin", &remote_path]);
1387 run_git_command(dir.path(), &["push", "-u", "origin", "main"]);
1388
1389 run_git_command(
1390 contributor_dir.path(),
1391 &["clone", &remote_path, &contributor_clone_path_text],
1392 );
1393 run_git_command(
1394 &contributor_clone_path,
1395 &["config", "user.name", "Contributor User"],
1396 );
1397 run_git_command(
1398 &contributor_clone_path,
1399 &["config", "user.email", "contributor@example.com"],
1400 );
1401 run_git_command(
1402 &contributor_clone_path,
1403 &["checkout", "-B", "main", "origin/main"],
1404 );
1405 fs::write(contributor_clone_path.join("remote.txt"), "remote change")
1406 .expect("failed to write remote change");
1407 run_git_command(&contributor_clone_path, &["add", "remote.txt"]);
1408 run_git_command(
1409 &contributor_clone_path,
1410 &["commit", "-m", "Remote commit title"],
1411 );
1412 run_git_command(&contributor_clone_path, &["push", "origin", "main"]);
1413 run_git_command(dir.path(), &["fetch", "origin"]);
1414
1415 let titles = list_upstream_commit_titles(dir.path().to_path_buf())
1417 .await
1418 .expect("failed to list upstream commit titles");
1419
1420 assert_eq!(titles, vec!["Remote commit title".to_string()]);
1422 }
1423
1424 #[tokio::test]
1425 async fn test_list_local_commit_titles_returns_error_without_upstream() {
1426 let dir = tempdir().expect("failed to create temp dir");
1428 setup_test_git_repo(dir.path());
1429
1430 let result = list_local_commit_titles(dir.path().to_path_buf()).await;
1432
1433 assert!(result.is_err());
1435 }
1436
1437 #[tokio::test]
1438 async fn test_list_local_commit_titles_returns_new_local_commit_titles() {
1439 let dir = tempdir().expect("failed to create temp dir");
1441 let remote_dir = tempdir().expect("failed to create remote temp dir");
1442 setup_test_git_repo(dir.path());
1443 run_git_command(remote_dir.path(), &["init", "--bare"]);
1444
1445 let remote_path = remote_dir.path().to_string_lossy().to_string();
1446 run_git_command(dir.path(), &["remote", "add", "origin", &remote_path]);
1447 run_git_command(dir.path(), &["push", "-u", "origin", "main"]);
1448
1449 fs::write(dir.path().join("local_1.txt"), "local change 1")
1450 .expect("failed to write local change 1");
1451 run_git_command(dir.path(), &["add", "local_1.txt"]);
1452 run_git_command(dir.path(), &["commit", "-m", "Local commit title one"]);
1453
1454 fs::write(dir.path().join("local_2.txt"), "local change 2")
1455 .expect("failed to write local change 2");
1456 run_git_command(dir.path(), &["add", "local_2.txt"]);
1457 run_git_command(dir.path(), &["commit", "-m", "Local commit title two"]);
1458
1459 let titles = list_local_commit_titles(dir.path().to_path_buf())
1461 .await
1462 .expect("failed to list local commit titles");
1463
1464 assert_eq!(
1466 titles,
1467 vec![
1468 "Local commit title one".to_string(),
1469 "Local commit title two".to_string(),
1470 ]
1471 );
1472 }
1473
1474 #[tokio::test]
1475 async fn test_push_current_branch_returns_error_without_remote() {
1476 let dir = tempdir().expect("failed to create temp dir");
1478 setup_test_git_repo(dir.path());
1479
1480 let result = push_current_branch(dir.path().to_path_buf()).await;
1482
1483 assert!(result.is_err());
1485 }
1486
1487 #[tokio::test]
1488 async fn test_push_current_branch_returns_upstream_reference() {
1489 let dir = tempdir().expect("failed to create temp dir");
1491 let remote_dir = tempdir().expect("failed to create remote temp dir");
1492 setup_test_git_repo(dir.path());
1493 run_git_command(remote_dir.path(), &["init", "--bare"]);
1494 let remote_path = remote_dir.path().to_string_lossy().to_string();
1495 run_git_command(dir.path(), &["remote", "add", "origin", &remote_path]);
1496
1497 let upstream_reference = push_current_branch(dir.path().to_path_buf())
1499 .await
1500 .expect("push should set upstream");
1501
1502 assert_eq!(upstream_reference, "origin/main");
1504 }
1505
1506 #[tokio::test]
1507 async fn test_push_current_branch_to_remote_branch_returns_upstream_reference() {
1508 let dir = tempdir().expect("failed to create temp dir");
1510 let remote_dir = tempdir().expect("failed to create remote temp dir");
1511 setup_test_git_repo(dir.path());
1512 run_git_command(remote_dir.path(), &["init", "--bare"]);
1513 let remote_path = remote_dir.path().to_string_lossy().to_string();
1514 run_git_command(dir.path(), &["remote", "add", "origin", &remote_path]);
1515
1516 let upstream_reference = push_current_branch_to_remote_branch(
1518 dir.path().to_path_buf(),
1519 "review/custom-branch".to_string(),
1520 )
1521 .await
1522 .expect("push should set a custom upstream");
1523
1524 assert_eq!(upstream_reference, "origin/review/custom-branch");
1526 }
1527
1528 #[test]
1529 fn test_is_no_upstream_error_detects_upstream_hint() {
1530 let detail = "fatal: The current branch main has no upstream branch.";
1532
1533 let is_no_upstream = sync::is_no_upstream_error(detail);
1535
1536 assert!(is_no_upstream);
1538 }
1539
1540 #[test]
1541 fn test_is_rebase_conflict_detects_conflict_keyword() {
1542 let detail = "CONFLICT (content): Merge conflict in src/main.rs";
1544
1545 assert!(rebase::is_rebase_conflict(detail));
1547 }
1548
1549 #[test]
1550 fn test_is_rebase_conflict_detects_could_not_apply() {
1551 let detail = "error: could not apply abc1234... Update handler";
1553
1554 assert!(rebase::is_rebase_conflict(detail));
1556 }
1557
1558 #[test]
1559 fn test_is_rebase_conflict_detects_mark_as_resolved() {
1560 let detail = "hint: mark them as resolved using git add";
1562
1563 assert!(rebase::is_rebase_conflict(detail));
1565 }
1566
1567 #[test]
1568 fn test_is_rebase_conflict_detects_unresolved_conflict() {
1569 let detail = "fatal: Exiting because of an unresolved conflict.";
1571
1572 assert!(rebase::is_rebase_conflict(detail));
1574 }
1575
1576 #[test]
1577 fn test_is_rebase_conflict_detects_committing_not_possible() {
1578 let detail = "error: Committing is not possible because you have unmerged files.";
1580
1581 assert!(rebase::is_rebase_conflict(detail));
1583 }
1584
1585 #[test]
1586 fn test_is_rebase_conflict_returns_false_for_unrelated_error() {
1587 let detail = "fatal: not a git repository (or any parent up to mount point /)";
1589
1590 assert!(!rebase::is_rebase_conflict(detail));
1592 }
1593
1594 #[test]
1595 fn test_is_rebase_conflict_returns_false_for_index_lock_error() {
1596 let detail = "fatal: Unable to create '.git/index.lock': File exists.";
1598
1599 assert!(!rebase::is_rebase_conflict(detail));
1601 }
1602}