diff --git a/crates/gitbutler-branch-actions/src/integration.rs b/crates/gitbutler-branch-actions/src/integration.rs index 00c309338..b43c47549 100644 --- a/crates/gitbutler-branch-actions/src/integration.rs +++ b/crates/gitbutler-branch-actions/src/integration.rs @@ -53,9 +53,7 @@ pub(crate) fn get_workspace_head(ctx: &CommandContext) -> Result { workspace_tree = repo.find_commit(merge_base)?.tree()?; } else { let gix_repo = ctx.gix_repository_for_merging()?; - let mut merge_options_fail_fast = gix_repo.tree_merge_options()?; - let conflict_kind = gix::merge::tree::UnresolvedConflict::Renames; - merge_options_fail_fast.fail_on_conflict = Some(conflict_kind); + let (merge_options_fail_fast, conflict_kind) = gix_repo.merge_options_fail_fast()?; let merge_tree_id = git2_to_gix_object_id(repo.find_commit(target.sha)?.tree_id()); for branch in virtual_branches.iter_mut() { let branch_head = repo.find_commit(branch.head())?; diff --git a/crates/gitbutler-branch-actions/src/virtual.rs b/crates/gitbutler-branch-actions/src/virtual.rs index dbc04dc6a..e25280eac 100644 --- a/crates/gitbutler-branch-actions/src/virtual.rs +++ b/crates/gitbutler-branch-actions/src/virtual.rs @@ -25,7 +25,7 @@ use gitbutler_project::access::WorktreeWritePermission; use gitbutler_reference::{normalize_branch_name, Refname, RemoteRefname}; use gitbutler_repo::{ rebase::{cherry_rebase, cherry_rebase_group}, - LogUntil, RepositoryExt, + GixRepositoryExt, LogUntil, RepositoryExt, }; use gitbutler_repo_actions::RepoActionsExt; use gitbutler_stack::{ @@ -180,25 +180,37 @@ pub fn unapply_ownership( .find_commit(workspace_commit_id) .context("failed to find target commit")?; - let base_tree = target_commit.tree().context("failed to get target tree")?; - let final_tree = applied_statuses.into_iter().fold( - target_commit.tree().context("failed to get target tree"), - |final_tree, status| { - let final_tree = final_tree?; + let base_tree_id = git2_to_gix_object_id(target_commit.tree_id()); + let gix_repo = ctx.gix_repository_for_merging()?; + let (merge_options_fail_fast, conflict_kind) = gix_repo.merge_options_fail_fast()?; + let final_tree_id = applied_statuses.into_iter().try_fold( + git2_to_gix_object_id(target_commit.tree_id()), + |final_tree_id, status| -> Result<_> { let files = status .1 .into_iter() .map(|file| (file.path, file.hunks)) .collect::)>>(); - let tree_oid = gitbutler_diff::write::hunks_onto_oid(ctx, workspace_commit_id, files)?; - let branch_tree = repo.find_tree(tree_oid)?; - let mut result = repo.merge_trees(&base_tree, &final_tree, &branch_tree, None)?; - let final_tree_oid = result.write_tree_to(ctx.repository())?; - repo.find_tree(final_tree_oid) - .context("failed to find tree") + let branch_tree_id = + gitbutler_diff::write::hunks_onto_oid(ctx, workspace_commit_id, files)?; + let mut merge = gix_repo.merge_trees( + base_tree_id, + final_tree_id, + git2_to_gix_object_id(branch_tree_id), + gix_repo.default_merge_labels(), + merge_options_fail_fast.clone(), + )?; + if merge.has_unresolved_conflicts(conflict_kind) { + bail!("Tree has conflicts after merge") + } + merge + .tree + .write(|tree| gix_repo.write(tree)) + .map_err(|err| anyhow!("Could not write merged tree: {err}")) }, )?; + let final_tree = repo.find_tree(gix_to_git2_oid(final_tree_id))?; let final_tree_oid = gitbutler_diff::write::hunks_onto_tree(ctx, &final_tree, diff, true)?; let final_tree = repo .find_tree(final_tree_oid) @@ -1035,9 +1047,7 @@ impl IsCommitIntegrated<'_, '_, '_> { } // try to merge our tree into the upstream tree - let mut merge_options = self.gix_repo.tree_merge_options()?; - let conflict_kind = gix::merge::tree::UnresolvedConflict::Renames; - merge_options.fail_on_conflict = Some(conflict_kind); + let (merge_options, conflict_kind) = self.gix_repo.merge_options_fail_fast()?; let mut merge_output = self .gix_repo .merge_trees( diff --git a/crates/gitbutler-repo/src/repository_ext.rs b/crates/gitbutler-repo/src/repository_ext.rs index e49479d60..0647e8c44 100644 --- a/crates/gitbutler-repo/src/repository_ext.rs +++ b/crates/gitbutler-repo/src/repository_ext.rs @@ -17,6 +17,7 @@ use gitbutler_oxidize::{ }; use gitbutler_reference::{Refname, RemoteRefname}; use gix::fs::is_executable; +use gix::merge::tree::{Options, UnresolvedConflict}; use gix::objs::WriteTo; use tracing::instrument; @@ -731,6 +732,15 @@ pub trait GixRepositoryExt: Sized { other: Some("theirs".into()), } } + + /// Return options suitable for merging so that the merge stops immediately after the first conflict. + /// It also returns the conflict kind to use when checking for unresolved conflicts. + fn merge_options_fail_fast( + &self, + ) -> Result<( + gix::merge::tree::Options, + gix::merge::tree::UnresolvedConflict, + )>; } impl GixRepositoryExt for gix::Repository { @@ -759,9 +769,7 @@ impl GixRepositoryExt for gix::Repository { our_tree: gix::ObjectId, their_tree: gix::ObjectId, ) -> Result { - let mut options = self.tree_merge_options()?; - let conflict_kind = gix::merge::tree::UnresolvedConflict::Renames; - options.fail_on_conflict = Some(conflict_kind); + let (options, conflict_kind) = self.merge_options_fail_fast()?; let merge_outcome = self .merge_trees( ancestor_tree, @@ -773,6 +781,13 @@ impl GixRepositoryExt for gix::Repository { .context("failed to merge trees")?; Ok(!merge_outcome.has_unresolved_conflicts(conflict_kind)) } + + fn merge_options_fail_fast(&self) -> Result<(Options, UnresolvedConflict)> { + let mut options = self.tree_merge_options()?; + let conflict_kind = gix::merge::tree::UnresolvedConflict::Renames; + options.fail_on_conflict = Some(conflict_kind); + Ok((options, conflict_kind)) + } } type OidFilter = dyn Fn(&git2::Commit) -> Result;