gitdataai/libs/git/commit/rebase.rs
2026-04-15 09:08:09 +08:00

137 lines
4.7 KiB
Rust

//! 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<CommitOid> {
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<git2::Commit> = 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 = &current_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(""),
&current_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(())
}
}