346 lines
11 KiB
Rust
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(¤t_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(¤t_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))
|
|
}
|
|
}
|