//! 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 { 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 { let oids: Vec<_> = oids .iter() .map(|o| o.to_oid().map_err(|_| GitError::InvalidOid(o.to_string()))) .collect::, _>>()?; 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 { let oids: Vec<_> = oids .iter() .map(|o| o.to_oid().map_err(|_| GitError::InvalidOid(o.to_string()))) .collect::, _>>()?; 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, ) -> 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, ) -> 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> { 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 { 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 = 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)) } }