use gix_hash::ObjectId; use gix_object::FindExt; use super::{Complete, Conflict, EmptyBehavior, Error, Options, Outcome, Pick}; /// Replay `picks` onto `onto`, one cherry-pick at a time, and return the new tip. /// /// `picks` are the commits to replay, **oldest first**, each with at most one parent. /// /// `labels` name the sides for conflict markers, and are only observable when `options` are relaxed /// enough to let a marked-up blob through. #[allow(clippy::too_many_arguments)] pub fn replay( onto: &gix_hash::oid, picks: &[ObjectId], committer: &gix_actor::Signature, labels: gix_merge::blob::builtin_driver::text::Labels<'_>, objects: &(impl gix_object::FindObjectOrHeader + gix_object::Write), diff_state: &mut gix_diff::tree::State, diff_resource_cache: &mut gix_diff::blob::Platform, blob_merge: &mut gix_merge::blob::Platform, options: Options, ) -> Result { let Options { mut tree_merge, treat_as_unresolved, empty, } = options; tree_merge.fail_on_conflict = Some(treat_as_unresolved); let empty_tree = ObjectId::empty_tree(onto.kind()); let mut new_parent = onto.to_owned(); let mut new_parent_tree = commit_tree(objects, &new_parent)?; let mut out = Vec::with_capacity(picks.len()); let mut buf = Vec::new(); for pick in picks { let old_commit = objects.find_commit(pick, &mut buf)?.to_owned()?; let old_parent_tree = match old_commit.parents.as_slice() { [] => empty_tree, [one] => commit_tree(objects, one)?, many => { return Err(Error::MergeCommit { id: *pick, parents: many.len(), }); } }; let was_empty = old_parent_tree == old_commit.tree; // When the new parent's tree is already the commit's own base, the merge would have *ours* // equal to the base and could only ever come out as *theirs*, so take that answer directly. let new_tree = if new_parent_tree == old_parent_tree { old_commit.tree } else { let merge = gix_merge::tree( &old_parent_tree, &new_parent_tree, &old_commit.tree, labels, objects, |buf| objects.write_buf(gix_object::Kind::Blob, buf), diff_state, diff_resource_cache, blob_merge, tree_merge.clone(), ) .map_err(|err| Error::TreeMerge(Box::new(err)))?; if merge .conflicts .iter() .any(|conflict| conflict.is_unresolved(treat_as_unresolved)) { return Ok(Outcome::Conflict(Conflict { at: *pick, onto: new_parent, picks: out, conflicts: merge.conflicts, })); } let mut editor = merge.tree; editor .write(|tree| objects.write(tree)) .map_err(Error::WriteTree)? }; // TODO: gate for only one parent when we support `--rebase-merges` (follow jj behavior) let is_empty = new_parent_tree == new_tree; let should_abandon = match empty { EmptyBehavior::Keep => false, EmptyBehavior::AbandonWasEmpty => is_empty && was_empty, EmptyBehavior::AbandonNewlyEmpty => is_empty && !was_empty, EmptyBehavior::AbandonAllEmpty => is_empty, }; if should_abandon { out.push(Pick::Dropped); continue; } let mut rewritten = old_commit; rewritten.tree = new_tree; rewritten.parents = std::iter::once(new_parent).collect(); rewritten.committer = committer.clone(); rewritten .extra_headers .retain(|(name, _)| !crate::STALE_HEADERS.iter().any(|stale| name == stale)); new_parent = objects.write(&rewritten).map_err(Error::WriteCommit)?; new_parent_tree = new_tree; out.push(Pick::Rewritten(new_parent)); } Ok(Outcome::Complete(Complete { tip: new_parent, picks: out, })) } fn commit_tree( objects: &impl gix_object::FindObjectOrHeader, id: &gix_hash::oid, ) -> Result { let mut buf = Vec::new(); Ok(objects.find_commit(id, &mut buf)?.tree()) }