//! 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 { 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 { 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 { 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 { 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)), } } }