137 lines
4.7 KiB
Rust
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 = ¤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(())
|
|
}
|
|
}
|