gitdataai/libs/git/branch/merge.rs
2026-04-15 09:08:09 +08:00

151 lines
4.9 KiB
Rust

//! Branch merge operations.
use crate::commit::types::CommitOid;
use crate::{GitDomain, GitError, GitResult};
impl GitDomain {
pub fn branch_is_merged(&self, branch: &str, into: &str) -> GitResult<bool> {
let branch_oid = self
.branch_target(branch)?
.ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", branch)))?
.to_oid()?;
let into_oid = self
.branch_target(into)?
.ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", into)))?
.to_oid()?;
self.repo
.graph_ahead_behind(into_oid, branch_oid)
.map(|(ahead, _)| ahead == 0)
.map_err(|e| GitError::Internal(e.to_string()))
}
pub fn branch_merge_base(&self, branch1: &str, branch2: &str) -> GitResult<CommitOid> {
let oid1 = self
.branch_target(branch1)?
.ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", branch1)))?
.to_oid()?;
let oid2 = self
.branch_target(branch2)?
.ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", branch2)))?
.to_oid()?;
let base = self
.repo()
.merge_base(oid1, oid2)
.map_err(|e| GitError::Internal(e.to_string()))?;
Ok(CommitOid::from_git2(base))
}
pub fn branch_is_ancestor(&self, child: &str, ancestor: &str) -> GitResult<bool> {
let child_oid = self
.branch_target(child)?
.ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", child)))?
.to_oid()?;
let ancestor_oid = self
.branch_target(ancestor)?
.ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", ancestor)))?
.to_oid()?;
self.repo
.graph_ahead_behind(child_oid, ancestor_oid)
.map(|(_, behind)| behind == 0)
.map_err(|e| GitError::Internal(e.to_string()))
}
pub fn branch_fast_forward(&self, target: &str, force: bool) -> GitResult<CommitOid> {
let target_oid = self
.branch_target(target)?
.ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", target)))?
.to_oid()?;
let head = self
.repo()
.head()
.ok()
.ok_or_else(|| GitError::Internal("HEAD is not attached".to_string()))?;
let head_oid = head
.target()
.ok_or_else(|| GitError::Internal("HEAD has no target".to_string()))?;
let head_name = head
.name()
.ok_or_else(|| GitError::Internal("HEAD has no name".to_string()))?;
let ref_name = if head_name.starts_with("refs/") {
head_name.to_string()
} else {
format!("refs/heads/{}", head_name)
};
let (ahead, behind) = self
.repo()
.graph_ahead_behind(head_oid, target_oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
if behind == 0 {
return Ok(CommitOid::from_git2(head_oid));
}
if !force && ahead > 0 {
// ahead > 0 && behind > 0 means diverged; ahead == 0 && behind > 0 is valid FF
return Err(GitError::Internal(
"not a fast-forward: branches have diverged".to_string(),
));
}
self.repo
.reference_matching(
ref_name.as_str(),
target_oid,
true,
head_oid,
"fast-forward",
)
.map_err(|e| GitError::Internal(e.to_string()))?;
self.repo
.set_head_detached(target_oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
self.repo
.checkout_tree(self.repo.find_commit(target_oid)?.as_object(), None)
.map_err(|e| GitError::Internal(e.to_string()))?;
Ok(CommitOid::from_git2(target_oid))
}
pub fn branch_abort_merge(&self) -> GitResult<()> {
let head_oid = self
.repo()
.head()
.ok()
.and_then(|r| r.target())
.ok_or_else(|| GitError::Internal("HEAD is not attached".to_string()))?;
let obj = self
.repo()
.find_object(head_oid, None)
.map_err(|e| GitError::Internal(e.to_string()))?;
self.repo
.reset(&obj, git2::ResetType::Hard, None)
.map_err(|e| GitError::Internal(e.to_string()))
}
pub fn branch_is_conflicted(&self) -> bool {
self.repo
.index()
.map(|idx| idx.has_conflicts())
.unwrap_or(false)
}
pub fn branch_tracking_difference(&self, name: &str) -> GitResult<(usize, usize)> {
let upstream = self.branch_upstream(name)?;
match upstream {
Some(u) => self.branch_ahead_behind(name, &u.name),
None => Ok((0, 0)),
}
}
}