Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0ae3791

Browse files
committedSep 12, 2024··
Add the first test for an octopus merge
1 parent 3a1fe6a commit 0ae3791

File tree

5 files changed

+660
-0
lines changed

5 files changed

+660
-0
lines changed
 

‎Cargo.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎gix-merge/Cargo.toml

Lines changed: 26 additions & 0 deletions
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/mod.rs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
use bstr::BString;
2+
use std::path::PathBuf;
3+
4+
///
5+
pub mod pipeline;
6+
7+
/// A way to classify a resource suitable for merging.
8+
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
9+
pub enum ResourceKind {
10+
/// Our side of the state.
11+
CurrentOrOurs,
12+
/// Their side of the state.
13+
OtherOrTheirs,
14+
/// The state of the common base of both ours and theirs.
15+
CommonAncestorOrBase,
16+
}
17+
18+
/// Define a driver program that merges
19+
///
20+
/// Some values are related to diffing, some are related to conversions.
21+
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
22+
pub enum BuiltinDriver {
23+
/// Perform a merge between text-sources such that conflicts are marked according to
24+
/// `merge.conflictStyle` in the Git configuration.
25+
///
26+
/// If any of the inputs, *base*, *ours* or *theirs* looks like non-text/binary,
27+
/// the [`Binary`](Self::Binary) driver will be used instead.
28+
///
29+
/// Also see [`TextDriverConflictStyle`].
30+
#[default]
31+
Text,
32+
/// Merge 'unmergable' content by choosing *ours* or *theirs*, without performing
33+
/// an actual merge.
34+
///
35+
/// Note that if the merge operation is for virtual ancestor (a merge for merge-bases),
36+
/// then *ours* will always be chosen.
37+
Binary,
38+
/// Merge text-sources and resolve conflicts by adding conflicting lines one after another,
39+
/// in random order, without adding conflict markers either.
40+
///
41+
/// This can be useful for files that change a lot, but will remain usable merely by adding
42+
/// all changed lines.
43+
Union,
44+
}
45+
46+
/// The way the built-in [text driver](BuiltinDriver::Text) will express merge conflicts in the
47+
/// resulting file.
48+
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
49+
pub enum TextDriverConflictStyle {
50+
/// Only show the zealously minified conflicting lines of the local changes and the incoming (other) changes,
51+
/// hiding the base version entirely.
52+
///
53+
/// ```
54+
/// line1-changed-by-both
55+
/// <<<<<<< local
56+
/// line2-to-be-changed-in-incoming
57+
/// =======
58+
/// line2-changed
59+
/// >>>>>>> incoming
60+
///```
61+
#[default]
62+
Merge,
63+
/// Show non-minimized hunks of local changes, the base, and the incoming (other) changes.
64+
///
65+
/// This mode does not hide any information.
66+
/// ```
67+
/// <<<<<<< local
68+
/// line1-changed-by-both
69+
/// line2-to-be-changed-in-incoming
70+
/// ||||||| 9a8d80c
71+
/// line1-to-be-changed-by-both
72+
/// line2-to-be-changed-in-incoming
73+
/// =======
74+
/// line1-changed-by-both
75+
/// line2-changed
76+
/// >>>>>>> incoming
77+
///```
78+
Diff3,
79+
/// Like [`Diff3](Self::Diff3), but will show *minimized* hunks of local change and the incoming (other) changes,
80+
/// as well as non-minimized hunks of the base.
81+
///
82+
/// ```
83+
/// line1-changed-by-both
84+
/// <<<<<<< local
85+
/// line2-to-be-changed-in-incoming
86+
/// ||||||| 9a8d80c
87+
/// line1-to-be-changed-by-both
88+
/// line2-to-be-changed-in-incoming
89+
/// =======
90+
/// line2-changed
91+
/// >>>>>>> incoming
92+
/// ```
93+
ZealousDiff3,
94+
}
95+
96+
impl BuiltinDriver {
97+
/// Return the name of this instance.
98+
pub fn as_str(&self) -> &str {
99+
match self {
100+
BuiltinDriver::Text => "text",
101+
BuiltinDriver::Binary => "binary",
102+
BuiltinDriver::Union => "union",
103+
}
104+
}
105+
106+
/// Get all available built-in drivers.
107+
pub fn all() -> &'static [Self] {
108+
&[BuiltinDriver::Text, BuiltinDriver::Binary, BuiltinDriver::Union]
109+
}
110+
111+
/// Try to match one of our variants to `name`, case-sensitive, and return its instance.
112+
pub fn by_name(name: &str) -> Option<Self> {
113+
Self::all().iter().find(|variant| variant.as_str() == name).copied()
114+
}
115+
}
116+
117+
/// Define a driver program that merges
118+
///
119+
/// Some values are related to diffing, some are related to conversions.
120+
#[derive(Default, Debug, Clone, PartialEq, Eq)]
121+
pub struct Driver {
122+
/// The name of the driver, as referred to by `[merge "name"]` in the git configuration.
123+
pub name: BString,
124+
/// The human-readable version of `name`, only to be used for displaying driver-information to the user.
125+
pub display_name: BString,
126+
/// The command to execute to perform the merge entirely like `<command> %O %A %B %L %P %S %X %Y`.
127+
///
128+
/// * **%O**
129+
/// - the common ancestor version, or *base*.
130+
/// * **%A**
131+
/// - the current version, or *ours*.
132+
/// * **%B**
133+
/// - the other version, or *theirs*.
134+
/// * **%L**
135+
/// - The conflict-marker size as positive number.
136+
/// * **%P**
137+
/// - The path in which the merged result will be stored.
138+
/// * **%S**
139+
/// - The conflict-label for the common ancestor or *base*.
140+
/// * **%X**
141+
/// - The conflict-label for the current version or *ours*.
142+
/// * **%Y**
143+
/// - The conflict-label for the other version or *theirs*.
144+
///
145+
/// Note that conflict-labels are behind the conflict markers, to annotate them
146+
pub command: BString,
147+
/// If `true`, this is the `name` of the driver to use when a virtual-merge-base is created, as a merge of all
148+
/// available merge-bases if there are more than one.
149+
///
150+
/// This value can also be special built-in drivers named `text`, `binary` or `union`. Note that user-defined
151+
/// drivers with the same name will be preferred over built-in ones, but only for files whose git attributes
152+
/// specified the driver by *name*.
153+
pub recursive: Option<BString>,
154+
}
155+
156+
/// A conversion pipeline to take an object or path from what's stored in Git to what can be merged, while
157+
/// following the guidance of git-attributes at the respective path to learn how the merge should be performed.
158+
///
159+
/// Depending on the source, different conversions are performed:
160+
///
161+
/// * `worktree on disk` -> `object for storage in git`
162+
/// * `object` -> `possibly renormalized object`
163+
/// - Renormalization means that the `object` is converted to what would be checked out into the work-tree,
164+
/// just to turn it back into an object.
165+
#[derive(Clone)]
166+
pub struct Pipeline {
167+
/// A way to read data directly from the worktree.
168+
pub roots: pipeline::WorktreeRoots,
169+
/// A pipeline to convert objects from the worktree to Git, and also from Git to the worktree, and back to Git.
170+
pub filter: gix_filter::Pipeline,
171+
/// Options affecting the way we read files.
172+
pub options: pipeline::Options,
173+
/// All available merge drivers.
174+
///
175+
/// They are referenced in git-attributes by name, and we hand out indices into this array.
176+
drivers: Vec<Driver>,
177+
/// Pre-configured attributes to obtain additional merge-related information.
178+
attrs: gix_filter::attributes::search::Outcome,
179+
/// A buffer to produce disk-accessible paths from worktree roots.
180+
path: PathBuf,
181+
}

‎gix-merge/src/blob/pipeline.rs

Lines changed: 433 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,433 @@
1+
use super::{BuiltinDriver, Pipeline, ResourceKind};
2+
use bstr::{BStr, ByteSlice};
3+
use gix_filter::attributes;
4+
use gix_filter::driver::apply::{Delay, MaybeDelayed};
5+
use gix_filter::pipeline::convert::{ToGitOutcome, ToWorktreeOutcome};
6+
use gix_object::tree::EntryKind;
7+
use std::io::Read;
8+
use std::path::{Path, PathBuf};
9+
10+
/// Options for use in a [`Pipeline`].
11+
#[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
12+
pub struct Options {
13+
/// The amount of bytes that an object has to reach before being treated as binary.
14+
/// These objects will not be queried, nor will their data be processed in any way.
15+
/// If `0`, no file is ever considered binary due to their size.
16+
///
17+
/// Note that for files stored in `git`, what counts is their stored, decompressed size,
18+
/// thus `git-lfs` files would typically not be considered binary unless one explicitly sets
19+
/// them.
20+
/// However, if they are to be retrieved from the worktree, the worktree size is what matters,
21+
/// even though that also might be a `git-lfs` file which is small in Git.
22+
pub large_file_threshold_bytes: u64,
23+
/// Capabilities of the file system which affect how we read worktree files.
24+
pub fs: gix_fs::Capabilities,
25+
/// Define which driver to use if the `merge` attribute for a resource is unspecified.
26+
///
27+
/// This is the value of the `merge.default` git configuration.
28+
pub default_driver: Option<BuiltinDriver>,
29+
}
30+
31+
/// The specific way to convert a resource.
32+
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
33+
pub enum Mode {
34+
/// Prepare resources as they are stored in `git`.
35+
///
36+
/// This is naturally the case when object-ids are used, but a conversion is needed
37+
/// when data is read from a worktree.
38+
#[default]
39+
ToGit,
40+
/// For sources that are object-ids, convert them to what *would* be stored in the worktree,
41+
/// and back to what *would* be stored in Git.
42+
///
43+
/// Sources that are located in a worktree are merely converted to what *would* be stored in Git.
44+
Renormalize,
45+
}
46+
47+
/// A way to access roots for different kinds of resources that are possibly located and accessible in a worktree.
48+
#[derive(Clone, Debug, Default)]
49+
pub struct WorktreeRoots {
50+
/// The worktree root where the current (or our) version of the resource is present.
51+
pub current_root: Option<PathBuf>,
52+
/// The worktree root where the other (or their) version of the resource is present.
53+
pub other_root: Option<PathBuf>,
54+
/// The worktree root where containing the resource of the common ancestor of our and their version.
55+
pub common_ancestor_root: Option<PathBuf>,
56+
}
57+
58+
impl WorktreeRoots {
59+
/// Return the root path for the given `kind`
60+
pub fn by_kind(&self, kind: ResourceKind) -> Option<&Path> {
61+
match kind {
62+
ResourceKind::CurrentOrOurs => self.current_root.as_deref(),
63+
ResourceKind::CommonAncestorOrBase => self.common_ancestor_root.as_deref(),
64+
ResourceKind::OtherOrTheirs => self.other_root.as_deref(),
65+
}
66+
}
67+
68+
/// Return `true` if all worktree roots are unset.
69+
pub fn is_unset(&self) -> bool {
70+
self.current_root.is_none() && self.other_root.is_none() && self.common_ancestor_root.is_none()
71+
}
72+
}
73+
74+
/// Lifecycle
75+
impl Pipeline {
76+
/// Create a new instance of a pipeline which produces blobs suitable for merging.
77+
///
78+
/// `roots` allow to read worktree files directly, and `worktree_filter` is used
79+
/// to transform object database data directly. `drivers` further configure individual paths.
80+
/// `options` are used to further configure the way we act..
81+
pub fn new(
82+
roots: WorktreeRoots,
83+
worktree_filter: gix_filter::Pipeline,
84+
mut drivers: Vec<super::Driver>,
85+
options: Options,
86+
) -> Self {
87+
drivers.sort_by(|a, b| a.name.cmp(&b.name));
88+
Pipeline {
89+
roots,
90+
filter: worktree_filter,
91+
drivers,
92+
options,
93+
attrs: {
94+
let mut out = gix_filter::attributes::search::Outcome::default();
95+
out.initialize_with_selection(&Default::default(), Some("merge"));
96+
out
97+
},
98+
path: Default::default(),
99+
}
100+
}
101+
}
102+
103+
/// Access
104+
impl Pipeline {
105+
/// Return all drivers that this instance was initialized with.
106+
///
107+
/// They are sorted by [`name`](super::Driver::name) to support binary searches.
108+
pub fn drivers(&self) -> &[super::Driver] {
109+
&self.drivers
110+
}
111+
}
112+
113+
/// Data as part of an [Outcome].
114+
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
115+
pub enum Data {
116+
/// The data to use for merging was written into the buffer that was passed during the call to [`Pipeline::convert_to_mergeable()`].
117+
Buffer,
118+
/// The size that the binary blob had at the given revision, without having applied filters, as it's either
119+
/// considered binary or above the big-file threshold.
120+
///
121+
/// In this state, the binary file cannot be merged.
122+
Binary {
123+
/// The size of the object prior to performing any filtering or as it was found on disk.
124+
///
125+
/// Note that technically, the size isn't always representative of the same 'state' of the
126+
/// content, as once it can be the size of the blob in git, and once it's the size of file
127+
/// in the worktree - both can differ a lot depending on filters.
128+
size: u64,
129+
},
130+
}
131+
132+
/// The selection of the driver to use by a resource obtained with [`Pipeline::convert_to_mergeable()`].
133+
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
134+
pub enum DriverChoice {
135+
/// Use the given built-in driver to perform the merge.
136+
BuiltIn(BuiltinDriver),
137+
/// Use the user-provided driver program using the index into [the pipelines driver array](Pipeline::drivers().
138+
Index(usize),
139+
}
140+
141+
impl Default for DriverChoice {
142+
fn default() -> Self {
143+
DriverChoice::BuiltIn(Default::default())
144+
}
145+
}
146+
147+
/// The outcome returned by [Pipeline::convert_to_mergeable()].
148+
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
149+
pub struct Outcome {
150+
/// If available, an index into the `drivers` field to access more diff-related information of the driver for items
151+
/// at the given path, as previously determined by git-attributes.
152+
///
153+
/// * `merge` is set
154+
/// - Use the [`BuiltinDriver::Text`]
155+
/// * `-merge` is unset
156+
/// - Use the [`BuiltinDriver::Binary`]
157+
/// * `!merge` is unspecified
158+
/// - Use [`Options::default_driver`] or [`BuiltinDriver::Text`].
159+
/// * `merge=name`
160+
/// - Search for a user-configured or built-in driver called `name`.
161+
/// - If not found, silently default to [`BuiltinDriver::Text`]
162+
///
163+
/// Note that drivers are queried even if there is no object available.
164+
pub driver: DriverChoice,
165+
/// The data itself, suitable for diffing, and if the object or worktree item is present at all.
166+
pub data: Option<Data>,
167+
}
168+
169+
///
170+
pub mod convert_to_mergeable {
171+
use std::collections::TryReserveError;
172+
173+
use bstr::BString;
174+
use gix_object::tree::EntryKind;
175+
176+
/// The error returned by [Pipeline::convert_to_mergeable()](super::Pipeline::convert_to_mergeable()).
177+
#[derive(Debug, thiserror::Error)]
178+
#[allow(missing_docs)]
179+
pub enum Error {
180+
#[error("Entry at '{rela_path}' must be regular file or symlink, but was {actual:?}")]
181+
InvalidEntryKind { rela_path: BString, actual: EntryKind },
182+
#[error("Entry at '{rela_path}' could not be read as symbolic link")]
183+
ReadLink { rela_path: BString, source: std::io::Error },
184+
#[error("Entry at '{rela_path}' could not be opened for reading or read from")]
185+
OpenOrRead { rela_path: BString, source: std::io::Error },
186+
#[error("Entry at '{rela_path}' could not be copied from a filter process to a memory buffer")]
187+
StreamCopy { rela_path: BString, source: std::io::Error },
188+
#[error(transparent)]
189+
FindObject(#[from] gix_object::find::existing_object::Error),
190+
#[error(transparent)]
191+
ConvertToWorktree(#[from] gix_filter::pipeline::convert::to_worktree::Error),
192+
#[error(transparent)]
193+
ConvertToGit(#[from] gix_filter::pipeline::convert::to_git::Error),
194+
#[error("Memory allocation failed")]
195+
OutOfMemory(#[from] TryReserveError),
196+
}
197+
}
198+
199+
/// Conversion
200+
impl Pipeline {
201+
/// Convert the object at `id`, `mode`, `rela_path` and `kind`, providing access to `attributes` and `objects`.
202+
/// The resulting merge-able data is written into `out`, if it's not too large or considered binary.
203+
/// The returned [`Outcome`] contains information on how to use `out`, or if it's filled at all.
204+
///
205+
/// `attributes` must be returning the attributes at `rela_path`, and `objects` must be usable if `kind` is
206+
/// a resource in the object database, i.e. if no worktree root is available. It's notable that if a worktree root
207+
/// is present for `kind`, then a `rela_path` is used to access it on disk.
208+
///
209+
/// If `id` [is null](gix_hash::ObjectId::is_null()) or the file in question doesn't exist in the worktree in case
210+
/// [a root](WorktreeRoots) is present, then `out` will be left cleared and [Outcome::data] will be `None`.
211+
/// This is useful to simplify the calling code as empty buffers signal that nothing is there.
212+
///
213+
/// Note that `mode` is trusted, and we will not re-validate that the entry in the worktree actually is of that mode.
214+
/// Only blobs are allowed.
215+
///
216+
/// Use `convert` to control what kind of the resource will be produced.
217+
#[allow(clippy::too_many_arguments)]
218+
pub fn convert_to_mergeable(
219+
&mut self,
220+
id: &gix_hash::oid,
221+
mode: EntryKind,
222+
rela_path: &BStr,
223+
kind: ResourceKind,
224+
attributes: &mut dyn FnMut(&BStr, &mut gix_filter::attributes::search::Outcome),
225+
objects: &dyn gix_object::FindObjectOrHeader,
226+
convert: Mode,
227+
out: &mut Vec<u8>,
228+
) -> Result<Outcome, convert_to_mergeable::Error> {
229+
if !matches!(mode, EntryKind::Blob | EntryKind::BlobExecutable) {
230+
return Err(convert_to_mergeable::Error::InvalidEntryKind {
231+
rela_path: rela_path.to_owned(),
232+
actual: mode,
233+
});
234+
}
235+
236+
out.clear();
237+
attributes(rela_path, &mut self.attrs);
238+
let attr = self.attrs.iter_selected().next().expect("pre-initialized with 'diff'");
239+
let driver = match attr.assignment.state {
240+
attributes::StateRef::Set => DriverChoice::BuiltIn(BuiltinDriver::Text),
241+
attributes::StateRef::Unset => DriverChoice::BuiltIn(BuiltinDriver::Binary),
242+
attributes::StateRef::Value(name) => {
243+
let name = name.as_bstr();
244+
self.drivers
245+
.binary_search_by(|d| d.name.as_bstr().cmp(name))
246+
.ok()
247+
.map(DriverChoice::Index)
248+
.or_else(|| {
249+
name.to_str()
250+
.ok()
251+
.and_then(BuiltinDriver::by_name)
252+
.map(DriverChoice::BuiltIn)
253+
})
254+
.unwrap_or_default()
255+
}
256+
attributes::StateRef::Unspecified => self
257+
.options
258+
.default_driver
259+
.map(DriverChoice::BuiltIn)
260+
.unwrap_or_default(),
261+
};
262+
match self.roots.by_kind(kind) {
263+
Some(root) => {
264+
self.path.clear();
265+
self.path.push(root);
266+
self.path.push(gix_path::from_bstr(rela_path));
267+
let size_in_bytes = (self.options.large_file_threshold_bytes > 0)
268+
.then(|| {
269+
none_if_missing(self.path.metadata().map(|md| md.len())).map_err(|err| {
270+
convert_to_mergeable::Error::OpenOrRead {
271+
rela_path: rela_path.to_owned(),
272+
source: err,
273+
}
274+
})
275+
})
276+
.transpose()?;
277+
let data = match size_in_bytes {
278+
Some(None) => None, // missing as identified by the size check
279+
Some(Some(size)) if size > self.options.large_file_threshold_bytes => Some(Data::Binary { size }),
280+
_ => {
281+
let file = none_if_missing(std::fs::File::open(&self.path)).map_err(|err| {
282+
convert_to_mergeable::Error::OpenOrRead {
283+
rela_path: rela_path.to_owned(),
284+
source: err,
285+
}
286+
})?;
287+
288+
if let Some(file) = file {
289+
match convert {
290+
Mode::ToGit | Mode::Renormalize => {
291+
let res = self.filter.convert_to_git(
292+
file,
293+
gix_path::from_bstr(rela_path).as_ref(),
294+
attributes,
295+
&mut |buf| objects.try_find(id, buf).map(|obj| obj.map(|_| ())),
296+
)?;
297+
298+
match res {
299+
ToGitOutcome::Unchanged(mut file) => {
300+
file.read_to_end(out).map_err(|err| {
301+
convert_to_mergeable::Error::OpenOrRead {
302+
rela_path: rela_path.to_owned(),
303+
source: err,
304+
}
305+
})?;
306+
}
307+
ToGitOutcome::Process(mut stream) => {
308+
stream.read_to_end(out).map_err(|err| {
309+
convert_to_mergeable::Error::OpenOrRead {
310+
rela_path: rela_path.to_owned(),
311+
source: err,
312+
}
313+
})?;
314+
}
315+
ToGitOutcome::Buffer(buf) => {
316+
out.clear();
317+
out.try_reserve(buf.len())?;
318+
out.extend_from_slice(buf);
319+
}
320+
}
321+
}
322+
}
323+
324+
Some(if is_binary_buf(out) {
325+
let size = out.len() as u64;
326+
out.clear();
327+
Data::Binary { size }
328+
} else {
329+
Data::Buffer
330+
})
331+
} else {
332+
None
333+
}
334+
}
335+
};
336+
Ok(Outcome { driver, data })
337+
}
338+
None => {
339+
let data = if id.is_null() {
340+
None
341+
} else {
342+
let header = objects
343+
.try_header(id)
344+
.map_err(gix_object::find::existing_object::Error::Find)?
345+
.ok_or_else(|| gix_object::find::existing_object::Error::NotFound { oid: id.to_owned() })?;
346+
let is_binary = self.options.large_file_threshold_bytes > 0
347+
&& header.size > self.options.large_file_threshold_bytes;
348+
let data = if is_binary {
349+
Data::Binary { size: header.size }
350+
} else {
351+
objects
352+
.try_find(id, out)
353+
.map_err(gix_object::find::existing_object::Error::Find)?
354+
.ok_or_else(|| gix_object::find::existing_object::Error::NotFound { oid: id.to_owned() })?;
355+
356+
if convert == Mode::Renormalize {
357+
let res = self
358+
.filter
359+
.convert_to_worktree(out, rela_path, attributes, Delay::Forbid)?;
360+
361+
match res {
362+
ToWorktreeOutcome::Unchanged(_) => {}
363+
ToWorktreeOutcome::Buffer(src) => {
364+
out.clear();
365+
out.try_reserve(src.len())?;
366+
out.extend_from_slice(src);
367+
}
368+
ToWorktreeOutcome::Process(MaybeDelayed::Immediate(mut stream)) => {
369+
std::io::copy(&mut stream, out).map_err(|err| {
370+
convert_to_mergeable::Error::StreamCopy {
371+
rela_path: rela_path.to_owned(),
372+
source: err,
373+
}
374+
})?;
375+
}
376+
ToWorktreeOutcome::Process(MaybeDelayed::Delayed(_)) => {
377+
unreachable!("we prohibit this")
378+
}
379+
};
380+
}
381+
382+
let res = self.filter.convert_to_git(
383+
&**out,
384+
&gix_path::from_bstr(rela_path),
385+
attributes,
386+
&mut |buf| objects.try_find(id, buf).map(|obj| obj.map(|_| ())),
387+
)?;
388+
389+
match res {
390+
ToGitOutcome::Unchanged(_) => {}
391+
ToGitOutcome::Process(mut stream) => {
392+
stream
393+
.read_to_end(out)
394+
.map_err(|err| convert_to_mergeable::Error::OpenOrRead {
395+
rela_path: rela_path.to_owned(),
396+
source: err,
397+
})?;
398+
}
399+
ToGitOutcome::Buffer(buf) => {
400+
out.clear();
401+
out.try_reserve(buf.len())?;
402+
out.extend_from_slice(buf);
403+
}
404+
}
405+
406+
if is_binary_buf(out) {
407+
let size = out.len() as u64;
408+
out.clear();
409+
Data::Binary { size }
410+
} else {
411+
Data::Buffer
412+
}
413+
};
414+
Some(data)
415+
};
416+
Ok(Outcome { driver, data })
417+
}
418+
}
419+
}
420+
}
421+
422+
fn none_if_missing<T>(res: std::io::Result<T>) -> std::io::Result<Option<T>> {
423+
match res {
424+
Ok(data) => Ok(Some(data)),
425+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
426+
Err(err) => Err(err),
427+
}
428+
}
429+
430+
fn is_binary_buf(buf: &[u8]) -> bool {
431+
let buf = &buf[..buf.len().min(8000)];
432+
buf.contains(&0)
433+
}

‎gix-merge/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
#![deny(rust_2018_idioms)]
22
#![forbid(unsafe_code)]
3+
4+
///
5+
#[cfg(feature = "blob")]
6+
pub mod blob;

0 commit comments

Comments
 (0)
Please sign in to comment.