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};
10use super::sync::{
11 BranchTrackingMap, PullRebaseResult, SingleCommitMessageStrategy, WorktreeFileContent,
12};
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, diff_changed_files, fetch_remote, find_git_repo_root,
17 get_ahead_behind, get_ref_ahead_behind, has_commits_since, has_merge_conflicts,
18 has_unmerged_paths, head_commit_message, head_hash, head_short_hash, in_progress_operation,
19 is_rebase_in_progress, is_worktree_clean, list_conflicted_files, list_local_commit_titles,
20 list_staged_conflict_marker_files, list_upstream_commit_titles, main_checkout_working_tree,
21 main_repo_root, pull_rebase, push_current_branch, push_current_branch_to_new_remote_branch,
22 push_current_branch_to_remote_branch, rebase, rebase_continue, rebase_onto_start, rebase_start,
23 ref_hash, remote_branch_exists, remove_worktree, repo_url, run_pre_commit_hook, squash_merge,
24 squash_merge_diff, stage_all, sync, tracked_worktree_status, worktree_status,
25};
26
27pub type GitFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
29
30#[cfg_attr(any(test, feature = "test-utils"), mockall::automock)]
35pub trait GitClient: Send + Sync {
36 fn detect_git_info(&self, dir: PathBuf) -> GitFuture<Option<String>>;
41
42 fn find_git_repo_root(&self, dir: PathBuf) -> GitFuture<Option<PathBuf>>;
46
47 fn check_pre_commit_hook_ready(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>>;
53
54 fn run_pre_commit_hook(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>>;
62
63 fn create_worktree(
70 &self,
71 repo_path: PathBuf,
72 worktree_path: PathBuf,
73 branch_name: String,
74 start_ref: String,
75 ) -> GitFuture<Result<(), GitError>>;
76
77 fn remove_worktree(&self, worktree_path: PathBuf) -> GitFuture<Result<(), GitError>>;
83
84 fn squash_merge_diff(
90 &self,
91 repo_path: PathBuf,
92 source_branch: String,
93 target_branch: String,
94 ) -> GitFuture<Result<String, GitError>>;
95
96 fn squash_merge(
102 &self,
103 repo_path: PathBuf,
104 source_branch: String,
105 target_branch: String,
106 commit_message: String,
107 ) -> GitFuture<Result<SquashMergeOutcome, GitError>>;
108
109 fn rebase(&self, repo_path: PathBuf, target_branch: String) -> GitFuture<Result<(), GitError>>;
114
115 fn rebase_start(
121 &self,
122 repo_path: PathBuf,
123 target_branch: String,
124 ) -> GitFuture<Result<RebaseStepResult, GitError>>;
125
126 fn rebase_onto_start(
131 &self,
132 repo_path: PathBuf,
133 new_base: String,
134 old_base: String,
135 ) -> GitFuture<Result<RebaseStepResult, GitError>>;
136
137 fn rebase_continue(&self, repo_path: PathBuf) -> GitFuture<Result<RebaseStepResult, GitError>>;
142
143 fn abort_rebase(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>>;
148
149 fn is_rebase_in_progress(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>>;
155
156 fn in_progress_operation(
161 &self,
162 repo_path: PathBuf,
163 ) -> GitFuture<Result<Option<InProgressGitOperation>, GitError>>;
164
165 fn has_unmerged_paths(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>>;
170
171 fn list_staged_conflict_marker_files(
177 &self,
178 repo_path: PathBuf,
179 paths: Vec<String>,
180 ) -> GitFuture<Result<Vec<String>, GitError>>;
181
182 fn list_conflicted_files(&self, repo_path: PathBuf)
187 -> GitFuture<Result<Vec<String>, GitError>>;
188
189 fn commit_all(&self, repo_path: PathBuf, message: String) -> GitFuture<Result<(), GitError>>;
194
195 fn commit_all_preserving_single_commit(
203 &self,
204 repo_path: PathBuf,
205 base_branch: String,
206 commit_message: String,
207 message_strategy: SingleCommitMessageStrategy,
208 ) -> GitFuture<Result<(), GitError>>;
209
210 fn stage_all(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>>;
215
216 fn head_short_hash(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
221
222 fn head_hash(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
227
228 fn ref_hash(
233 &self,
234 repo_path: PathBuf,
235 reference: String,
236 ) -> GitFuture<Result<String, GitError>>;
237
238 fn head_commit_message(
244 &self,
245 repo_path: PathBuf,
246 ) -> GitFuture<Result<Option<String>, GitError>>;
247
248 fn delete_branch(
254 &self,
255 repo_path: PathBuf,
256 branch_name: String,
257 ) -> GitFuture<Result<(), GitError>>;
258
259 fn diff(&self, repo_path: PathBuf, base_branch: String) -> GitFuture<Result<String, GitError>>;
265
266 fn diff_changed_files(
273 &self,
274 repo_path: PathBuf,
275 base_branch: String,
276 ) -> GitFuture<Result<Vec<String>, GitError>>;
277
278 fn read_worktree_file(
283 &self,
284 repo_path: PathBuf,
285 relative_path: String,
286 ) -> GitFuture<Result<WorktreeFileContent, GitError>>;
287
288 fn is_worktree_clean(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>>;
293
294 fn worktree_status(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
299
300 fn tracked_worktree_status(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
305
306 fn has_commits_since(
312 &self,
313 repo_path: PathBuf,
314 base_branch: String,
315 ) -> GitFuture<Result<bool, GitError>>;
316
317 fn pull_rebase(&self, repo_path: PathBuf) -> GitFuture<Result<PullRebaseResult, GitError>>;
322
323 fn push_current_branch(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
330
331 fn push_current_branch_to_remote_branch(
338 &self,
339 repo_path: PathBuf,
340 remote_branch_name: String,
341 ) -> GitFuture<Result<String, GitError>>;
342
343 fn push_current_branch_to_new_remote_branch(
349 &self,
350 repo_path: PathBuf,
351 remote_branch_name: String,
352 ) -> GitFuture<Result<String, GitError>>;
353
354 fn remote_branch_exists(
360 &self,
361 repo_path: PathBuf,
362 remote_branch_name: String,
363 ) -> GitFuture<Result<bool, GitError>>;
364
365 fn current_upstream_reference(&self, repo_path: PathBuf)
370 -> GitFuture<Result<String, GitError>>;
371
372 fn fetch_remote(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>>;
377
378 fn get_ahead_behind(&self, repo_path: PathBuf) -> GitFuture<Result<(u32, u32), GitError>>;
383
384 fn get_ref_ahead_behind(
392 &self,
393 repo_path: PathBuf,
394 left_ref: String,
395 right_ref: String,
396 ) -> GitFuture<Result<(u32, u32), GitError>>;
397
398 fn has_merge_conflicts(
405 &self,
406 repo_path: PathBuf,
407 source_branch: String,
408 target_branch: String,
409 ) -> GitFuture<Result<bool, GitError>>;
410
411 fn branch_tracking_statuses(
420 &self,
421 repo_path: PathBuf,
422 ) -> GitFuture<Result<BranchTrackingMap, GitError>>;
423
424 fn list_upstream_commit_titles(
431 &self,
432 repo_path: PathBuf,
433 ) -> GitFuture<Result<Vec<String>, GitError>>;
434
435 fn list_local_commit_titles(
441 &self,
442 repo_path: PathBuf,
443 ) -> GitFuture<Result<Vec<String>, GitError>>;
444
445 fn repo_url(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>>;
450
451 fn main_repo_root(&self, repo_path: PathBuf) -> GitFuture<Result<PathBuf, GitError>>;
456
457 fn main_checkout_working_tree(
465 &self,
466 repo_path: PathBuf,
467 ) -> GitFuture<Result<Option<PathBuf>, GitError>>;
468}
469
470pub struct RealGitClient;
472
473impl GitClient for RealGitClient {
474 fn detect_git_info(&self, dir: PathBuf) -> GitFuture<Option<String>> {
475 Box::pin(async move { detect_git_info(dir).await })
476 }
477
478 fn find_git_repo_root(&self, dir: PathBuf) -> GitFuture<Option<PathBuf>> {
479 Box::pin(async move { find_git_repo_root(dir).await })
480 }
481
482 fn check_pre_commit_hook_ready(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>> {
483 Box::pin(async move { check_pre_commit_hook_ready(repo_path).await })
484 }
485
486 fn run_pre_commit_hook(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>> {
487 Box::pin(async move { run_pre_commit_hook(repo_path).await })
488 }
489
490 fn create_worktree(
491 &self,
492 repo_path: PathBuf,
493 worktree_path: PathBuf,
494 branch_name: String,
495 start_ref: String,
496 ) -> GitFuture<Result<(), GitError>> {
497 Box::pin(
498 async move { create_worktree(repo_path, worktree_path, branch_name, start_ref).await },
499 )
500 }
501
502 fn remove_worktree(&self, worktree_path: PathBuf) -> GitFuture<Result<(), GitError>> {
503 Box::pin(async move { remove_worktree(worktree_path).await })
504 }
505
506 fn squash_merge_diff(
507 &self,
508 repo_path: PathBuf,
509 source_branch: String,
510 target_branch: String,
511 ) -> GitFuture<Result<String, GitError>> {
512 Box::pin(async move { squash_merge_diff(repo_path, source_branch, target_branch).await })
513 }
514
515 fn squash_merge(
516 &self,
517 repo_path: PathBuf,
518 source_branch: String,
519 target_branch: String,
520 commit_message: String,
521 ) -> GitFuture<Result<SquashMergeOutcome, GitError>> {
522 Box::pin(async move {
523 squash_merge(repo_path, source_branch, target_branch, commit_message).await
524 })
525 }
526
527 fn rebase(&self, repo_path: PathBuf, target_branch: String) -> GitFuture<Result<(), GitError>> {
528 Box::pin(async move { rebase::rebase(repo_path, target_branch).await })
529 }
530
531 fn rebase_start(
532 &self,
533 repo_path: PathBuf,
534 target_branch: String,
535 ) -> GitFuture<Result<RebaseStepResult, GitError>> {
536 Box::pin(async move { rebase_start(repo_path, target_branch).await })
537 }
538
539 fn rebase_onto_start(
540 &self,
541 repo_path: PathBuf,
542 new_base: String,
543 old_base: String,
544 ) -> GitFuture<Result<RebaseStepResult, GitError>> {
545 Box::pin(async move { rebase_onto_start(repo_path, new_base, old_base).await })
546 }
547
548 fn rebase_continue(&self, repo_path: PathBuf) -> GitFuture<Result<RebaseStepResult, GitError>> {
549 Box::pin(async move { rebase_continue(repo_path).await })
550 }
551
552 fn abort_rebase(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>> {
553 Box::pin(async move { abort_rebase(repo_path).await })
554 }
555
556 fn is_rebase_in_progress(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>> {
557 Box::pin(async move { is_rebase_in_progress(repo_path).await })
558 }
559
560 fn in_progress_operation(
561 &self,
562 repo_path: PathBuf,
563 ) -> GitFuture<Result<Option<InProgressGitOperation>, GitError>> {
564 Box::pin(async move { in_progress_operation(repo_path).await })
565 }
566
567 fn has_unmerged_paths(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>> {
568 Box::pin(async move { has_unmerged_paths(repo_path).await })
569 }
570
571 fn list_staged_conflict_marker_files(
572 &self,
573 repo_path: PathBuf,
574 paths: Vec<String>,
575 ) -> GitFuture<Result<Vec<String>, GitError>> {
576 Box::pin(async move { list_staged_conflict_marker_files(repo_path, paths).await })
577 }
578
579 fn list_conflicted_files(
580 &self,
581 repo_path: PathBuf,
582 ) -> GitFuture<Result<Vec<String>, GitError>> {
583 Box::pin(async move { list_conflicted_files(repo_path).await })
584 }
585
586 fn commit_all(&self, repo_path: PathBuf, message: String) -> GitFuture<Result<(), GitError>> {
587 Box::pin(async move { commit_all(repo_path, message).await })
588 }
589
590 fn commit_all_preserving_single_commit(
591 &self,
592 repo_path: PathBuf,
593 base_branch: String,
594 commit_message: String,
595 message_strategy: SingleCommitMessageStrategy,
596 ) -> GitFuture<Result<(), GitError>> {
597 Box::pin(async move {
598 commit_all_preserving_single_commit(
599 repo_path,
600 base_branch,
601 commit_message,
602 message_strategy,
603 )
604 .await
605 })
606 }
607
608 fn stage_all(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>> {
609 Box::pin(async move { stage_all(repo_path).await })
610 }
611
612 fn head_short_hash(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
613 Box::pin(async move { head_short_hash(repo_path).await })
614 }
615
616 fn head_hash(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
617 Box::pin(async move { head_hash(repo_path).await })
618 }
619
620 fn ref_hash(
621 &self,
622 repo_path: PathBuf,
623 reference: String,
624 ) -> GitFuture<Result<String, GitError>> {
625 Box::pin(async move { ref_hash(repo_path, reference).await })
626 }
627
628 fn head_commit_message(
629 &self,
630 repo_path: PathBuf,
631 ) -> GitFuture<Result<Option<String>, GitError>> {
632 Box::pin(async move { head_commit_message(repo_path).await })
633 }
634
635 fn delete_branch(
636 &self,
637 repo_path: PathBuf,
638 branch_name: String,
639 ) -> GitFuture<Result<(), GitError>> {
640 Box::pin(async move { delete_branch(repo_path, branch_name).await })
641 }
642
643 fn diff(&self, repo_path: PathBuf, base_branch: String) -> GitFuture<Result<String, GitError>> {
644 Box::pin(async move { diff(repo_path, base_branch).await })
645 }
646
647 fn diff_changed_files(
648 &self,
649 repo_path: PathBuf,
650 base_branch: String,
651 ) -> GitFuture<Result<Vec<String>, GitError>> {
652 Box::pin(async move { diff_changed_files(repo_path, base_branch).await })
653 }
654
655 fn read_worktree_file(
656 &self,
657 repo_path: PathBuf,
658 relative_path: String,
659 ) -> GitFuture<Result<WorktreeFileContent, GitError>> {
660 Box::pin(async move { sync::read_worktree_file(repo_path, relative_path).await })
661 }
662
663 fn is_worktree_clean(&self, repo_path: PathBuf) -> GitFuture<Result<bool, GitError>> {
664 Box::pin(async move { is_worktree_clean(repo_path).await })
665 }
666
667 fn worktree_status(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
668 Box::pin(async move { worktree_status(repo_path).await })
669 }
670
671 fn tracked_worktree_status(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
672 Box::pin(async move { tracked_worktree_status(repo_path).await })
673 }
674
675 fn has_commits_since(
676 &self,
677 repo_path: PathBuf,
678 base_branch: String,
679 ) -> GitFuture<Result<bool, GitError>> {
680 Box::pin(async move { has_commits_since(repo_path, base_branch).await })
681 }
682
683 fn pull_rebase(&self, repo_path: PathBuf) -> GitFuture<Result<PullRebaseResult, GitError>> {
684 Box::pin(async move { pull_rebase(repo_path).await })
685 }
686
687 fn push_current_branch(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
688 Box::pin(async move { push_current_branch(repo_path).await })
689 }
690
691 fn push_current_branch_to_remote_branch(
692 &self,
693 repo_path: PathBuf,
694 remote_branch_name: String,
695 ) -> GitFuture<Result<String, GitError>> {
696 Box::pin(async move {
697 push_current_branch_to_remote_branch(repo_path, remote_branch_name).await
698 })
699 }
700
701 fn push_current_branch_to_new_remote_branch(
702 &self,
703 repo_path: PathBuf,
704 remote_branch_name: String,
705 ) -> GitFuture<Result<String, GitError>> {
706 Box::pin(async move {
707 push_current_branch_to_new_remote_branch(repo_path, remote_branch_name).await
708 })
709 }
710
711 fn remote_branch_exists(
712 &self,
713 repo_path: PathBuf,
714 remote_branch_name: String,
715 ) -> GitFuture<Result<bool, GitError>> {
716 Box::pin(async move { remote_branch_exists(repo_path, remote_branch_name).await })
717 }
718
719 fn current_upstream_reference(
720 &self,
721 repo_path: PathBuf,
722 ) -> GitFuture<Result<String, GitError>> {
723 Box::pin(async move { current_upstream_reference(repo_path).await })
724 }
725
726 fn fetch_remote(&self, repo_path: PathBuf) -> GitFuture<Result<(), GitError>> {
727 Box::pin(async move { fetch_remote(repo_path).await })
728 }
729
730 fn get_ahead_behind(&self, repo_path: PathBuf) -> GitFuture<Result<(u32, u32), GitError>> {
731 Box::pin(async move { get_ahead_behind(repo_path).await })
732 }
733
734 fn get_ref_ahead_behind(
735 &self,
736 repo_path: PathBuf,
737 left_ref: String,
738 right_ref: String,
739 ) -> GitFuture<Result<(u32, u32), GitError>> {
740 Box::pin(async move { get_ref_ahead_behind(repo_path, left_ref, right_ref).await })
741 }
742
743 fn has_merge_conflicts(
744 &self,
745 repo_path: PathBuf,
746 source_branch: String,
747 target_branch: String,
748 ) -> GitFuture<Result<bool, GitError>> {
749 Box::pin(async move { has_merge_conflicts(repo_path, source_branch, target_branch).await })
750 }
751
752 fn branch_tracking_statuses(
753 &self,
754 repo_path: PathBuf,
755 ) -> GitFuture<Result<BranchTrackingMap, GitError>> {
756 Box::pin(async move { branch_tracking_statuses(repo_path).await })
757 }
758
759 fn list_upstream_commit_titles(
760 &self,
761 repo_path: PathBuf,
762 ) -> GitFuture<Result<Vec<String>, GitError>> {
763 Box::pin(async move { list_upstream_commit_titles(repo_path).await })
764 }
765
766 fn list_local_commit_titles(
767 &self,
768 repo_path: PathBuf,
769 ) -> GitFuture<Result<Vec<String>, GitError>> {
770 Box::pin(async move { list_local_commit_titles(repo_path).await })
771 }
772
773 fn repo_url(&self, repo_path: PathBuf) -> GitFuture<Result<String, GitError>> {
774 Box::pin(async move { repo_url(repo_path).await })
775 }
776
777 fn main_repo_root(&self, repo_path: PathBuf) -> GitFuture<Result<PathBuf, GitError>> {
778 Box::pin(async move { main_repo_root(repo_path).await })
779 }
780
781 fn main_checkout_working_tree(
782 &self,
783 repo_path: PathBuf,
784 ) -> GitFuture<Result<Option<PathBuf>, GitError>> {
785 Box::pin(async move { main_checkout_working_tree(repo_path).await })
786 }
787}
788
789#[cfg(test)]
790#[path = "client_test.rs"]
791mod tests;