//! Rebase operations. //! //! Manual rebase implementation since git2's `RebaseSession` has type inference //! issues with `Option<&[AnnotatedCommit]>` in Rust. The approach: //! 1. Walk all commits from `base_oid` (exclusive) to `source_oid` (inclusive). //! 2. For each commit, apply its tree diff onto the current HEAD. //! 3. Create a new commit with the same message on top of the current HEAD. use crate::commit::types::CommitOid; use crate::{GitDomain, GitError, GitResult}; impl GitDomain { /// Rebase the commits on `source_oid` onto `base_oid`. pub fn rebase_commits( &self, base_oid: &CommitOid, source_oid: &CommitOid, ) -> GitResult { let base = base_oid .to_oid() .map_err(|_| GitError::InvalidOid(base_oid.to_string()))?; let source = source_oid .to_oid() .map_err(|_| GitError::InvalidOid(source_oid.to_string()))?; // Collect all commits from base (exclusive) to source (inclusive). let mut revwalk = self .repo() .revwalk() .map_err(|e| GitError::Internal(e.to_string()))?; revwalk .push(source) .map_err(|e| GitError::Internal(e.to_string()))?; revwalk .hide(base) .map_err(|e| GitError::Internal(e.to_string()))?; let mut commits: Vec = Vec::new(); for oid_result in revwalk { let oid = oid_result.map_err(|e| GitError::Internal(e.to_string()))?; let commit = self .repo() .find_commit(oid) .map_err(|e| GitError::Internal(e.to_string()))?; commits.push(commit); } if commits.is_empty() { return Err(GitError::Internal("No commits to rebase".to_string())); } // Sort oldest-first (topological). Use OID as tiebreaker for commits with identical timestamps. commits.sort_by(|a, b| { let time_cmp = a.time().seconds().cmp(&b.time().seconds()); if time_cmp != std::cmp::Ordering::Equal { time_cmp } else { // Same timestamp: use OID for deterministic ordering. a.id().cmp(&b.id()) } }); let sig = self .repo() .signature() .map_err(|e| GitError::Internal(e.to_string()))?; // Start with the base commit's tree. let base_commit = self .repo() .find_commit(base) .map_err(|e| GitError::Internal(e.to_string()))?; let mut current_tree = base_commit .tree() .map_err(|e| GitError::Internal(e.to_string()))?; let mut last_oid = base; for commit in &commits { let parent_tree = ¤t_tree; let commit_tree = commit .tree() .map_err(|e| GitError::Internal(e.to_string()))?; // Diff the parent tree with this commit's tree. let diff = self .repo() .diff_tree_to_tree(Some(parent_tree), Some(&commit_tree), None) .map_err(|e| GitError::Internal(e.to_string()))?; // Apply the diff to parent_tree, producing a new tree. let mut new_index = self .repo() .apply_to_tree(parent_tree, &diff, None) .map_err(|e| GitError::Internal(e.to_string()))?; let new_tree_oid = new_index .write_tree() .map_err(|e| GitError::Internal(e.to_string()))?; current_tree = self .repo() .find_tree(new_tree_oid) .map_err(|e| GitError::Internal(e.to_string()))?; // Find the parent commit for the rebased commit. let parent_commit = self .repo() .find_commit(last_oid) .map_err(|e| GitError::Internal(e.to_string()))?; // Create the rebased commit on top of the current base. let new_oid = self .repo() .commit( Some("HEAD"), &sig, &sig, commit.message().unwrap_or(""), ¤t_tree, &[&parent_commit], ) .map_err(|e| GitError::Internal(e.to_string()))?; last_oid = new_oid; } Ok(CommitOid::from_git2(last_oid)) } pub fn rebase_abort(&self) -> GitResult<()> { // git2 rebase sessions are not persistent across process exits. // The caller resets HEAD to the original position. Ok(()) } }