Skip to content

Commit

Permalink
Add baseline tests for tree-merges along with first plain merge.
Browse files Browse the repository at this point in the history
That way, Git can indicate what we need to match, and we have enough infrastructure to perform
simple merges (seemingly) correctly.
  • Loading branch information
Byron committed Oct 17, 2024
1 parent 7af99ad commit d1f1f22
Show file tree
Hide file tree
Showing 10 changed files with 615 additions and 16 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 13 additions & 12 deletions gix-merge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,34 @@ workspace = true
doctest = false

[features]
default = ["blob"]
## Enable diffing of blobs using imara-diff, which also allows for a generic rewrite tracking implementation.
blob = ["dep:imara-diff", "dep:gix-filter", "dep:gix-worktree", "dep:gix-path", "dep:gix-fs", "dep:gix-command", "dep:gix-tempfile", "dep:gix-trace", "dep:gix-quote"]
## Data structures implement `serde::Serialize` and `serde::Deserialize`.
serde = ["dep:serde", "gix-hash/serde", "gix-object/serde"]

[dependencies]
gix-hash = { version = "^0.14.2", path = "../gix-hash" }
gix-object = { version = "^0.44.0", path = "../gix-object" }
gix-filter = { version = "^0.13.0", path = "../gix-filter", optional = true }
gix-worktree = { version = "^0.36.0", path = "../gix-worktree", default-features = false, features = ["attributes"], optional = true }
gix-command = { version = "^0.3.9", path = "../gix-command", optional = true }
gix-path = { version = "^0.10.11", path = "../gix-path", optional = true }
gix-fs = { version = "^0.11.3", path = "../gix-fs", optional = true }
gix-tempfile = { version = "^14.0.0", path = "../gix-tempfile", optional = true }
gix-trace = { version = "^0.1.10", path = "../gix-trace", optional = true }
gix-quote = { version = "^0.4.12", path = "../gix-quote", optional = true }
gix-filter = { version = "^0.13.0", path = "../gix-filter" }
gix-worktree = { version = "^0.36.0", path = "../gix-worktree", default-features = false, features = ["attributes"] }
gix-command = { version = "^0.3.9", path = "../gix-command" }
gix-path = { version = "^0.10.11", path = "../gix-path" }
gix-fs = { version = "^0.11.3", path = "../gix-fs" }
gix-tempfile = { version = "^14.0.0", path = "../gix-tempfile" }
gix-trace = { version = "^0.1.10", path = "../gix-trace" }
gix-quote = { version = "^0.4.12", path = "../gix-quote" }
gix-revision = { version = "^0.29.0", path = "../gix-revision", default-features = false, features = ["merge_base"] }
gix-revwalk = { version = "^0.15.0", path = "../gix-revwalk" }
gix-diff = { version = "^0.46.0", path = "../gix-diff", default-features = false, features = ["blob"] }

thiserror = "1.0.63"
imara-diff = { version = "0.1.7", optional = true }
imara-diff = { version = "0.1.7" }
bstr = { version = "1.5.0", default-features = false }
serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }

document-features = { version = "0.2.0", optional = true }

[dev-dependencies]
gix-testtools = { path = "../tests/tools" }
gix-odb = { path = "../gix-odb" }
pretty_assertions = "1.4.0"

[package.metadata.docs.rs]
Expand Down
105 changes: 105 additions & 0 deletions gix-merge/src/commit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/// The error returned by [`commit()`](crate::commit()).
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error(transparent)]
MergeBase(#[from] gix_revision::merge_base::Error),
#[error(transparent)]
MergeTree(#[from] crate::tree::Error),
#[error("No common ancestor between {our_commit_id} and {their_commit_id}")]
NoMergeBase {
/// The commit on our side that was to be merged.
our_commit_id: gix_hash::ObjectId,
/// The commit on their side that was to be merged.
their_commit_id: gix_hash::ObjectId,
},
#[error("Could not find ancestor, our or their commit to extract tree from")]
FindCommit(#[from] gix_object::find::existing_object::Error),
}

/// A way to configure [`commit()`](crate::commit()).
#[derive(Default, Debug, Copy, Clone)]
pub struct Options {
/// If `true`, merging unrelated commits is allowed, with the merge-base being assumed as empty tree.
pub allow_missing_merge_base: bool,
/// Options to define how trees should be merged.
pub tree_merge: crate::tree::Options,
/// Options to define how to merge blobs.
///
/// Note that these are temporarily overwritten if multiple merge-bases are merged into one.
pub blob_merge: crate::blob::platform::merge::Options,
}

pub(super) mod function {
use crate::commit::{Error, Options};
use gix_object::FindExt;

/// Like [`tree()`](crate::tree()), but it takes only two commits, `our_commit` and `their_commit` to automatically
/// compute the merge-bases among them.
/// If there are multiple merge bases, these will be auto-merged into one, recursively, if
/// [`allow_missing_merge_base`](Options::allow_missing_merge_base) is `true`.
///
/// `labels` are names where [`current`](crate::blob::builtin_driver::text::Labels::current) is a name for `our_commit`
/// and [`other`](crate::blob::builtin_driver::text::Labels::other) is a name for `their_commit`.
/// If [`ancestor`](crate::blob::builtin_driver::text::Labels::ancestor) is unset, it will be set by us based on the
/// merge-bases of `our_commit` and `their_commit`.
///
/// The `graph` is used to find the merge-base between `our_commit` and `their_commit`, and can also act as cache
/// to speed up subsequent merge-base queries.
///
/// ### Performance
///
/// Note that `objects` *should* have an object cache to greatly accelerate tree-retrieval.
pub fn commit<'a>(
our_commit: gix_hash::ObjectId,
their_commit: gix_hash::ObjectId,
mut labels: crate::blob::builtin_driver::text::Labels<'_>,
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: &impl gix_object::FindObjectOrHeader,
options: Options,
) -> Result<crate::tree::Outcome<'a>, Error> {
let merge_bases_commit_ids = gix_revision::merge_base(our_commit, &[their_commit], graph)?;
let (merge_base_commit_id, ancestor_name) = match merge_bases_commit_ids {
Some(base_commit) if base_commit.len() == 1 => (base_commit[0], None),
Some(_base_commits) => {
todo!("merge multiple bases into one");
(our_commit.kind().null(), Some("merged common ancestors".into()))
}
None => {
if options.allow_missing_merge_base {
(
gix_hash::ObjectId::empty_tree(our_commit.kind()),
Some("empty tree".into()),
)
} else {
return Err(Error::NoMergeBase {
our_commit_id: our_commit,
their_commit_id: their_commit,
});
}
}
};
if labels.ancestor.is_none() {
labels.ancestor = ancestor_name;
}

let mut state = gix_diff::tree::State::default();
let merge_base_tree_id = objects.find_commit(&merge_base_commit_id, &mut state.buf1)?.tree();
let our_tree_id = objects.find_commit(&our_commit, &mut state.buf1)?.tree();
let their_tree_id = objects.find_commit(&their_commit, &mut state.buf1)?.tree();

Ok(crate::tree(
&merge_base_tree_id,
&our_tree_id,
&their_tree_id,
labels,
objects,
&mut state,
diff_resource_cache,
blob_merge,
options.tree_merge,
)?)
}
}
7 changes: 6 additions & 1 deletion gix-merge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@
#![forbid(unsafe_code)]

///
#[cfg(feature = "blob")]
pub mod blob;
///
pub mod commit;
pub use commit::function::commit;
///
pub mod tree;
pub use tree::function::tree;
Loading

0 comments on commit d1f1f22

Please sign in to comment.