Skip to content

Commit

Permalink
Merge pull request #1618 from GitoxideLabs/merge
Browse files Browse the repository at this point in the history
octopus-merge (part 5: tree-merge-ORT three-way)
  • Loading branch information
Byron authored Nov 2, 2024
2 parents 417bf95 + 84707c2 commit 3fb989b
Show file tree
Hide file tree
Showing 109 changed files with 5,597 additions and 860 deletions.
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

0 comments on commit 3fb989b

Please sign in to comment.