|
| 1 | +/// The error returned by [`commit()`](crate::commit()). |
| 2 | +#[derive(Debug, thiserror::Error)] |
| 3 | +#[allow(missing_docs)] |
| 4 | +pub enum Error { |
| 5 | + #[error(transparent)] |
| 6 | + MergeBase(#[from] gix_revision::merge_base::Error), |
| 7 | + #[error(transparent)] |
| 8 | + MergeTree(#[from] crate::tree::Error), |
| 9 | + #[error("Failed to write tree for merged merge-base or virtual commit")] |
| 10 | + WriteObject(gix_object::write::Error), |
| 11 | + #[error("No common ancestor between {our_commit_id} and {their_commit_id}")] |
| 12 | + NoMergeBase { |
| 13 | + /// The commit on our side that was to be merged. |
| 14 | + our_commit_id: gix_hash::ObjectId, |
| 15 | + /// The commit on their side that was to be merged. |
| 16 | + their_commit_id: gix_hash::ObjectId, |
| 17 | + }, |
| 18 | + #[error("Could not find ancestor, our or their commit to extract tree from")] |
| 19 | + FindCommit(#[from] gix_object::find::existing_object::Error), |
| 20 | +} |
| 21 | + |
| 22 | +/// A way to configure [`commit()`](crate::commit()). |
| 23 | +#[derive(Default, Debug, Clone)] |
| 24 | +pub struct Options { |
| 25 | + /// If `true`, merging unrelated commits is allowed, with the merge-base being assumed as empty tree. |
| 26 | + pub allow_missing_merge_base: bool, |
| 27 | + /// Options to define how trees should be merged. |
| 28 | + pub tree_merge: crate::tree::Options, |
| 29 | + /// If `true`, do not merge multiple merge-bases into one. Instead, just use the first one. |
| 30 | + // TODO: test |
| 31 | + #[doc(alias = "no_recursive", alias = "git2")] |
| 32 | + pub use_first_merge_base: bool, |
| 33 | +} |
| 34 | + |
| 35 | +/// The result of [`commit()`](crate::commit()). |
| 36 | +#[derive(Clone)] |
| 37 | +pub struct Outcome<'a> { |
| 38 | + /// The outcome of the actual tree-merge. |
| 39 | + pub tree_merge: crate::tree::Outcome<'a>, |
| 40 | + /// The tree id of the base commit we used. This is either… |
| 41 | + /// * the single merge-base we found |
| 42 | + /// * the first of multiple merge-bases if [`use_first_merge_base`](Options::use_first_merge_base) was `true`. |
| 43 | + /// * the merged tree of all merge-bases, which then isn't linked to an actual commit. |
| 44 | + /// * an empty tree, if [`allow_missing_merge_base`](Options::allow_missing_merge_base) is enabled. |
| 45 | + pub merge_base_tree_id: gix_hash::ObjectId, |
| 46 | + /// The object ids of all the commits which were found to be merge-bases, or `None` if there was no merge-base. |
| 47 | + pub merge_bases: Option<Vec<gix_hash::ObjectId>>, |
| 48 | + /// A list of virtual commits that were created to merge multiple merge-bases into one. |
| 49 | + /// As they are not reachable by anything they will be garbage collected, but knowing them provides options. |
| 50 | + pub virtual_merge_bases: Vec<gix_hash::ObjectId>, |
| 51 | +} |
| 52 | + |
| 53 | +pub(super) mod function { |
| 54 | + use crate::commit::{Error, Options}; |
| 55 | + use gix_object::FindExt; |
| 56 | + use std::borrow::Cow; |
| 57 | + |
| 58 | + /// Like [`tree()`](crate::tree()), but it takes only two commits, `our_commit` and `their_commit` to automatically |
| 59 | + /// compute the merge-bases among them. |
| 60 | + /// If there are multiple merge bases, these will be auto-merged into one, recursively, if |
| 61 | + /// [`allow_missing_merge_base`](Options::allow_missing_merge_base) is `true`. |
| 62 | + /// |
| 63 | + /// `labels` are names where [`current`](crate::blob::builtin_driver::text::Labels::current) is a name for `our_commit` |
| 64 | + /// and [`other`](crate::blob::builtin_driver::text::Labels::other) is a name for `their_commit`. |
| 65 | + /// If [`ancestor`](crate::blob::builtin_driver::text::Labels::ancestor) is unset, it will be set by us based on the |
| 66 | + /// merge-bases of `our_commit` and `their_commit`. |
| 67 | + /// |
| 68 | + /// The `graph` is used to find the merge-base between `our_commit` and `their_commit`, and can also act as cache |
| 69 | + /// to speed up subsequent merge-base queries. |
| 70 | + /// |
| 71 | + /// Use `abbreviate_hash(id)` to shorten the given `id` according to standard git shortening rules. It's used in case |
| 72 | + /// the ancestor-label isn't explicitly set so that the merge base label becomes the shortened `id`. |
| 73 | + /// Note that it's a dyn closure only to make it possible to recursively call this function in case of multiple merge-bases. |
| 74 | + /// |
| 75 | + /// `write_object` is used only if it's allowed to merge multiple merge-bases into one, and if there |
| 76 | + /// are multiple merge bases, and to write merged buffers as blobs. |
| 77 | + /// |
| 78 | + /// ### Performance |
| 79 | + /// |
| 80 | + /// Note that `objects` *should* have an object cache to greatly accelerate tree-retrieval. |
| 81 | + /// |
| 82 | + /// ### Notes |
| 83 | + /// |
| 84 | + /// When merging merge-bases recursively, the options are adjusted automatically to act like Git, i.e. merge binary |
| 85 | + /// blobs and resolve with *ours*. |
| 86 | + /// |
| 87 | + /// ### Deviation |
| 88 | + /// |
| 89 | + /// * It's known that certain conflicts around symbolic links can be auto-resolved. We don't have an option for this |
| 90 | + /// at all, yet, primarily as Git seems to not implement the *ours*/*theirs* choice in other places even though it |
| 91 | + /// reasonably could. So we leave it to the caller to continue processing the returned tree at will. |
| 92 | + /// * Git treats symbolic links, when merged, like binaries choosing one over the other, which is also affected by |
| 93 | + /// which side is chosen. In our case, they always conflict. TODO: fix this, with custom merge-strategy for symlinks. |
| 94 | + #[allow(clippy::too_many_arguments)] |
| 95 | + pub fn commit<'objects>( |
| 96 | + our_commit: gix_hash::ObjectId, |
| 97 | + their_commit: gix_hash::ObjectId, |
| 98 | + labels: crate::blob::builtin_driver::text::Labels<'_>, |
| 99 | + graph: &mut gix_revwalk::Graph<'_, '_, gix_revwalk::graph::Commit<gix_revision::merge_base::Flags>>, |
| 100 | + diff_resource_cache: &mut gix_diff::blob::Platform, |
| 101 | + blob_merge: &mut crate::blob::Platform, |
| 102 | + objects: &'objects (impl gix_object::FindObjectOrHeader + gix_object::Write), |
| 103 | + abbreviate_hash: &mut dyn FnMut(&gix_hash::oid) -> String, |
| 104 | + options: Options, |
| 105 | + ) -> Result<super::Outcome<'objects>, Error> { |
| 106 | + let merge_bases = gix_revision::merge_base(our_commit, &[their_commit], graph)?; |
| 107 | + let mut virtual_merge_bases = Vec::new(); |
| 108 | + let mut state = gix_diff::tree::State::default(); |
| 109 | + let mut commit_to_tree = |
| 110 | + |commit_id: gix_hash::ObjectId| objects.find_commit(&commit_id, &mut state.buf1).map(|c| c.tree()); |
| 111 | + |
| 112 | + let (merge_base_tree_id, ancestor_name): (_, Cow<'_, str>) = match merge_bases.clone() { |
| 113 | + Some(base_commit) if base_commit.len() == 1 => { |
| 114 | + (commit_to_tree(base_commit[0])?, abbreviate_hash(&base_commit[0]).into()) |
| 115 | + } |
| 116 | + Some(mut base_commits) => { |
| 117 | + let virtual_base_tree = if options.use_first_merge_base { |
| 118 | + let first = *base_commits.first().expect("if Some() there is at least one."); |
| 119 | + commit_to_tree(first)? |
| 120 | + } else { |
| 121 | + let mut merged_commit_id = base_commits.pop().expect("at least one base"); |
| 122 | + let mut options = options.clone(); |
| 123 | + options.tree_merge.blob_merge.is_virtual_ancestor = true; |
| 124 | + options.tree_merge.blob_merge.resolve_binary_with = |
| 125 | + Some(crate::blob::builtin_driver::binary::ResolveWith::Ours); |
| 126 | + let labels = crate::blob::builtin_driver::text::Labels { |
| 127 | + current: Some("Temporary merge branch 1".into()), |
| 128 | + other: Some("Temporary merge branch 2".into()), |
| 129 | + ..labels |
| 130 | + }; |
| 131 | + while let Some(next_commit_id) = base_commits.pop() { |
| 132 | + options.tree_merge.call_depth += 1; |
| 133 | + let mut out = commit( |
| 134 | + merged_commit_id, |
| 135 | + next_commit_id, |
| 136 | + labels, |
| 137 | + graph, |
| 138 | + diff_resource_cache, |
| 139 | + blob_merge, |
| 140 | + objects, |
| 141 | + abbreviate_hash, |
| 142 | + options.clone(), |
| 143 | + )?; |
| 144 | + let merged_tree_id = out |
| 145 | + .tree_merge |
| 146 | + .tree |
| 147 | + .write(|tree| objects.write(tree)) |
| 148 | + .map_err(Error::WriteObject)?; |
| 149 | + |
| 150 | + merged_commit_id = |
| 151 | + create_virtual_commit(objects, merged_commit_id, next_commit_id, merged_tree_id)?; |
| 152 | + |
| 153 | + virtual_merge_bases.extend(out.virtual_merge_bases); |
| 154 | + virtual_merge_bases.push(merged_commit_id); |
| 155 | + } |
| 156 | + commit_to_tree(merged_commit_id)? |
| 157 | + }; |
| 158 | + (virtual_base_tree, "merged common ancestors".into()) |
| 159 | + } |
| 160 | + None => { |
| 161 | + if options.allow_missing_merge_base { |
| 162 | + (gix_hash::ObjectId::empty_tree(our_commit.kind()), "empty tree".into()) |
| 163 | + } else { |
| 164 | + return Err(Error::NoMergeBase { |
| 165 | + our_commit_id: our_commit, |
| 166 | + their_commit_id: their_commit, |
| 167 | + }); |
| 168 | + } |
| 169 | + } |
| 170 | + }; |
| 171 | + |
| 172 | + let mut labels = labels; // TODO(borrowchk): this re-assignment shouldn't be needed. |
| 173 | + if labels.ancestor.is_none() { |
| 174 | + labels.ancestor = Some(ancestor_name.as_ref().into()); |
| 175 | + } |
| 176 | + |
| 177 | + let our_tree_id = objects.find_commit(&our_commit, &mut state.buf1)?.tree(); |
| 178 | + let their_tree_id = objects.find_commit(&their_commit, &mut state.buf1)?.tree(); |
| 179 | + |
| 180 | + let outcome = crate::tree( |
| 181 | + &merge_base_tree_id, |
| 182 | + &our_tree_id, |
| 183 | + &their_tree_id, |
| 184 | + labels, |
| 185 | + objects, |
| 186 | + |buf| objects.write_buf(gix_object::Kind::Blob, buf), |
| 187 | + &mut state, |
| 188 | + diff_resource_cache, |
| 189 | + blob_merge, |
| 190 | + options.tree_merge, |
| 191 | + )?; |
| 192 | + |
| 193 | + Ok(super::Outcome { |
| 194 | + tree_merge: outcome, |
| 195 | + merge_bases, |
| 196 | + merge_base_tree_id, |
| 197 | + virtual_merge_bases, |
| 198 | + }) |
| 199 | + } |
| 200 | + |
| 201 | + fn create_virtual_commit( |
| 202 | + objects: &(impl gix_object::Find + gix_object::Write), |
| 203 | + parent_a: gix_hash::ObjectId, |
| 204 | + parent_b: gix_hash::ObjectId, |
| 205 | + tree_id: gix_hash::ObjectId, |
| 206 | + ) -> Result<gix_hash::ObjectId, Error> { |
| 207 | + let mut buf = Vec::new(); |
| 208 | + let mut commit: gix_object::Commit = objects.find_commit(&parent_a, &mut buf)?.into(); |
| 209 | + commit.parents = vec![parent_a, parent_b].into(); |
| 210 | + commit.tree = tree_id; |
| 211 | + objects.write(&commit).map_err(Error::WriteObject) |
| 212 | + } |
| 213 | +} |
0 commit comments