Skip to content

Commit a876533

Browse files
authored
Merge pull request #1651 from GitoxideLabs/merge
tree-merge follow-up
2 parents 8e99eba + 8d590f3 commit a876533

File tree

25 files changed

+347
-107
lines changed

25 files changed

+347
-107
lines changed

Diff for: README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ What follows is a high-level list of features and those which are planned:
4646
* [x] blob-diff
4747
* [ ] merge
4848
- [x] blobs
49-
- [ ] trees
49+
- [x] trees
5050
- [ ] commits
5151
* [ ] rebase
5252
* [ ] commit

Diff for: gitoxide-core/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ serde = ["gix/serde", "dep:serde_json", "dep:serde", "bytesize/serde"]
4949

5050
[dependencies]
5151
# deselect everything else (like "performance") as this should be controllable by the parent application.
52-
gix = { version = "^0.67.0", path = "../gix", default-features = false, features = ["blob-merge", "blob-diff", "revision", "mailmap", "excludes", "attributes", "worktree-mutation", "credentials", "interrupt", "status", "dirwalk"] }
52+
gix = { version = "^0.67.0", path = "../gix", default-features = false, features = ["merge", "blob-diff", "revision", "mailmap", "excludes", "attributes", "worktree-mutation", "credentials", "interrupt", "status", "dirwalk"] }
5353
gix-pack-for-configuration-only = { package = "gix-pack", version = "^0.54.0", path = "../gix-pack", default-features = false, features = ["pack-cache-lru-dynamic", "pack-cache-lru-static", "generate", "streaming-input"] }
5454
gix-transport-configuration-only = { package = "gix-transport", version = "^0.43.0", path = "../gix-transport", default-features = false }
5555
gix-archive-for-configuration-only = { package = "gix-archive", version = "^0.16.0", path = "../gix-archive", optional = true, features = ["tar", "tar_gz"] }

Diff for: gitoxide-core/src/repository/merge/tree.rs

+9-18
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::OutputFormat;
22

33
pub struct Options {
44
pub format: OutputFormat,
5-
pub resolve_content_merge: Option<gix::merge::blob::builtin_driver::text::Conflict>,
5+
pub file_favor: Option<gix::merge::tree::FileFavor>,
66
pub in_memory: bool,
77
}
88

@@ -12,8 +12,6 @@ pub(super) mod function {
1212
use anyhow::{anyhow, bail, Context};
1313
use gix::bstr::BString;
1414
use gix::bstr::ByteSlice;
15-
use gix::merge::blob::builtin_driver::binary;
16-
use gix::merge::blob::builtin_driver::text::Conflict;
1715
use gix::merge::tree::UnresolvedConflict;
1816
use gix::prelude::Write;
1917

@@ -29,7 +27,7 @@ pub(super) mod function {
2927
theirs: BString,
3028
Options {
3129
format,
32-
resolve_content_merge,
30+
file_favor,
3331
in_memory,
3432
}: Options,
3533
) -> anyhow::Result<()> {
@@ -44,17 +42,7 @@ pub(super) mod function {
4442
let (ours_ref, ours_id) = refname_and_tree(&repo, ours)?;
4543
let (theirs_ref, theirs_id) = refname_and_tree(&repo, theirs)?;
4644

47-
let mut options = repo.tree_merge_options()?;
48-
if let Some(resolve) = resolve_content_merge {
49-
options.blob_merge.text.conflict = resolve;
50-
options.blob_merge.resolve_binary_with = match resolve {
51-
Conflict::Keep { .. } => None,
52-
Conflict::ResolveWithOurs => Some(binary::ResolveWith::Ours),
53-
Conflict::ResolveWithTheirs => Some(binary::ResolveWith::Theirs),
54-
Conflict::ResolveWithUnion => None,
55-
};
56-
}
57-
45+
let options = repo.tree_merge_options()?.with_file_favor(file_favor);
5846
let base_id_str = base_id.to_string();
5947
let ours_id_str = ours_id.to_string();
6048
let theirs_id_str = theirs_id.to_string();
@@ -72,12 +60,15 @@ pub(super) mod function {
7260
.map_or(theirs_id_str.as_str().into(), |n| n.as_bstr())
7361
.into(),
7462
};
75-
let mut res = repo.merge_trees(base_id, ours_id, theirs_id, labels, options)?;
63+
let res = repo.merge_trees(base_id, ours_id, theirs_id, labels, options)?;
64+
let has_conflicts = res.conflicts.is_empty();
65+
let has_unresolved_conflicts = res.has_unresolved_conflicts(UnresolvedConflict::Renames);
7666
{
7767
let _span = gix::trace::detail!("Writing merged tree");
7868
let mut written = 0;
7969
let tree_id = res
8070
.tree
71+
.detach()
8172
.write(|tree| {
8273
written += 1;
8374
repo.write(tree)
@@ -86,10 +77,10 @@ pub(super) mod function {
8677
writeln!(out, "{tree_id} (wrote {written} trees)")?;
8778
}
8879

89-
if !res.conflicts.is_empty() {
80+
if !has_conflicts {
9081
writeln!(err, "{} possibly resolved conflicts", res.conflicts.len())?;
9182
}
92-
if res.has_unresolved_conflicts(UnresolvedConflict::Renames) {
83+
if has_unresolved_conflicts {
9384
bail!("Tree conflicted")
9485
}
9586
Ok(())

Diff for: gix-merge/src/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
//! Provide facilities to merge *blobs*, *trees* and *commits*.
2+
//!
3+
//! * [blob-merges](blob) look at file content.
4+
//! * [tree-merges](mod@tree) look at trees and merge them structurally, triggering blob-merges as needed.
5+
//! * [commit-merges](mod@commit) are like tree merges, but compute or create the merge-base on the fly.
16
#![deny(rust_2018_idioms)]
27
#![forbid(unsafe_code)]
38

Diff for: gix-merge/src/tree/function.rs

+2-25
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ use crate::tree::utils::{
33
to_components, track, unique_path_in_tree, ChangeList, ChangeListRef, PossibleConflict, TrackedChange, TreeNodes,
44
};
55
use crate::tree::ConflictMapping::{Original, Swapped};
6-
use crate::tree::{
7-
Conflict, ConflictMapping, ContentMerge, Error, Options, Outcome, Resolution, ResolutionFailure, UnresolvedConflict,
8-
};
6+
use crate::tree::{Conflict, ConflictMapping, ContentMerge, Error, Options, Outcome, Resolution, ResolutionFailure};
97
use bstr::{BString, ByteSlice};
108
use gix_diff::tree::recorder::Location;
119
use gix_diff::tree_with_rewrites::Change;
@@ -129,7 +127,7 @@ where
129127
let mut failed_on_first_conflict = false;
130128
let mut should_fail_on_conflict = |conflict: Conflict| -> bool {
131129
if let Some(how) = options.fail_on_conflict {
132-
if conflict.resolution.is_err() || is_unresolved(std::slice::from_ref(&conflict), how) {
130+
if conflict.resolution.is_err() || conflict.is_unresolved(how) {
133131
failed_on_first_conflict = true;
134132
}
135133
}
@@ -1002,27 +1000,6 @@ where
10021000
})
10031001
}
10041002

1005-
pub(super) fn is_unresolved(conflicts: &[Conflict], how: UnresolvedConflict) -> bool {
1006-
match how {
1007-
UnresolvedConflict::ConflictMarkers => conflicts.iter().any(|c| {
1008-
c.resolution.is_err()
1009-
|| c.content_merge().map_or(false, |info| {
1010-
matches!(info.resolution, crate::blob::Resolution::Conflict)
1011-
})
1012-
}),
1013-
UnresolvedConflict::Renames => conflicts.iter().any(|c| match &c.resolution {
1014-
Ok(success) => match success {
1015-
Resolution::SourceLocationAffectedByRename { .. }
1016-
| Resolution::OursModifiedTheirsRenamedAndChangedThenRename { .. } => true,
1017-
Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => {
1018-
matches!(merged_blob.resolution, crate::blob::Resolution::Conflict)
1019-
}
1020-
},
1021-
Err(_failure) => true,
1022-
}),
1023-
}
1024-
}
1025-
10261003
fn involves_submodule(a: &EntryMode, b: &EntryMode) -> bool {
10271004
a.is_commit() || b.is_commit()
10281005
}

Diff for: gix-merge/src/tree/mod.rs

+32-9
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub enum Error {
2929
/// The outcome produced by [`tree()`](crate::tree()).
3030
#[derive(Clone)]
3131
pub struct Outcome<'a> {
32-
/// The ready-made (but unwritten) which is the *base* tree, including all non-conflicting changes, and the changes that had
32+
/// The ready-made (but unwritten) *base* tree, including all non-conflicting changes, and the changes that had
3333
/// conflicts which could be resolved automatically.
3434
///
3535
/// This means, if all of their changes were conflicting, this will be equivalent to the *base* tree.
@@ -61,7 +61,7 @@ impl Outcome<'_> {
6161
/// Return `true` if there is any conflict that would still need to be resolved as they would yield undesirable trees.
6262
/// This is based on `how` to determine what should be considered unresolved.
6363
pub fn has_unresolved_conflicts(&self, how: UnresolvedConflict) -> bool {
64-
function::is_unresolved(&self.conflicts, how)
64+
self.conflicts.iter().any(|c| c.is_unresolved(how))
6565
}
6666
}
6767

@@ -70,7 +70,7 @@ impl Outcome<'_> {
7070
#[derive(Debug, Clone)]
7171
pub struct Conflict {
7272
/// A record on how the conflict resolution succeeded with `Ok(_)` or failed with `Err(_)`.
73-
/// In case of `Err(_)`, no write
73+
/// Note that in case of `Err(_)`, edits may still have been made to the tree to aid resolution.
7474
/// On failure, one can examine `ours` and `theirs` to potentially find a custom solution.
7575
/// Note that the descriptions of resolutions or resolution failures may be swapped compared
7676
/// to the actual changes. This is due to changes like `modification|deletion` being treated the
@@ -81,13 +81,17 @@ pub struct Conflict {
8181
pub ours: Change,
8282
/// The change representing *their* side.
8383
pub theirs: Change,
84+
/// Determine how to interpret the `ours` and `theirs` fields. This is used to implement [`Self::changes_in_resolution()`]
85+
/// and [`Self::into_parts_by_resolution()`].
8486
map: ConflictMapping,
8587
}
8688

87-
/// A utility to help define which side is what.
89+
/// A utility to help define which side is what in the [`Conflict`] type.
8890
#[derive(Debug, Clone, Copy)]
8991
enum ConflictMapping {
92+
/// The sides are as described in the field documentation, i.e. `ours` is `ours`.
9093
Original,
94+
/// The sides are the opposite of the field documentation. i.e. `ours` is `theirs` and `theirs` is `ours`.
9195
Swapped,
9296
}
9397

@@ -104,6 +108,28 @@ impl ConflictMapping {
104108
}
105109

106110
impl Conflict {
111+
/// Return `true` if this instance is considered unresolved based on the criterion specified by `how`.
112+
pub fn is_unresolved(&self, how: UnresolvedConflict) -> bool {
113+
match how {
114+
UnresolvedConflict::ConflictMarkers => {
115+
self.resolution.is_err()
116+
|| self.content_merge().map_or(false, |info| {
117+
matches!(info.resolution, crate::blob::Resolution::Conflict)
118+
})
119+
}
120+
UnresolvedConflict::Renames => match &self.resolution {
121+
Ok(success) => match success {
122+
Resolution::SourceLocationAffectedByRename { .. }
123+
| Resolution::OursModifiedTheirsRenamedAndChangedThenRename { .. } => true,
124+
Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => {
125+
matches!(merged_blob.resolution, crate::blob::Resolution::Conflict)
126+
}
127+
},
128+
Err(_failure) => true,
129+
},
130+
}
131+
}
132+
107133
/// Returns the changes of fields `ours` and `theirs` so they match their description in the
108134
/// [`Resolution`] or [`ResolutionFailure`] respectively.
109135
/// Without this, the sides may appear swapped as `ours|theirs` is treated the same as `theirs/ours`
@@ -236,11 +262,8 @@ pub struct Options {
236262
/// The context to use when invoking merge-drivers.
237263
pub blob_merge_command_ctx: gix_command::Context,
238264
/// If `Some(what-is-unresolved)`, the first unresolved conflict will cause the entire merge to stop.
239-
/// This is useful to see if there is any conflict, without performing the whole operation.
240-
// TODO: Maybe remove this if the cost of figuring out conflicts is so low - after all, the data structures
241-
// and initial diff is the expensive thing right now, which are always done upfront.
242-
// However, this could change once we know do everything during the traversal, which probably doesn't work
243-
// without caching stuff and is too complicated to actually do.
265+
/// This is useful to see if there is any conflict, without performing the whole operation, something
266+
/// that can be very relevant during merges that would cause a lot of blob-diffs.
244267
pub fail_on_conflict: Option<UnresolvedConflict>,
245268
/// This value also affects the size of merge-conflict markers, to allow differentiating
246269
/// merge conflicts on each level, for any value greater than 0, with values `N` causing `N*2`

Diff for: gix/Cargo.toml

+4-4
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,11 @@ extras = [
6565
"interrupt",
6666
"status",
6767
"dirwalk",
68-
"blob-merge"
6968
]
7069

7170
## A collection of features that need a larger MSRV, and thus are disabled by default.
72-
need-more-recent-msrv = ["tree-editor"]
71+
## * `blob-merge` should be in extras, but needs `tree-editor` for convenience.
72+
need-more-recent-msrv = ["merge", "tree-editor"]
7373

7474
## Various progress-related features that improve the look of progress message units.
7575
comfort = [
@@ -139,7 +139,7 @@ revparse-regex = ["regex", "revision"]
139139
blob-diff = ["gix-diff/blob", "attributes"]
140140

141141
## Add functions to specifically merge files, using the standard three-way merge that git offers.
142-
blob-merge = ["blob-diff", "dep:gix-merge", "attributes"]
142+
merge = ["tree-editor", "blob-diff", "dep:gix-merge", "attributes"]
143143

144144
## Make it possible to turn a tree into a stream of bytes, which can be decoded to entries and turned into various other formats.
145145
worktree-stream = ["gix-worktree-stream", "attributes"]
@@ -399,7 +399,7 @@ document-features = { version = "0.2.0", optional = true }
399399

400400
[dev-dependencies]
401401
# For additional features that aren't enabled by default due to MSRV
402-
gix = { path = ".", default-features = false, features = ["tree-editor"] }
402+
gix = { path = ".", default-features = false, features = ["need-more-recent-msrv"] }
403403
pretty_assertions = "1.4.0"
404404
gix-testtools = { path = "../tests/tools" }
405405
is_ci = "1.1.1"

Diff for: gix/src/config/cache/access.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ impl Cache {
100100
Ok(out)
101101
}
102102

103-
#[cfg(feature = "blob-merge")]
103+
#[cfg(feature = "merge")]
104104
pub(crate) fn merge_drivers(&self) -> Result<Vec<gix_merge::blob::Driver>, config::merge::drivers::Error> {
105105
let mut out = Vec::<gix_merge::blob::Driver>::new();
106106
for section in self
@@ -136,7 +136,7 @@ impl Cache {
136136
Ok(out)
137137
}
138138

139-
#[cfg(feature = "blob-merge")]
139+
#[cfg(feature = "merge")]
140140
pub(crate) fn merge_pipeline_options(
141141
&self,
142142
) -> Result<gix_merge::blob::pipeline::Options, config::merge::pipeline_options::Error> {

Diff for: gix/src/config/tree/sections/merge.rs

+7-7
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ impl Merge {
1515
"The limit is actually squared, so 1000 stands for up to 1 million diffs if fuzzy rename tracking is enabled",
1616
);
1717
/// The `merge.renames` key.
18-
#[cfg(feature = "blob-merge")]
18+
#[cfg(feature = "merge")]
1919
pub const RENAMES: super::diff::Renames = super::diff::Renames::new_renames("renames", &config::Tree::MERGE);
2020
/// The `merge.renormalize` key
2121
pub const RENORMALIZE: keys::Boolean = keys::Boolean::new_boolean("renormalize", &Tree::MERGE);
@@ -31,7 +31,7 @@ impl Merge {
3131
pub const DRIVER_RECURSIVE: keys::String = keys::String::new_string("recursive", &config::Tree::MERGE)
3232
.with_subsection_requirement(Some(SubSectionRequirement::Parameter("driver")));
3333
/// The `merge.conflictStyle` key.
34-
#[cfg(feature = "blob-merge")]
34+
#[cfg(feature = "merge")]
3535
pub const CONFLICT_STYLE: ConflictStyle =
3636
ConflictStyle::new_with_validate("conflictStyle", &config::Tree::MERGE, validate::ConflictStyle);
3737
}
@@ -44,24 +44,24 @@ impl Section for Merge {
4444
fn keys(&self) -> &[&dyn Key] {
4545
&[
4646
&Self::RENAME_LIMIT,
47-
#[cfg(feature = "blob-merge")]
47+
#[cfg(feature = "merge")]
4848
&Self::RENAMES,
4949
&Self::RENORMALIZE,
5050
&Self::DEFAULT,
5151
&Self::DRIVER_NAME,
5252
&Self::DRIVER_COMMAND,
5353
&Self::DRIVER_RECURSIVE,
54-
#[cfg(feature = "blob-merge")]
54+
#[cfg(feature = "merge")]
5555
&Self::CONFLICT_STYLE,
5656
]
5757
}
5858
}
5959

6060
/// The `merge.conflictStyle` key.
61-
#[cfg(feature = "blob-merge")]
61+
#[cfg(feature = "merge")]
6262
pub type ConflictStyle = keys::Any<validate::ConflictStyle>;
6363

64-
#[cfg(feature = "blob-merge")]
64+
#[cfg(feature = "merge")]
6565
mod conflict_style {
6666
use crate::{bstr::BStr, config, config::tree::sections::merge::ConflictStyle};
6767
use gix_merge::blob::builtin_driver::text;
@@ -87,7 +87,7 @@ mod conflict_style {
8787
}
8888
}
8989

90-
#[cfg(feature = "blob-merge")]
90+
#[cfg(feature = "merge")]
9191
mod validate {
9292
use crate::{
9393
bstr::BStr,

Diff for: gix/src/lib.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,6 @@ pub use gix_ignore as ignore;
120120
#[cfg(feature = "index")]
121121
pub use gix_index as index;
122122
pub use gix_lock as lock;
123-
#[cfg(feature = "blob-merge")]
124-
pub use gix_merge as merge;
125123
#[cfg(feature = "credentials")]
126124
pub use gix_negotiate as negotiate;
127125
pub use gix_object as objs;
@@ -204,6 +202,10 @@ pub mod push;
204202
///
205203
pub mod diff;
206204

205+
///
206+
#[cfg(feature = "merge")]
207+
pub mod merge;
208+
207209
/// See [`ThreadSafeRepository::discover()`], but returns a [`Repository`] instead.
208210
///
209211
/// # Note

0 commit comments

Comments
 (0)