Skip to content

Commit 3fb989b

Browse files
authored
Merge pull request #1618 from GitoxideLabs/merge
octopus-merge (part 5: tree-merge-ORT three-way)
2 parents 417bf95 + 84707c2 commit 3fb989b

File tree

109 files changed

+5597
-860
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+5597
-860
lines changed

.github/FUNDING.yml

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
github: byron
2+
open_collective: gitoxide

Cargo.lock

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

crate-status.md

+10-3
Original file line numberDiff line numberDiff line change
@@ -338,14 +338,21 @@ Check out the [performance discussion][gix-diff-performance] as well.
338338

339339
### gix-merge
340340

341-
* [x] three-way merge analysis of **blobs** with choice of how to resolve conflicts
341+
* [x] three-way content-merge analysis of **blobs** with choice of how to resolve conflicts
342+
- [x] respect git attributes and drivers.
342343
- [ ] choose how to resolve conflicts on the data-structure
343-
- [ ] produce a new blob based on data-structure containing possible resolutions
344+
- [ ] more efficient handling of paths with `merge=binary` attributes (do not load them into memory)
345+
- [x] produce a new blob based on data-structure containing possible resolutions
344346
- [x] `merge` style
345347
- [x] `diff3` style
346348
- [x] `zdiff` style
349+
- [ ] various newlines-related options during the merge (see https://git-scm.com/docs/git-merge#Documentation/git-merge.txt-ignore-space-change).
347350
- [ ] a way to control inter-hunk merging based on proximity (maybe via `gix-diff` feature which could use the same)
348-
* [ ] diff-heuristics match Git perfectly
351+
* [x] **tree**-diff-heuristics match Git for its test-cases
352+
- [ ] a way to generate an index with stages
353+
- *currently the data it provides won't generate index entries, and possibly can't be used for it yet*
354+
- [ ] submodule merges (*right now they count as conflicts if they differ*)
355+
* [x] **commits** - with handling of multiple merge bases by recursive merge-base merge
349356
* [x] API documentation
350357
* [ ] Examples
351358

gitoxide-core/src/pack/explode.rs

+5-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use anyhow::{anyhow, Result};
99
use gix::{
1010
hash::ObjectId,
1111
object, objs, odb,
12-
odb::{loose, pack, Write},
12+
odb::{loose, pack},
13+
prelude::Write,
1314
NestedProgress,
1415
};
1516

@@ -96,8 +97,8 @@ enum OutputWriter {
9697
Sink(odb::Sink),
9798
}
9899

99-
impl gix::odb::Write for OutputWriter {
100-
fn write_buf(&self, kind: object::Kind, from: &[u8]) -> Result<ObjectId, gix::odb::write::Error> {
100+
impl gix::objs::Write for OutputWriter {
101+
fn write_buf(&self, kind: object::Kind, from: &[u8]) -> Result<ObjectId, gix::objs::write::Error> {
101102
match self {
102103
OutputWriter::Loose(db) => db.write_buf(kind, from),
103104
OutputWriter::Sink(db) => db.write_buf(kind, from),
@@ -109,7 +110,7 @@ impl gix::odb::Write for OutputWriter {
109110
kind: object::Kind,
110111
size: u64,
111112
from: &mut dyn Read,
112-
) -> Result<ObjectId, gix::odb::write::Error> {
113+
) -> Result<ObjectId, gix::objs::write::Error> {
113114
match self {
114115
OutputWriter::Loose(db) => db.write_stream(kind, size, from),
115116
OutputWriter::Sink(db) => db.write_stream(kind, size, from),

gitoxide-core/src/repository/diff.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use gix::bstr::{BString, ByteSlice};
22
use gix::objs::tree::EntryMode;
3+
use gix::odb::store::RefreshMode;
34
use gix::prelude::ObjectIdExt;
45

56
pub fn tree(
@@ -9,6 +10,7 @@ pub fn tree(
910
new_treeish: BString,
1011
) -> anyhow::Result<()> {
1112
repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?));
13+
repo.objects.refresh = RefreshMode::Never;
1214

1315
let old_tree_id = repo.rev_parse_single(old_treeish.as_bstr())?;
1416
let new_tree_id = repo.rev_parse_single(new_treeish.as_bstr())?;

gitoxide-core/src/repository/merge.rs renamed to gitoxide-core/src/repository/merge/file.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::OutputFormat;
2-
use anyhow::{bail, Context};
2+
use anyhow::{anyhow, bail, Context};
33
use gix::bstr::BString;
44
use gix::bstr::ByteSlice;
55
use gix::merge::blob::builtin_driver::binary;
@@ -83,8 +83,11 @@ pub fn file(
8383
other: Some(theirs.as_bstr()),
8484
};
8585
let mut buf = repo.empty_reusable_buffer();
86-
let (pick, resolution) = platform.merge(&mut buf, labels, repo.command_context()?)?;
87-
let buf = platform.buffer_by_pick(pick).unwrap_or(&buf);
86+
let (pick, resolution) = platform.merge(&mut buf, labels, &repo.command_context()?)?;
87+
let buf = platform
88+
.buffer_by_pick(pick)
89+
.map_err(|_| anyhow!("Participating object was too large"))?
90+
.unwrap_or(&buf);
8891
out.write_all(buf)?;
8992

9093
if resolution == Resolution::Conflict {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mod file;
2+
pub use file::file;
3+
4+
pub mod tree;
5+
pub use tree::function::tree;
+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
use crate::OutputFormat;
2+
3+
pub struct Options {
4+
pub format: OutputFormat,
5+
pub resolve_content_merge: Option<gix::merge::blob::builtin_driver::text::Conflict>,
6+
pub in_memory: bool,
7+
}
8+
9+
pub(super) mod function {
10+
11+
use crate::OutputFormat;
12+
use anyhow::{anyhow, bail, Context};
13+
use gix::bstr::BString;
14+
use gix::bstr::ByteSlice;
15+
use gix::merge::blob::builtin_driver::binary;
16+
use gix::merge::blob::builtin_driver::text::Conflict;
17+
use gix::merge::tree::UnresolvedConflict;
18+
use gix::prelude::Write;
19+
20+
use super::Options;
21+
22+
#[allow(clippy::too_many_arguments)]
23+
pub fn tree(
24+
mut repo: gix::Repository,
25+
out: &mut dyn std::io::Write,
26+
err: &mut dyn std::io::Write,
27+
base: BString,
28+
ours: BString,
29+
theirs: BString,
30+
Options {
31+
format,
32+
resolve_content_merge,
33+
in_memory,
34+
}: Options,
35+
) -> anyhow::Result<()> {
36+
if format != OutputFormat::Human {
37+
bail!("JSON output isn't implemented yet");
38+
}
39+
repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?));
40+
if in_memory {
41+
repo.objects.enable_object_memory();
42+
}
43+
let (base_ref, base_id) = refname_and_tree(&repo, base)?;
44+
let (ours_ref, ours_id) = refname_and_tree(&repo, ours)?;
45+
let (theirs_ref, theirs_id) = refname_and_tree(&repo, theirs)?;
46+
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+
58+
let base_id_str = base_id.to_string();
59+
let ours_id_str = ours_id.to_string();
60+
let theirs_id_str = theirs_id.to_string();
61+
let labels = gix::merge::blob::builtin_driver::text::Labels {
62+
ancestor: base_ref
63+
.as_ref()
64+
.map_or(base_id_str.as_str().into(), |n| n.as_bstr())
65+
.into(),
66+
current: ours_ref
67+
.as_ref()
68+
.map_or(ours_id_str.as_str().into(), |n| n.as_bstr())
69+
.into(),
70+
other: theirs_ref
71+
.as_ref()
72+
.map_or(theirs_id_str.as_str().into(), |n| n.as_bstr())
73+
.into(),
74+
};
75+
let mut res = repo.merge_trees(base_id, ours_id, theirs_id, labels, options)?;
76+
{
77+
let _span = gix::trace::detail!("Writing merged tree");
78+
let mut written = 0;
79+
let tree_id = res
80+
.tree
81+
.write(|tree| {
82+
written += 1;
83+
repo.write(tree)
84+
})
85+
.map_err(|err| anyhow!("{err}"))?;
86+
writeln!(out, "{tree_id} (wrote {written} trees)")?;
87+
}
88+
89+
if !res.conflicts.is_empty() {
90+
writeln!(err, "{} possibly resolved conflicts", res.conflicts.len())?;
91+
}
92+
if res.has_unresolved_conflicts(UnresolvedConflict::Renames) {
93+
bail!("Tree conflicted")
94+
}
95+
Ok(())
96+
}
97+
98+
fn refname_and_tree(
99+
repo: &gix::Repository,
100+
revspec: BString,
101+
) -> anyhow::Result<(Option<BString>, gix::hash::ObjectId)> {
102+
let spec = repo.rev_parse(revspec.as_bstr())?;
103+
let tree_id = spec
104+
.single()
105+
.context("Expected revspec to expand to a single rev only")?
106+
.object()?
107+
.peel_to_tree()?
108+
.id;
109+
let refname = spec.first_reference().map(|r| r.name.shorten().as_bstr().to_owned());
110+
Ok((refname, tree_id))
111+
}
112+
}

gix-diff/src/rewrites/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
use crate::tree::visit::ChangeId;
12
use crate::Rewrites;
3+
use std::collections::BTreeSet;
24

35
/// Types related to the rename tracker for renames, rewrites and copies.
46
pub mod tracker;
@@ -12,6 +14,8 @@ pub struct Tracker<T> {
1214
path_backing: Vec<u8>,
1315
/// How to track copies and/or rewrites.
1416
rewrites: Rewrites,
17+
/// Previously emitted relation ids of rewrite pairs, with `(deleted source, added destination)`.
18+
child_renames: BTreeSet<(ChangeId, ChangeId)>,
1519
}
1620

1721
/// Determine in which set of files to search for copies.

0 commit comments

Comments
 (0)