Skip to content

octopus-merge (part 5: tree-merge-ORT three-way) #1618

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Nov 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
32d6bfb
fix: assure that `gix tree diff` doesn't slow down too much when shor…
Byron Oct 31, 2024
1f40a89
Add a link to OpenCollective
Byron Oct 15, 2024
eb9f9c3
Improve error message to be less elusive
Byron Oct 12, 2024
eb55c00
feat: Allow access to `tree::State::buf1|2`.
Byron Oct 14, 2024
76e6762
feat: add `tree_with_rewrites::Change(Ref)::previous_location()`
Byron Oct 17, 2024
6740520
feat: add traits for partial equality comparison to `tree_with_rewrit…
Byron Oct 21, 2024
e2ea398
feat: Add `tree::Editor::get()` to get entries directly from the editor.
Byron Oct 17, 2024
78a5355
fix!: prefer to receive borrowed `gix_command::Context` when it's jus…
Byron Oct 19, 2024
c1cf08c
feat!: Don't fail on big files during blob-merge, but turn them into …
Byron Oct 30, 2024
dd99991
feat: add `blob::PlatformRef::id_by_pick()` to more efficiently pick …
Byron Oct 30, 2024
278dd88
adapt to changes in `gix-merge`
Byron Oct 19, 2024
de1cfb6
fix!: Adjust blob-merge baseline to also test the reverse of each ope…
Byron Oct 20, 2024
29aad45
feat: `FindExt` now supports empty trees and empty blobs natively.
Byron Oct 25, 2024
ba7b811
feat: support rename tracking across changed directories
Byron Oct 25, 2024
1fbb946
fix: make sure ordinary capitalized partial names can be found by par…
Byron Oct 28, 2024
7325c58
feat!: move `gix-odb::Write` trait to `gix-object::Write`.
Byron Oct 31, 2024
96488f7
adapt to changes in `gix-object` and `gix-odb`
Byron Oct 31, 2024
4b1764c
feat: add `tree()` and `commit()` merge support, en par with `merge-O…
Byron Oct 31, 2024
80e006b
support for merge related options in config tree
Byron Nov 1, 2024
bd91d6a
feat: respect the `conflict-marker-size` attribute as well.
Byron Nov 1, 2024
07746f3
feat: `gix::Repository` implements all traits for object reading and …
Byron Nov 2, 2024
d1ac584
feat: add `Repository::merge_trees()`
Byron Nov 1, 2024
84707c2
feat: add `gix merge tree` to merge trees similarly to `git merge-tree`.
Byron Nov 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
github: byron
open_collective: gitoxide
10 changes: 8 additions & 2 deletions Cargo.lock

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

13 changes: 10 additions & 3 deletions crate-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,14 +338,21 @@ Check out the [performance discussion][gix-diff-performance] as well.

### gix-merge

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

Expand Down
9 changes: 5 additions & 4 deletions gitoxide-core/src/pack/explode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use anyhow::{anyhow, Result};
use gix::{
hash::ObjectId,
object, objs, odb,
odb::{loose, pack, Write},
odb::{loose, pack},
prelude::Write,
NestedProgress,
};

Expand Down Expand Up @@ -96,8 +97,8 @@ enum OutputWriter {
Sink(odb::Sink),
}

impl gix::odb::Write for OutputWriter {
fn write_buf(&self, kind: object::Kind, from: &[u8]) -> Result<ObjectId, gix::odb::write::Error> {
impl gix::objs::Write for OutputWriter {
fn write_buf(&self, kind: object::Kind, from: &[u8]) -> Result<ObjectId, gix::objs::write::Error> {
match self {
OutputWriter::Loose(db) => db.write_buf(kind, from),
OutputWriter::Sink(db) => db.write_buf(kind, from),
Expand All @@ -109,7 +110,7 @@ impl gix::odb::Write for OutputWriter {
kind: object::Kind,
size: u64,
from: &mut dyn Read,
) -> Result<ObjectId, gix::odb::write::Error> {
) -> Result<ObjectId, gix::objs::write::Error> {
match self {
OutputWriter::Loose(db) => db.write_stream(kind, size, from),
OutputWriter::Sink(db) => db.write_stream(kind, size, from),
Expand Down
2 changes: 2 additions & 0 deletions gitoxide-core/src/repository/diff.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use gix::bstr::{BString, ByteSlice};
use gix::objs::tree::EntryMode;
use gix::odb::store::RefreshMode;
use gix::prelude::ObjectIdExt;

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

let old_tree_id = repo.rev_parse_single(old_treeish.as_bstr())?;
let new_tree_id = repo.rev_parse_single(new_treeish.as_bstr())?;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::OutputFormat;
use anyhow::{bail, Context};
use anyhow::{anyhow, bail, Context};
use gix::bstr::BString;
use gix::bstr::ByteSlice;
use gix::merge::blob::builtin_driver::binary;
Expand Down Expand Up @@ -83,8 +83,11 @@ pub fn file(
other: Some(theirs.as_bstr()),
};
let mut buf = repo.empty_reusable_buffer();
let (pick, resolution) = platform.merge(&mut buf, labels, repo.command_context()?)?;
let buf = platform.buffer_by_pick(pick).unwrap_or(&buf);
let (pick, resolution) = platform.merge(&mut buf, labels, &repo.command_context()?)?;
let buf = platform
.buffer_by_pick(pick)
.map_err(|_| anyhow!("Participating object was too large"))?
.unwrap_or(&buf);
out.write_all(buf)?;

if resolution == Resolution::Conflict {
Expand Down
5 changes: 5 additions & 0 deletions gitoxide-core/src/repository/merge/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod file;
pub use file::file;

pub mod tree;
pub use tree::function::tree;
112 changes: 112 additions & 0 deletions gitoxide-core/src/repository/merge/tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use crate::OutputFormat;

pub struct Options {
pub format: OutputFormat,
pub resolve_content_merge: Option<gix::merge::blob::builtin_driver::text::Conflict>,
pub in_memory: bool,
}

pub(super) mod function {

use crate::OutputFormat;
use anyhow::{anyhow, bail, Context};
use gix::bstr::BString;
use gix::bstr::ByteSlice;
use gix::merge::blob::builtin_driver::binary;
use gix::merge::blob::builtin_driver::text::Conflict;
use gix::merge::tree::UnresolvedConflict;
use gix::prelude::Write;

use super::Options;

#[allow(clippy::too_many_arguments)]
pub fn tree(
mut repo: gix::Repository,
out: &mut dyn std::io::Write,
err: &mut dyn std::io::Write,
base: BString,
ours: BString,
theirs: BString,
Options {
format,
resolve_content_merge,
in_memory,
}: Options,
) -> anyhow::Result<()> {
if format != OutputFormat::Human {
bail!("JSON output isn't implemented yet");
}
repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?));
if in_memory {
repo.objects.enable_object_memory();
}
let (base_ref, base_id) = refname_and_tree(&repo, base)?;
let (ours_ref, ours_id) = refname_and_tree(&repo, ours)?;
let (theirs_ref, theirs_id) = refname_and_tree(&repo, theirs)?;

let mut options = repo.tree_merge_options()?;
if let Some(resolve) = resolve_content_merge {
options.blob_merge.text.conflict = resolve;
options.blob_merge.resolve_binary_with = match resolve {
Conflict::Keep { .. } => None,
Conflict::ResolveWithOurs => Some(binary::ResolveWith::Ours),
Conflict::ResolveWithTheirs => Some(binary::ResolveWith::Theirs),
Conflict::ResolveWithUnion => None,
};
}

let base_id_str = base_id.to_string();
let ours_id_str = ours_id.to_string();
let theirs_id_str = theirs_id.to_string();
let labels = gix::merge::blob::builtin_driver::text::Labels {
ancestor: base_ref
.as_ref()
.map_or(base_id_str.as_str().into(), |n| n.as_bstr())
.into(),
current: ours_ref
.as_ref()
.map_or(ours_id_str.as_str().into(), |n| n.as_bstr())
.into(),
other: theirs_ref
.as_ref()
.map_or(theirs_id_str.as_str().into(), |n| n.as_bstr())
.into(),
};
let mut res = repo.merge_trees(base_id, ours_id, theirs_id, labels, options)?;
{
let _span = gix::trace::detail!("Writing merged tree");
let mut written = 0;
let tree_id = res
.tree
.write(|tree| {
written += 1;
repo.write(tree)
})
.map_err(|err| anyhow!("{err}"))?;
writeln!(out, "{tree_id} (wrote {written} trees)")?;
}

if !res.conflicts.is_empty() {
writeln!(err, "{} possibly resolved conflicts", res.conflicts.len())?;
}
if res.has_unresolved_conflicts(UnresolvedConflict::Renames) {
bail!("Tree conflicted")
}
Ok(())
}

fn refname_and_tree(
repo: &gix::Repository,
revspec: BString,
) -> anyhow::Result<(Option<BString>, gix::hash::ObjectId)> {
let spec = repo.rev_parse(revspec.as_bstr())?;
let tree_id = spec
.single()
.context("Expected revspec to expand to a single rev only")?
.object()?
.peel_to_tree()?
.id;
let refname = spec.first_reference().map(|r| r.name.shorten().as_bstr().to_owned());
Ok((refname, tree_id))
}
}
4 changes: 4 additions & 0 deletions gix-diff/src/rewrites/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::tree::visit::ChangeId;
use crate::Rewrites;
use std::collections::BTreeSet;

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

/// Determine in which set of files to search for copies.
Expand Down
Loading
Loading