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///
23/// # Errors
24///
25/// Returns an error if the repository cannot be accessed or branch verification fails.
26#[cfg(any(test, feature = "test-utils"))]
27pub fn verify_rebase_completed(upstream_branch: &str) -> io::Result<bool> {
28    let repo = git2::Repository::discover(".").map_err(|e| git2_to_io_error(&e))?;
29
30    // 1. Check if a rebase is still in progress
31    let state = repo.state();
32    if state == git2::RepositoryState::Rebase
33        || state == git2::RepositoryState::RebaseMerge
34        || state == git2::RepositoryState::RebaseInteractive
35    {
36        return Ok(false);
37    }
38
39    // 2. Check if there are any remaining conflicts in the index
40    let index = repo.index().map_err(|e| git2_to_io_error(&e))?;
41    if index.has_conflicts() {
42        return Ok(false);
43    }
44
45    // 3. Verify HEAD is valid
46    let head = repo.head().map_err(|e| {
47        io::Error::new(
48            io::ErrorKind::InvalidData,
49            format!("Repository HEAD is invalid: {e}"),
50        )
51    })?;
52
53    // 4. Verify the current branch is a descendant of upstream
54    if let Ok(head_commit) = head.peel_to_commit() {
55        if let Ok(upstream_object) = repo.revparse_single(upstream_branch) {
56            if let Ok(upstream_commit) = upstream_object.peel_to_commit() {
57                match repo.graph_descendant_of(head_commit.id(), upstream_commit.id()) {
58                    Ok(is_descendant) => {
59                        if is_descendant {
60                            return Ok(true);
61                        }
62                        return Ok(false);
63                    }
64                    Err(e) => {
65                        let _ = e;
66                    }
67                }
68            }
69        }
70    }
71
72    Ok(!index.has_conflicts())
73}
74
75/// Continue a rebase after conflict resolution.
76///
77/// **Note:** This function uses the current working directory to discover the repo.
78///
79/// # Errors
80///
81/// Returns error if the operation fails.
82pub fn continue_rebase(executor: &dyn crate::executor::ProcessExecutor) -> io::Result<()> {
83    let repo = git2::Repository::discover(".").map_err(|e| git2_to_io_error(&e))?;
84    continue_rebase_impl(&repo, executor)
85}
86
87/// Implementation of `continue_rebase`.
88fn continue_rebase_impl(
89    repo: &git2::Repository,
90    executor: &dyn crate::executor::ProcessExecutor,
91) -> io::Result<()> {
92    // Check if a rebase is in progress
93    let state = repo.state();
94    if state != git2::RepositoryState::Rebase
95        && state != git2::RepositoryState::RebaseMerge
96        && state != git2::RepositoryState::RebaseInteractive
97    {
98        return Err(io::Error::new(
99            io::ErrorKind::InvalidInput,
100            "No rebase in progress",
101        ));
102    }
103
104    // Check if there are still conflicts
105    let conflicted = get_conflicted_files()?;
106    if !conflicted.is_empty() {
107        return Err(io::Error::new(
108            io::ErrorKind::InvalidInput,
109            format!(
110                "Cannot continue rebase: {} file(s) still have conflicts",
111                conflicted.len()
112            ),
113        ));
114    }
115
116    // Use git CLI for continue via executor
117    let output = executor.execute("git", &["rebase", "--continue"], &[], None)?;
118
119    if output.status.success() {
120        Ok(())
121    } else {
122        Err(io::Error::other(format!(
123            "Failed to continue rebase: {}",
124            output.stderr
125        )))
126    }
127}
128
129/// Check if a rebase is currently in progress.
130///
131/// # Errors
132///
133/// Returns error if the operation fails.
134pub fn rebase_in_progress() -> io::Result<bool> {
135    let repo = git2::Repository::discover(".").map_err(|e| git2_to_io_error(&e))?;
136    Ok(rebase_in_progress_impl(&repo))
137}
138
139/// Implementation of `rebase_in_progress`.
140fn rebase_in_progress_impl(repo: &git2::Repository) -> bool {
141    let state = repo.state();
142    state == git2::RepositoryState::Rebase
143        || state == git2::RepositoryState::RebaseMerge
144        || state == git2::RepositoryState::RebaseInteractive
145}