/// 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<gix_hash::ObjectId>, /// 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<gix_hash::ObjectId>, graph: &mut gix_revwalk::Graph<'_, '_, gix_revwalk::graph::Commit<gix_revision::merge_base::Flags>>, 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<super::Outcome, crate::commit::Error> { 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<gix_hash::ObjectId, Error> { 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) }}
/// 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<gix_hash::ObjectId>,
/// 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<gix_hash::ObjectId>,
graph: &mut gix_revwalk::Graph<'_, '_, gix_revwalk::graph::Commit<gix_revision::merge_base::Flags>>,
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<super::Outcome, crate::commit::Error> {
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<gix_hash::ObjectId, Error> {
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)
}
}