gitdataai/libs/git/merge/ops.rs
2026-04-14 19:02:01 +08:00

346 lines
11 KiB
Rust

//! Merge operations.
use crate::commit::types::CommitOid;
use crate::merge::types::{
MergeAnalysisResult, MergeOptions, MergePreferenceResult, MergeheadInfo,
};
use crate::{GitDomain, GitError, GitResult};
impl GitDomain {
pub fn merge_analysis(
&self,
their_oid: &CommitOid,
) -> GitResult<(MergeAnalysisResult, MergePreferenceResult)> {
let oid = their_oid
.to_oid()
.map_err(|_| GitError::InvalidOid(their_oid.to_string()))?;
let head_ref = self
.repo()
.find_reference("HEAD")
.map_err(|e| GitError::Internal(e.to_string()))?;
let annotated = self
.repo()
.reference_to_annotated_commit(&head_ref)
.map_err(|e| GitError::Internal(e.to_string()))?;
let their_annotated = self
.repo()
.find_annotated_commit(oid)
.map_err(|e: git2::Error| GitError::Internal(e.to_string()))?;
let (analysis, pref) = self
.repo()
.merge_analysis(&[&annotated, &their_annotated])
.map_err(|e| GitError::Internal(e.to_string()))?;
Ok((
MergeAnalysisResult::from_git2(analysis),
MergePreferenceResult::from_git2(pref),
))
}
pub fn merge_analysis_for_ref(
&self,
ref_name: &str,
their_oid: &CommitOid,
) -> GitResult<(MergeAnalysisResult, MergePreferenceResult)> {
let oid = their_oid
.to_oid()
.map_err(|_| GitError::InvalidOid(their_oid.to_string()))?;
let reference = self
.repo()
.find_reference(ref_name)
.map_err(|e| GitError::Internal(e.to_string()))?;
let their_annotated = self
.repo()
.find_annotated_commit(oid)
.map_err(|e: git2::Error| GitError::Internal(e.to_string()))?;
let (analysis, pref) = self
.repo()
.merge_analysis_for_ref(&reference, &[&their_annotated])
.map_err(|e| GitError::Internal(e.to_string()))?;
Ok((
MergeAnalysisResult::from_git2(analysis),
MergePreferenceResult::from_git2(pref),
))
}
pub fn merge_base(&self, oid1: &CommitOid, oid2: &CommitOid) -> GitResult<CommitOid> {
let o1 = oid1
.to_oid()
.map_err(|_| GitError::InvalidOid(oid1.to_string()))?;
let o2 = oid2
.to_oid()
.map_err(|_| GitError::InvalidOid(oid2.to_string()))?;
let base = self
.repo()
.merge_base(o1, o2)
.map_err(|e| GitError::Internal(e.to_string()))?;
Ok(CommitOid::from_git2(base))
}
pub fn merge_base_many(&self, oids: &[CommitOid]) -> GitResult<CommitOid> {
let oids: Vec<_> = oids
.iter()
.map(|o| o.to_oid().map_err(|_| GitError::InvalidOid(o.to_string())))
.collect::<Result<Vec<_>, _>>()?;
let base = self
.repo()
.merge_base_many(&oids)
.map_err(|e| GitError::Internal(e.to_string()))?;
Ok(CommitOid::from_git2(base))
}
pub fn merge_base_octopus(&self, oids: &[CommitOid]) -> GitResult<CommitOid> {
let oids: Vec<_> = oids
.iter()
.map(|o| o.to_oid().map_err(|_| GitError::InvalidOid(o.to_string())))
.collect::<Result<Vec<_>, _>>()?;
let base = self
.repo()
.merge_base_octopus(&oids)
.map_err(|e| GitError::Internal(e.to_string()))?;
Ok(CommitOid::from_git2(base))
}
pub fn merge_commits(
&self,
local_commit: &CommitOid,
remote_commit: &CommitOid,
opts: Option<MergeOptions>,
) -> GitResult<()> {
let local_oid = local_commit
.to_oid()
.map_err(|_| GitError::InvalidOid(local_commit.to_string()))?;
let remote_oid = remote_commit
.to_oid()
.map_err(|_| GitError::InvalidOid(remote_commit.to_string()))?;
let local = self
.repo()
.find_commit(local_oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
let remote = self
.repo()
.find_commit(remote_oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
let mut merge_opts = opts
.map(|o| o.to_git2())
.unwrap_or_else(git2::MergeOptions::new);
self.repo()
.merge_commits(&local, &remote, Some(&mut merge_opts))
.map_err(|e| GitError::Internal(e.to_string()))?;
Ok(())
}
pub fn merge_trees(
&self,
ancestor_tree: &CommitOid,
our_tree: &CommitOid,
their_tree: &CommitOid,
opts: Option<MergeOptions>,
) -> GitResult<()> {
let ancestor_oid = ancestor_tree
.to_oid()
.map_err(|_| GitError::InvalidOid(ancestor_tree.to_string()))?;
let our_oid = our_tree
.to_oid()
.map_err(|_| GitError::InvalidOid(our_tree.to_string()))?;
let their_oid = their_tree
.to_oid()
.map_err(|_| GitError::InvalidOid(their_tree.to_string()))?;
let ancestor = self
.repo()
.find_tree(ancestor_oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
let ours = self
.repo()
.find_tree(our_oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
let theirs = self
.repo()
.find_tree(their_oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
let mut merge_opts = opts
.map(|o| o.to_git2())
.unwrap_or_else(git2::MergeOptions::new);
self.repo()
.merge_trees(&ancestor, &ours, &theirs, Some(&mut merge_opts))
.map_err(|e| GitError::Internal(e.to_string()))?;
Ok(())
}
pub fn merge_abort(&self) -> GitResult<()> {
self.repo()
.cleanup_state()
.map_err(|e| GitError::Internal(e.to_string()))
}
pub fn merge_is_in_progress(&self) -> bool {
matches!(
self.repo().state(),
git2::RepositoryState::Merge
| git2::RepositoryState::Revert
| git2::RepositoryState::RevertSequence
| git2::RepositoryState::CherryPick
| git2::RepositoryState::CherryPickSequence
)
}
pub fn mergehead_list(&mut self) -> GitResult<Vec<MergeheadInfo>> {
let mut heads = Vec::new();
self.repo_mut()?
.mergehead_foreach(|oid| {
heads.push(MergeheadInfo {
oid: CommitOid::from_git2(*oid),
});
true
})
.map_err(|e: git2::Error| GitError::Internal(e.to_string()))?;
Ok(heads)
}
pub fn merge_is_conflicted(&self) -> bool {
self.repo()
.index()
.map(|idx| idx.has_conflicts())
.unwrap_or(false)
}
/// Squash all commits from `source_branch` into a single commit on top of `base`.
pub fn squash_commits(&self, base: &CommitOid, source_branch: &str) -> GitResult<CommitOid> {
let base_oid = base
.to_oid()
.map_err(|_| GitError::InvalidOid(base.to_string()))?;
let source_ref = self
.repo()
.find_reference(source_branch)
.map_err(|e| GitError::Internal(e.to_string()))?;
let head_oid = source_ref
.target()
.ok_or_else(|| GitError::Internal("Branch has no target OID".to_string()))?;
// Get the merge base (the common ancestor)
let merge_base = self
.repo()
.merge_base(base_oid, head_oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
// Collect all commits from merge_base (exclusive) to head (inclusive)
let mut revwalk = self
.repo()
.revwalk()
.map_err(|e| GitError::Internal(e.to_string()))?;
revwalk
.push(head_oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
revwalk
.hide(merge_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() {
// Nothing to squash — return base as-is
return Ok(CommitOid::from_git2(base_oid));
}
// Sort commits oldest-first (topological order)
commits.sort_by_key(|c| c.time().seconds());
// Apply all patches onto a temporary tree.
// Strategy: apply patches sequentially using `git2::apply` on the index.
let base_tree = self
.repo()
.find_commit(base_oid)
.map_err(|e| GitError::Internal(e.to_string()))?
.tree()
.map_err(|e| GitError::Internal(e.to_string()))?;
// Build a diff from the accumulated patches
let sig = self
.repo()
.signature()
.map_err(|e| GitError::Internal(e.to_string()))?;
// Apply each commit's diff sequentially to build the squash tree.
let mut current_tree = base_tree;
for commit in &commits {
let commit_tree = commit
.tree()
.map_err(|e| GitError::Internal(e.to_string()))?;
let diff = self
.repo()
.diff_tree_to_tree(Some(&current_tree), Some(&commit_tree), None)
.map_err(|e| GitError::Internal(e.to_string()))?;
// apply_to_tree applies the diff to current_tree, returning a new Index.
let mut new_index = self
.repo()
.apply_to_tree(&current_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()))?;
}
let squash_tree = current_tree;
// Build the squash commit message: list all squashed commits
let mut msg = String::new();
for commit in &commits {
if !msg.is_empty() {
msg.push_str("\n");
}
msg.push_str(&format!("- {}", commit.summary().unwrap_or("(no message)")));
}
// Create the squash commit on top of base
let squash_oid = self
.repo()
.commit(
Some("HEAD"),
&sig,
&sig,
&msg,
&squash_tree,
&[&self
.repo()
.find_commit(base_oid)
.map_err(|e| GitError::Internal(e.to_string()))?],
)
.map_err(|e| GitError::Internal(e.to_string()))?;
Ok(CommitOid::from_git2(squash_oid))
}
}