diff --git a/Cargo.lock b/Cargo.lock index d2565ecc98..df2a44a831 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2545,13 +2545,15 @@ dependencies = [ "gitbutler-diff", "gitbutler-operating-modes", "gitbutler-oplog", + "gitbutler-oxidize", "gitbutler-project", "gitbutler-reference", "gitbutler-repo", "gitbutler-stack", - "gitbutler-time", "gitbutler-workspace", + "gix", "serde", + "tracing", ] [[package]] diff --git a/crates/gitbutler-edit-mode/Cargo.toml b/crates/gitbutler-edit-mode/Cargo.toml index 1525e51983..1cb654bf77 100644 --- a/crates/gitbutler-edit-mode/Cargo.toml +++ b/crates/gitbutler-edit-mode/Cargo.toml @@ -7,6 +7,7 @@ publish = false [dependencies] git2.workspace = true +gix.workspace = true anyhow.workspace = true bstr.workspace = true gitbutler-branch.workspace = true @@ -16,11 +17,12 @@ gitbutler-command-context.workspace = true gitbutler-operating-modes.workspace = true gitbutler-project.workspace = true gitbutler-branch-actions.workspace = true +gitbutler-oxidize.workspace = true gitbutler-reference.workspace = true -gitbutler-time.workspace = true gitbutler-oplog.workspace = true gitbutler-diff.workspace = true gitbutler-stack.workspace = true gitbutler-cherry-pick.workspace = true gitbutler-workspace.workspace = true serde.workspace = true +tracing.workspace = true \ No newline at end of file diff --git a/crates/gitbutler-edit-mode/src/lib.rs b/crates/gitbutler-edit-mode/src/lib.rs index b7baadf29f..f228b4a70d 100644 --- a/crates/gitbutler-edit-mode/src/lib.rs +++ b/crates/gitbutler-edit-mode/src/lib.rs @@ -8,7 +8,7 @@ use git2::build::CheckoutBuilder; use gitbutler_branch_actions::internal::list_virtual_branches; use gitbutler_branch_actions::{update_workspace_commit, RemoteBranchFile}; use gitbutler_cherry_pick::{ConflictedTreeKey, RepositoryExt as _}; -use gitbutler_command_context::CommandContext; +use gitbutler_command_context::{gix_repository_for_merging, CommandContext}; use gitbutler_commit::{ commit_ext::CommitExt, commit_headers::{CommitHeadersV2, HasCommitHeaders}, @@ -18,6 +18,7 @@ use gitbutler_operating_modes::{ operating_mode, read_edit_mode_metadata, write_edit_mode_metadata, EditModeMetadata, OperatingMode, EDIT_BRANCH_REF, WORKSPACE_BRANCH_REF, }; +use gitbutler_oxidize::{git2_to_gix_object_id, gix_to_git2_index, GixRepositoryExt}; use gitbutler_project::access::{WorktreeReadPermission, WorktreeWritePermission}; use gitbutler_reference::{ReferenceName, Refname}; use gitbutler_repo::{rebase::cherry_rebase, RepositoryExt}; @@ -28,42 +29,47 @@ use serde::Serialize; pub mod commands; +/// Returns an index of the the tree of `commit` if it is unconflicted, *or* produce a merged tree +/// if `commit` is conflicted. That tree is turned into an index that records the conflicts that occurred +/// during the merge. fn get_commit_index(repository: &git2::Repository, commit: &git2::Commit) -> Result { let commit_tree = commit.tree().context("Failed to get commit's tree")?; // Checkout the commit as unstaged changes if commit.is_conflicted() { let base = commit_tree .get_name(".conflict-base-0") - .context("Failed to get base")?; - let base = repository - .find_tree(base.id()) - .context("Failed to find base tree")?; - // Ours + .context("Failed to get base")? + .id(); let ours = commit_tree .get_name(".conflict-side-0") - .context("Failed to get base")?; - let ours = repository - .find_tree(ours.id()) - .context("Failed to find base tree")?; - // Theirs + .context("Failed to get base")? + .id(); let theirs = commit_tree .get_name(".conflict-side-1") - .context("Failed to get base")?; - let theirs = repository - .find_tree(theirs.id()) - .context("Failed to find base tree")?; - - let index = repository - .merge_trees(&base, &ours, &theirs, None) - .context("Failed to merge trees")?; - - Ok(index) + .context("Failed to get base")? + .id(); + + let gix_repo = gix_repository_for_merging(repository.path())?; + // Merge without favoring a side this time to get a tree containing the actual conflicts. + let mut merge_result = gix_repo.merge_trees( + git2_to_gix_object_id(base), + git2_to_gix_object_id(ours), + git2_to_gix_object_id(theirs), + gix_repo.default_merge_labels(), + gix_repo.tree_merge_options()?, + )?; + let merged_tree_id = merge_result.tree.write()?; + let mut index = gix_repo.index_from_tree(&merged_tree_id)?; + if !merge_result.index_changed_after_applying_conflicts( + &mut index, + gix::merge::tree::TreatAsUnresolved::git(), + ) { + tracing::warn!("There must be an issue with conflict-commit creation as re-merging the conflicting trees didn't yield a conflicting index."); + } + gix_to_git2_index(&index) } else { let mut index = git2::Index::new()?; - index - .read_tree(&commit_tree) - .context("Failed to set index tree")?; - + index.read_tree(&commit_tree)?; Ok(index) } } diff --git a/crates/gitbutler-oxidize/src/lib.rs b/crates/gitbutler-oxidize/src/lib.rs index 2e576af2d2..18f8384826 100644 --- a/crates/gitbutler-oxidize/src/lib.rs +++ b/crates/gitbutler-oxidize/src/lib.rs @@ -53,3 +53,56 @@ pub fn gix_to_git2_signature( &time, )?) } + +/// Convert a `gix` index into a `git2` one. +/// +/// Note that this is quite inefficient as it will have to re-allocate all paths. +/// +/// ## Note +/// +/// * Flags aren't fully supported right now, they are truncated, but good enough to get the stage right. +/// +/// ## IMPORTANT +/// +/// When removing this in favor of using the native `gix` index, do not forget to prune `REMOVE`d entries +/// or otherwise handle them. +pub fn gix_to_git2_index(index: &gix::index::State) -> anyhow::Result { + let mut out = git2::Index::new()?; + for entry @ gix::index::Entry { + stat: + gix::index::entry::Stat { + mtime, + ctime, + dev, + ino, + uid, + gid, + size, + }, + id, + flags, + mode, + .. + } in index.entries() + { + if flags.contains(gix::index::entry::Flags::REMOVE) { + continue; + } + let git2_entry = git2::IndexEntry { + ctime: git2::IndexTime::new(ctime.secs as i32, ctime.nsecs), + mtime: git2::IndexTime::new(mtime.secs as i32, mtime.nsecs), + dev: *dev, + ino: *ino, + mode: mode.bits(), + uid: *uid, + gid: *gid, + file_size: *size, + id: gix_to_git2_oid(*id), + flags: flags.bits() as u16, + flags_extended: 0, + path: entry.path(index).to_owned().into(), + }; + out.add(&git2_entry)? + } + Ok(out) +}