/// The outcome produced by [`commit::merge_base()`](crate::commit::virtual_merge_base()). pub struct Outcome { /// The commit ids of all the virtual merge bases we have produced in the process of recursively merging the merge-bases. /// As they have been written to the object database, they are still available until they are garbage collected. /// The last one is the most recently produced and the one returned as `commit_id`. /// This is never empty. pub virtual_merge_bases: nonempty::NonEmpty, /// The id of the commit that was created to hold the merged tree. pub commit_id: gix_hash::ObjectId, /// The hash of the merged tree. pub tree_id: gix_hash::ObjectId, } /// The error returned by [`commit::merge_base()`](crate::commit::virtual_merge_base()). #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] pub enum Error { #[error(transparent)] MergeTree(#[from] crate::tree::Error), #[error("Failed to write tree for merged merge-base or virtual commit")] WriteObject(gix_object::write::Error), #[error("Failed to decode a commit needed to build a virtual merge-base")] DecodeCommit(#[from] gix_object::decode::Error), #[error( "Conflicts occurred when trying to resolve multiple merge-bases by merging them. This is most certainly a bug." )] VirtualMergeBaseConflict, #[error("Could not find commit to use as basis for a virtual commit")] FindCommit(#[from] gix_object::find::existing_object::Error), } pub(super) mod function { use gix_object::FindExt; use super::Error; use crate::{ blob::builtin_driver, tree::{TreatAsUnresolved, treat_as_unresolved}, }; /// Create a single virtual merge-base by merging `first_commit`, `second_commit` and `others` into one. /// Note that `first_commit` and `second_commit` are expected to have been popped off `others`, so `first_commit` /// was the last provided merge-base of function that provides multiple merge-bases for a pair of commits. /// /// The parameters `graph`, `diff_resource_cache`, `blob_merge`, `objects`, `abbreviate_hash` and `options` are passed /// directly to [`tree()`](crate::tree()) for merging the trees of two merge-bases at a time. /// Note that most of `options` are overwritten to match the requirements of a merge-base merge. #[expect(clippy::too_many_arguments)] pub fn virtual_merge_base<'objects>( first_commit: gix_hash::ObjectId, second_commit: gix_hash::ObjectId, mut others: Vec, graph: &mut gix_revwalk::Graph<'_, '_, gix_revwalk::graph::Commit>, diff_resource_cache: &mut gix_diff::blob::Platform, blob_merge: &mut crate::blob::Platform, objects: &'objects (impl gix_object::FindObjectOrHeader + gix_object::Write), abbreviate_hash: &mut dyn FnMut(&gix_hash::oid) -> String, mut options: crate::tree::Options, ) -> Result { let mut merged_commit_id = first_commit; others.push(second_commit); options.tree_conflicts = Some(crate::tree::ResolveWith::Ancestor); options.blob_merge.is_virtual_ancestor = true; options.blob_merge.text.conflict = builtin_driver::text::Conflict::ResolveWithOurs; let favor_ancestor = Some(builtin_driver::binary::ResolveWith::Ancestor); options.blob_merge.resolve_binary_with = favor_ancestor; options.symlink_conflicts = favor_ancestor; let labels = builtin_driver::text::Labels { current: Some("Temporary merge branch 1".into()), other: Some("Temporary merge branch 2".into()), ancestor: None, }; let mut virtual_merge_bases = Vec::new(); let mut tree_id = None; while let Some(next_commit_id) = others.pop() { options.marker_size_multiplier += 1; let mut out = crate::commit( merged_commit_id, next_commit_id, labels, graph, diff_resource_cache, blob_merge, objects, abbreviate_hash, crate::commit::Options { allow_missing_merge_base: false, tree_merge: options.clone(), use_first_merge_base: false, }, )?; // This shouldn't happen, but if for some buggy reason it does, we rather bail. if out.tree_merge.has_unresolved_conflicts(TreatAsUnresolved { content_merge: treat_as_unresolved::ContentMerge::Markers, tree_merge: treat_as_unresolved::TreeMerge::Undecidable, }) { return Err(Error::VirtualMergeBaseConflict.into()); } let merged_tree_id = out .tree_merge .tree .write(|tree| objects.write(tree)) .map_err(Error::WriteObject)?; tree_id = Some(merged_tree_id); merged_commit_id = create_virtual_commit(objects, merged_commit_id, next_commit_id, merged_tree_id)?; virtual_merge_bases.extend(out.virtual_merge_bases); virtual_merge_bases.push(merged_commit_id); } Ok(super::Outcome { virtual_merge_bases: nonempty::NonEmpty::from_vec(virtual_merge_bases) .expect("the virtual merge-base process always creates at least one commit"), commit_id: merged_commit_id, tree_id: tree_id.map_or_else( || { let mut buf = Vec::new(); objects.find_commit(&merged_commit_id, &mut buf).map(|c| c.tree()) }, Ok, )?, }) } fn create_virtual_commit( objects: &(impl gix_object::Find + gix_object::Write), parent_a: gix_hash::ObjectId, parent_b: gix_hash::ObjectId, tree_id: gix_hash::ObjectId, ) -> Result { let mut buf = Vec::new(); let commit_ref = objects.find_commit(&parent_a, &mut buf)?; let mut commit = commit_ref.to_owned()?; commit.parents = vec![parent_a, parent_b].into(); commit.tree = tree_id; objects.write(&commit).map_err(Error::WriteObject) } }