Skip to main content

ralph_workflow/git_helpers/
rebase_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    if !rebase_in_progress_impl(repo) {
93        return Err(no_rebase_in_progress_error());
94    }
95
96    let conflicted = get_conflicted_files()?;
97    if !conflicted.is_empty() {
98        return Err(conflict_remains_error(conflicted.len()));
99    }
100
101    let output = executor.execute("git", &["rebase", "--continue"], &[], None)?;
102
103    if output.succeeded() {
104        Ok(())
105    } else {
106        Err(io::Error::other(format!(
107            "Failed to continue rebase: {}",
108            output.stderr
109        )))
110    }
111}
112
113fn no_rebase_in_progress_error() -> io::Error {
114    io::Error::new(io::ErrorKind::InvalidInput, "No rebase in progress")
115}
116
117fn conflict_remains_error(count: usize) -> io::Error {
118    io::Error::new(
119        io::ErrorKind::InvalidInput,
120        format!(
121            "Cannot continue rebase: {} file(s) still have conflicts",
122            count
123        ),
124    )
125}
126
127/// Check if a rebase is currently in progress.
128///
129/// # Errors
130///
131/// Returns error if the operation fails.
132pub fn rebase_in_progress() -> io::Result<bool> {
133    let repo = git2::Repository::discover(".").map_err(|e| git2_to_io_error(&e))?;
134    Ok(rebase_in_progress_impl(&repo))
135}
136
137/// Implementation of `rebase_in_progress`.
138fn rebase_in_progress_impl(repo: &git2::Repository) -> bool {
139    let state = repo.state();
140    state == git2::RepositoryState::Rebase
141        || state == git2::RepositoryState::RebaseMerge
142        || state == git2::RepositoryState::RebaseInteractive
143}