Skip to main content

ralph_workflow/git_helpers/rebase/operations/
continuation.rs

1// Core rebase operations: continue + verification + status.
2
3/// Verify that a rebase has completed successfully using LibGit2.
4///
5/// This function uses LibGit2 exclusively to verify that a rebase operation
6/// has completed successfully. It checks:
7/// - Repository state is clean (no rebase in progress)
8/// - HEAD is valid and not detached (unless expected)
9/// - Index has no conflicts
10/// - Current branch is descendant of upstream (rebase succeeded)
11///
12/// # Returns
13///
14/// Returns `Ok(true)` if rebase is verified as complete, `Ok(false)` if
15/// rebase is still in progress (conflicts remain), or an error if the
16/// repository state is invalid.
17///
18/// # Note
19///
20/// This is the authoritative source for rebase completion verification.
21/// It does NOT depend on parsing agent output or any other external signals.
22#[cfg(any(test, feature = "test-utils"))]
23pub fn verify_rebase_completed(upstream_branch: &str) -> io::Result<bool> {
24    let repo = git2::Repository::discover(".").map_err(|e| git2_to_io_error(&e))?;
25
26    // 1. Check if a rebase is still in progress
27    let state = repo.state();
28    if state == git2::RepositoryState::Rebase
29        || state == git2::RepositoryState::RebaseMerge
30        || state == git2::RepositoryState::RebaseInteractive
31    {
32        return Ok(false);
33    }
34
35    // 2. Check if there are any remaining conflicts in the index
36    let index = repo.index().map_err(|e| git2_to_io_error(&e))?;
37    if index.has_conflicts() {
38        return Ok(false);
39    }
40
41    // 3. Verify HEAD is valid
42    let head = repo.head().map_err(|e| {
43        io::Error::new(
44            io::ErrorKind::InvalidData,
45            format!("Repository HEAD is invalid: {e}"),
46        )
47    })?;
48
49    // 4. Verify the current branch is a descendant of upstream
50    if let Ok(head_commit) = head.peel_to_commit() {
51        if let Ok(upstream_object) = repo.revparse_single(upstream_branch) {
52            if let Ok(upstream_commit) = upstream_object.peel_to_commit() {
53                match repo.graph_descendant_of(head_commit.id(), upstream_commit.id()) {
54                    Ok(is_descendant) => {
55                        if is_descendant {
56                            return Ok(true);
57                        }
58                        return Ok(false);
59                    }
60                    Err(e) => {
61                        let _ = e;
62                    }
63                }
64            }
65        }
66    }
67
68    Ok(!index.has_conflicts())
69}
70
71/// Continue a rebase after conflict resolution.
72///
73/// **Note:** This function uses the current working directory to discover the repo.
74pub fn continue_rebase(executor: &dyn crate::executor::ProcessExecutor) -> io::Result<()> {
75    let repo = git2::Repository::discover(".").map_err(|e| git2_to_io_error(&e))?;
76    continue_rebase_impl(&repo, executor)
77}
78
79/// Implementation of continue_rebase.
80fn continue_rebase_impl(
81    repo: &git2::Repository,
82    executor: &dyn crate::executor::ProcessExecutor,
83) -> io::Result<()> {
84    // Check if a rebase is in progress
85    let state = repo.state();
86    if state != git2::RepositoryState::Rebase
87        && state != git2::RepositoryState::RebaseMerge
88        && state != git2::RepositoryState::RebaseInteractive
89    {
90        return Err(io::Error::new(
91            io::ErrorKind::InvalidInput,
92            "No rebase in progress",
93        ));
94    }
95
96    // Check if there are still conflicts
97    let conflicted = get_conflicted_files()?;
98    if !conflicted.is_empty() {
99        return Err(io::Error::new(
100            io::ErrorKind::InvalidInput,
101            format!(
102                "Cannot continue rebase: {} file(s) still have conflicts",
103                conflicted.len()
104            ),
105        ));
106    }
107
108    // Use git CLI for continue via executor
109    let output = executor.execute("git", &["rebase", "--continue"], &[], None)?;
110
111    if output.status.success() {
112        Ok(())
113    } else {
114        Err(io::Error::other(format!(
115            "Failed to continue rebase: {}",
116            output.stderr
117        )))
118    }
119}
120
121/// Check if a rebase is currently in progress.
122pub fn rebase_in_progress() -> io::Result<bool> {
123    let repo = git2::Repository::discover(".").map_err(|e| git2_to_io_error(&e))?;
124    rebase_in_progress_impl(&repo)
125}
126
127/// Implementation of rebase_in_progress.
128fn rebase_in_progress_impl(repo: &git2::Repository) -> io::Result<bool> {
129    let state = repo.state();
130    Ok(state == git2::RepositoryState::Rebase
131        || state == git2::RepositoryState::RebaseMerge
132        || state == git2::RepositoryState::RebaseInteractive)
133}