Skip to content

Commit b96d11f

Browse files
committed
Sketch the entire API surface to capture all parts of blob-merges
1 parent 865282f commit b96d11f

File tree

8 files changed

+1240
-0
lines changed

8 files changed

+1240
-0
lines changed

Cargo.lock

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-diff/src/blob/platform.rs

+1
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ impl Platform {
383383
///
384384
/// If one of the resources is binary, the operation reports an error as such resources don't make their data available
385385
/// which is required for the external diff to run.
386+
// TODO: fix this - the diff shouldn't fail if binary (or large) files are used, just copy them into tempfiles.
386387
pub fn prepare_diff_command(
387388
&self,
388389
diff_command: BString,

gix-merge/Cargo.toml

+26
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,31 @@ workspace = true
1414
[lib]
1515
doctest = false
1616

17+
[features]
18+
default = ["blob"]
19+
## Enable diffing of blobs using imara-diff, which also allows for a generic rewrite tracking implementation.
20+
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"]
21+
## Data structures implement `serde::Serialize` and `serde::Deserialize`.
22+
serde = ["dep:serde", "gix-hash/serde", "gix-object/serde"]
23+
1724
[dependencies]
25+
gix-hash = { version = "^0.14.2", path = "../gix-hash" }
26+
gix-object = { version = "^0.44.0", path = "../gix-object" }
27+
gix-filter = { version = "^0.13.0", path = "../gix-filter", optional = true }
28+
gix-worktree = { version = "^0.36.0", path = "../gix-worktree", default-features = false, features = ["attributes"], optional = true }
29+
gix-command = { version = "^0.3.9", path = "../gix-command", optional = true }
30+
gix-path = { version = "^0.10.11", path = "../gix-path", optional = true }
31+
gix-fs = { version = "^0.11.3", path = "../gix-fs", optional = true }
32+
gix-tempfile = { version = "^14.0.0", path = "../gix-tempfile", optional = true }
33+
gix-trace = { version = "^0.1.10", path = "../gix-trace", optional = true }
34+
35+
thiserror = "1.0.63"
36+
imara-diff = { version = "0.1.7", optional = true }
37+
bstr = { version = "1.5.0", default-features = false }
38+
serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }
39+
40+
document-features = { version = "0.2.0", optional = true }
1841

42+
[package.metadata.docs.rs]
43+
all-features = true
44+
features = ["document-features"]

gix-merge/src/blob/builtin_driver.rs

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
use crate::blob::BuiltinDriver;
2+
3+
impl BuiltinDriver {
4+
/// Return the name of this instance.
5+
pub fn as_str(&self) -> &str {
6+
match self {
7+
BuiltinDriver::Text => "text",
8+
BuiltinDriver::Binary => "binary",
9+
BuiltinDriver::Union => "union",
10+
}
11+
}
12+
13+
/// Get all available built-in drivers.
14+
pub fn all() -> &'static [Self] {
15+
&[BuiltinDriver::Text, BuiltinDriver::Binary, BuiltinDriver::Union]
16+
}
17+
18+
/// Try to match one of our variants to `name`, case-sensitive, and return its instance.
19+
pub fn by_name(name: &str) -> Option<Self> {
20+
Self::all().iter().find(|variant| variant.as_str() == name).copied()
21+
}
22+
}
23+
24+
///
25+
pub mod binary {
26+
use crate::blob::Resolution;
27+
28+
/// What to do when having to pick a side to resolve a conflict.
29+
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
30+
pub enum ResolveWith {
31+
/// Chose the ancestor to resolve a conflict.
32+
Ancestor,
33+
/// Chose our side to resolve a conflict.
34+
Ours,
35+
/// Chose their side to resolve a conflict.
36+
Theirs,
37+
}
38+
39+
/// Tell the caller of [`merge()`] which side was picked
40+
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
41+
pub enum Pick {
42+
/// Chose the ancestor.
43+
Ancestor,
44+
/// Chose our side.
45+
Ours,
46+
/// Chose their side.
47+
Theirs,
48+
}
49+
50+
/// As this algorithm doesn't look at the actual data, it returns a choice solely based on logic.
51+
///
52+
/// It always results in a conflict with `current` being picked unless `on_conflict` is not `None`.
53+
pub fn merge(on_conflict: Option<ResolveWith>) -> (Pick, Resolution) {
54+
match on_conflict {
55+
None => (Pick::Ours, Resolution::Conflict),
56+
Some(ResolveWith::Ours) => (Pick::Ours, Resolution::Complete),
57+
Some(ResolveWith::Theirs) => (Pick::Theirs, Resolution::Complete),
58+
Some(ResolveWith::Ancestor) => (Pick::Ancestor, Resolution::Complete),
59+
}
60+
}
61+
}
62+
63+
///
64+
pub mod text {
65+
use crate::blob::Resolution;
66+
67+
/// The way the built-in [text driver](crate::blob::BuiltinDriver::Text) will express
68+
/// merge conflicts in the resulting file.
69+
#[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
70+
pub enum ConflictStyle {
71+
/// Only show the zealously minified conflicting lines of the local changes and the incoming (other) changes,
72+
/// hiding the base version entirely.
73+
///
74+
/// ```
75+
/// line1-changed-by-both
76+
/// <<<<<<< local
77+
/// line2-to-be-changed-in-incoming
78+
/// =======
79+
/// line2-changed
80+
/// >>>>>>> incoming
81+
///```
82+
#[default]
83+
Merge,
84+
/// Show non-minimized hunks of local changes, the base, and the incoming (other) changes.
85+
///
86+
/// This mode does not hide any information.
87+
/// ```
88+
/// <<<<<<< local
89+
/// line1-changed-by-both
90+
/// line2-to-be-changed-in-incoming
91+
/// ||||||| 9a8d80c
92+
/// line1-to-be-changed-by-both
93+
/// line2-to-be-changed-in-incoming
94+
/// =======
95+
/// line1-changed-by-both
96+
/// line2-changed
97+
/// >>>>>>> incoming
98+
///```
99+
Diff3,
100+
/// Like [`Diff3](Self::Diff3), but will show *minimized* hunks of local change and the incoming (other) changes,
101+
/// as well as non-minimized hunks of the base.
102+
///
103+
/// ```
104+
/// line1-changed-by-both
105+
/// <<<<<<< local
106+
/// line2-to-be-changed-in-incoming
107+
/// ||||||| 9a8d80c
108+
/// line1-to-be-changed-by-both
109+
/// line2-to-be-changed-in-incoming
110+
/// =======
111+
/// line2-changed
112+
/// >>>>>>> incoming
113+
/// ```
114+
ZealousDiff3,
115+
}
116+
117+
/// Options for the builtin [text driver](crate::blob::BuiltinDriver::Text).
118+
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
119+
pub struct Options {
120+
/// How to visualize conflicts in merged files.
121+
pub conflict_style: ConflictStyle,
122+
/// The amount of markers to draw, defaults to 7, i.e. `<<<<<<<`
123+
pub marker_size: usize,
124+
/// Decide what to do to automatically resolve conflicts.
125+
/// If `None`, add conflict markers according to `conflict_style` and `marker_size`.
126+
pub on_conflict: Option<ResolveWith>,
127+
}
128+
129+
impl Default for Options {
130+
fn default() -> Self {
131+
Options {
132+
conflict_style: Default::default(),
133+
marker_size: 7,
134+
on_conflict: None,
135+
}
136+
}
137+
}
138+
139+
/// What to do to resolve a conflict.
140+
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
141+
pub enum ResolveWith {
142+
/// Chose our side to resolve a conflict.
143+
Ours,
144+
/// Chose their side to resolve a conflict.
145+
Theirs,
146+
/// Place our and their lines one after another, in any order
147+
Union,
148+
}
149+
150+
/// Merge `current` and `other` with `ancestor` as base according to `opts`.
151+
///
152+
/// Place the merged result in `out` and return the resolution.
153+
pub fn merge(_out: &mut Vec<u8>, _current: &[u8], _ancestor: &[u8], _other: &[u8], _opts: Options) -> Resolution {
154+
todo!("text merge");
155+
}
156+
}

gix-merge/src/blob/mod.rs

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// TODO: remove this - only needed while &mut Vec<u8> isn't used.
2+
#![allow(clippy::ptr_arg)]
3+
4+
use bstr::BString;
5+
use std::path::PathBuf;
6+
7+
///
8+
pub mod builtin_driver;
9+
///
10+
pub mod pipeline;
11+
///
12+
pub mod platform;
13+
14+
/// Identify a merge resolution.
15+
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
16+
pub enum Resolution {
17+
/// Everything could be resolved during the merge.
18+
Complete,
19+
/// A conflict is still present.
20+
Conflict,
21+
}
22+
23+
/// A way to classify a resource suitable for merging.
24+
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
25+
pub enum ResourceKind {
26+
/// Our side of the state.
27+
CurrentOrOurs,
28+
/// Their side of the state.
29+
OtherOrTheirs,
30+
/// The state of the common base of both ours and theirs.
31+
CommonAncestorOrBase,
32+
}
33+
34+
/// Define a driver program that merges
35+
///
36+
/// Some values are related to diffing, some are related to conversions.
37+
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
38+
pub enum BuiltinDriver {
39+
/// Perform a merge between text-sources such that conflicts are marked according to
40+
/// `merge.conflictStyle` in the Git configuration.
41+
///
42+
/// If any of the inputs, *base*, *ours* or *theirs* looks like non-text/binary,
43+
/// the [`Binary`](Self::Binary) driver will be used instead.
44+
///
45+
/// Also see [`builtin_driver::text::ConflictStyle`].
46+
#[default]
47+
Text,
48+
/// Merge 'unmergable' content by choosing *ours* or *theirs*, without performing
49+
/// an actual merge.
50+
///
51+
/// Note that if the merge operation is for virtual ancestor (a merge for merge-bases),
52+
/// then *ours* will always be chosen.
53+
Binary,
54+
/// Merge text-sources and resolve conflicts by adding conflicting lines one after another,
55+
/// in random order, without adding conflict markers either.
56+
///
57+
/// This can be useful for files that change a lot, but will remain usable merely by adding
58+
/// all changed lines.
59+
Union,
60+
}
61+
62+
/// Define a driver program that merges
63+
///
64+
/// Some values are related to diffing, some are related to conversions.
65+
#[derive(Default, Debug, Clone, PartialEq, Eq)]
66+
pub struct Driver {
67+
/// The name of the driver, as referred to by `[merge "name"]` in the git configuration.
68+
pub name: BString,
69+
/// The human-readable version of `name`, only to be used for displaying driver-information to the user.
70+
pub display_name: BString,
71+
/// The command to execute to perform the merge entirely like `<command> %O %A %B %L %P %S %X %Y`.
72+
///
73+
/// * **%O**
74+
/// - the common ancestor version, or *base*.
75+
/// * **%A**
76+
/// - the current version, or *ours*.
77+
/// * **%B**
78+
/// - the other version, or *theirs*.
79+
/// * **%L**
80+
/// - The conflict-marker size as positive number.
81+
/// * **%P**
82+
/// - The path in which the merged result will be stored.
83+
/// * **%S**
84+
/// - The conflict-label for the common ancestor or *base*.
85+
/// * **%X**
86+
/// - The conflict-label for the current version or *ours*.
87+
/// * **%Y**
88+
/// - The conflict-label for the other version or *theirs*.
89+
///
90+
/// Note that conflict-labels are behind the conflict markers, to annotate them.
91+
///
92+
/// A typical invocation with all arguments substituted could then look like this:
93+
///
94+
/// ```
95+
/// <driver-program> .merge_file_nR2Qs1 .merge_file_WYXCJe .merge_file_UWbzrm 7 file e2a2970 HEAD feature
96+
/// ```
97+
pub command: BString,
98+
/// If `true`, this is the `name` of the driver to use when a virtual-merge-base is created, as a merge of all
99+
/// available merge-bases if there are more than one.
100+
///
101+
/// This value can also be special built-in drivers named `text`, `binary` or `union`. Note that user-defined
102+
/// drivers with the same name will be preferred over built-in ones, but only for files whose git attributes
103+
/// specified the driver by *name*.
104+
pub recursive: Option<BString>,
105+
}
106+
107+
/// A conversion pipeline to take an object or path from what's stored in Git to what can be merged, while
108+
/// following the guidance of git-attributes at the respective path to learn how the merge should be performed.
109+
///
110+
/// Depending on the source, different conversions are performed:
111+
///
112+
/// * `worktree on disk` -> `object for storage in git`
113+
/// * `object` -> `possibly renormalized object`
114+
/// - Renormalization means that the `object` is converted to what would be checked out into the work-tree,
115+
/// just to turn it back into an object.
116+
#[derive(Clone)]
117+
pub struct Pipeline {
118+
/// A way to read data directly from the worktree.
119+
pub roots: pipeline::WorktreeRoots,
120+
/// A pipeline to convert objects from the worktree to Git, and also from Git to the worktree, and back to Git.
121+
pub filter: gix_filter::Pipeline,
122+
/// Options affecting the way we read files.
123+
pub options: pipeline::Options,
124+
/// All available merge drivers.
125+
///
126+
/// They are referenced in git-attributes by name, and we hand out indices into this array.
127+
drivers: Vec<Driver>,
128+
/// Pre-configured attributes to obtain additional merge-related information.
129+
attrs: gix_filter::attributes::search::Outcome,
130+
/// A buffer to produce disk-accessible paths from worktree roots.
131+
path: PathBuf,
132+
}
133+
134+
/// A utility for gathering and processing all state necessary to perform a three-way merge.
135+
///
136+
/// It can re-use buffers if all three parts of participating in the merge are
137+
/// set repeatedly.
138+
#[derive(Clone)]
139+
pub struct Platform {
140+
/// The current version (ours).
141+
current: Option<platform::Resource>,
142+
/// The ancestor version (base).
143+
ancestor: Option<platform::Resource>,
144+
/// The other version (theirs).
145+
other: Option<platform::Resource>,
146+
147+
/// A way to convert objects into a diff-able format.
148+
pub filter: Pipeline,
149+
/// A way to access `.gitattributes`
150+
pub attr_stack: gix_worktree::Stack,
151+
152+
/// The way we convert resources into mergeable states.
153+
filter_mode: pipeline::Mode,
154+
}

0 commit comments

Comments
 (0)