diff --git a/crates/gitbutler-branch-actions/src/branch.rs b/crates/gitbutler-branch-actions/src/branch.rs index 1bca47235..735a9c35f 100644 --- a/crates/gitbutler-branch-actions/src/branch.rs +++ b/crates/gitbutler-branch-actions/src/branch.rs @@ -1,4 +1,3 @@ -use crate::integration::get_workspace_head; use crate::{RemoteBranchFile, VirtualBranchesExt}; use anyhow::{bail, Context, Result}; use bstr::{BStr, ByteSlice}; @@ -10,7 +9,7 @@ use gitbutler_command_context::CommandContext; use gitbutler_diff::DiffByPathMap; use gitbutler_project::access::WorktreeReadPermission; use gitbutler_reference::normalize_branch_name; -use gitbutler_repo::GixRepositoryExt; +use gitbutler_repo::{GixRepositoryExt, RepositoryExt as _}; use gitbutler_serde::BStringForFrontend; use gix::object::tree::diff::Action; use gix::prelude::ObjectIdExt; @@ -31,7 +30,7 @@ pub(crate) fn get_uncommited_files_raw( ctx: &CommandContext, _permission: &WorktreeReadPermission, ) -> Result { - gitbutler_diff::workdir(ctx.repository(), get_workspace_head(ctx)?) + gitbutler_diff::workdir(ctx.repository(), ctx.repository().head_commit()?.id()) .context("Failed to list uncommited files") } diff --git a/crates/gitbutler-branch-actions/src/upstream_integration.rs b/crates/gitbutler-branch-actions/src/upstream_integration.rs index 881b60c9e..c8a8526d7 100644 --- a/crates/gitbutler-branch-actions/src/upstream_integration.rs +++ b/crates/gitbutler-branch-actions/src/upstream_integration.rs @@ -613,6 +613,8 @@ mod test { let tempdir = tempdir().unwrap(); let repository = git2::Repository::init(tempdir.path()).unwrap(); let initial_commit = commit_file(&repository, None, &[("foo.txt", "bar")]); + // Create refs/heads/master + repository.branch("master", &initial_commit, false).unwrap(); let old_target = commit_file(&repository, Some(&initial_commit), &[("foo.txt", "baz")]); let branch_head = commit_file(&repository, Some(&old_target), &[("foo.txt", "fux")]); let new_target = commit_file(&repository, Some(&old_target), &[("foo.txt", "qux")]); diff --git a/crates/gitbutler-edit-mode/src/lib.rs b/crates/gitbutler-edit-mode/src/lib.rs index 5a2996f73..6e484b3b0 100644 --- a/crates/gitbutler-edit-mode/src/lib.rs +++ b/crates/gitbutler-edit-mode/src/lib.rs @@ -27,7 +27,6 @@ use gitbutler_repo::{ pub mod commands; pub const EDIT_UNCOMMITED_FILES_REF: &str = "refs/gitbutler/edit_uncommited_files"; -pub const EDIT_INITIAL_STATE_REF: &str = "refs/gitbutler/edit_initial_state"; fn save_uncommited_files(ctx: &CommandContext) -> Result<()> { let repository = ctx.repository(); @@ -123,7 +122,6 @@ fn checkout_edit_branch(ctx: &CommandContext, commit: &git2::Commit) -> Result<( commit.parent(0)? }; repository.reference(EDIT_BRANCH_REF, commit_parent.id(), true, "")?; - repository.reference(EDIT_INITIAL_STATE_REF, commit_parent.id(), true, "")?; repository.set_head(EDIT_BRANCH_REF)?; repository.checkout_head(Some(CheckoutBuilder::new().force().remove_untracked(true)))?; @@ -140,18 +138,6 @@ fn checkout_edit_branch(ctx: &CommandContext, commit: &git2::Commit) -> Result<( ), )?; - let tree = repository.create_wd_tree()?; - - // Commit initial state commit - repository.commit( - Some(EDIT_INITIAL_STATE_REF), - &author_signature, - &committer_signature, - "Initial state commit", - &tree, - &[&commit_parent], - )?; - Ok(()) } @@ -365,12 +351,9 @@ pub(crate) fn starting_index_state( let commit_parent = commit.parent(0)?; let commit_parent_tree = repository.find_real_tree(&commit_parent, Default::default())?; - let initial_state = repository - .find_reference(EDIT_INITIAL_STATE_REF)? - .peel_to_tree()?; + let index = get_commit_index(repository, &commit)?; - let diff = - repository.diff_tree_to_tree(Some(&commit_parent_tree), Some(&initial_state), None)?; + let diff = repository.diff_tree_to_index(Some(&commit_parent_tree), Some(&index), None)?; let diff_files = hunks_by_filepath(Some(repository), &diff)? .into_iter() diff --git a/crates/gitbutler-repo/src/rebase.rs b/crates/gitbutler-repo/src/rebase.rs index fc832194b..b3daa8937 100644 --- a/crates/gitbutler-repo/src/rebase.rs +++ b/crates/gitbutler-repo/src/rebase.rs @@ -10,7 +10,7 @@ use gitbutler_commit::{ }; use gitbutler_error::error::Marker; -use crate::{LogUntil, RepositoryExt as _}; +use crate::{temporary_workdir::TemporaryWorkdir, LogUntil, RepositoryExt as _}; /// cherry-pick based rebase, which handles empty commits /// this function takes a commit range and generates a Vector of commit oids @@ -73,7 +73,7 @@ pub fn cherry_rebase_group( return Ok(to_rebase); }; - let cherrypick_index = repository + let mut cherrypick_index = repository .cherry_pick_gitbutler(&head, &to_rebase, None) .context("failed to cherry pick")?; @@ -81,7 +81,12 @@ pub fn cherry_rebase_group( if !succeeding_rebases { return Err(anyhow!("failed to rebase")).context(Marker::BranchConflict); } - commit_conflicted_cherry_result(repository, head, to_rebase, cherrypick_index) + commit_conflicted_cherry_result( + repository, + head, + to_rebase, + &mut cherrypick_index, + ) } else { commit_unconflicted_cherry_result(repository, head, to_rebase, cherrypick_index) } @@ -141,7 +146,7 @@ fn commit_conflicted_cherry_result<'repository>( repository: &'repository git2::Repository, head: git2::Commit, to_rebase: git2::Commit, - cherrypick_index: git2::Index, + cherrypick_index: &mut git2::Index, ) -> Result> { let commit_headers = to_rebase.gitbutler_headers(); @@ -158,42 +163,9 @@ fn commit_conflicted_cherry_result<'repository>( b"You have checked out a GitButler Conflicted commit. You probably didn't mean to do this."; let readme_blob = repository.blob(readme_content)?; - let mut conflicted_files = Vec::new(); + let conflicted_files = resolve_index(repository, cherrypick_index)?; - // get a list of conflicted files from the index - let index_conflicts = cherrypick_index.conflicts()?.flatten().collect::>(); - for conflict in index_conflicts { - // For some reason we have to resolve the index with the "their" side - // rather than the "our" side, so we then go and later overwrite the - // output tree with the "our" side. - let index_entry = conflict.their.or(conflict.our); - if let Some(entry) = index_entry { - let path = std::str::from_utf8(&entry.path).unwrap().to_string(); - conflicted_files.push(path) - } - } - - let mut resolved_index = repository.cherry_pick_gitbutler( - &head, - &to_rebase, - Some(git2::MergeOptions::default().file_favor(git2::FileFavor::Ours)), - )?; - - let resolved_index_conflicts = resolved_index.conflicts()?.flatten().collect::>(); - for conflict in resolved_index_conflicts { - if let (Some(their), None) = (&conflict.their, &conflict.our) { - let path = std::str::from_utf8(&their.path).unwrap(); - let path = Path::new(path); - resolved_index.remove_path(path)?; - } - if let (None, Some(our)) = (conflict.their, conflict.our) { - let path = std::str::from_utf8(&our.path).unwrap(); - let path = Path::new(path); - resolved_index.remove_path(path)?; - } - } - - let resolved_tree_id = resolved_index.write_tree_to(repository)?; + let resolved_tree_id = cherrypick_index.write_tree_to(repository)?; // convert files into a string and save as a blob let conflicted_files_string = conflicted_files.join("\n"); @@ -255,6 +227,55 @@ fn commit_conflicted_cherry_result<'repository>( .context("failed to find commit") } +/// Automatically resolves an index with a preferences for the "our" side +/// +/// Within our rebasing and merging logic, "their" is the commit that is getting +/// cherry picked, and "our" is the commit that it is getting cherry picked on +/// to. +/// +/// This means that if we experience a conflict, we drop the changes that are +/// in the commit that is getting cherry picked in favor of what came before it +fn resolve_index( + repository: &git2::Repository, + cherrypick_index: &mut git2::Index, +) -> Result, anyhow::Error> { + let mut conflicted_files = vec![]; + let workdir = TemporaryWorkdir::open(repository)?; + workdir.repository().set_index(cherrypick_index)?; + let index_conflicts = cherrypick_index.conflicts()?.flatten().collect::>(); + + for mut conflict in index_conflicts { + if let Some(ancestor) = &conflict.ancestor { + let path = std::str::from_utf8(&ancestor.path).unwrap(); + let path = Path::new(path); + cherrypick_index.remove_path(path)?; + } + + if let (Some(their), None) = (&conflict.their, &conflict.our) { + let path = std::str::from_utf8(&their.path).unwrap(); + conflicted_files.push(path.to_string()); + let their_path = Path::new(path); + cherrypick_index.remove_path(their_path)?; + } else if let (None, Some(our)) = (&conflict.their, &mut conflict.our) { + let path = std::str::from_utf8(&our.path).unwrap(); + conflicted_files.push(path.to_string()); + let blob = repository.find_blob(our.id)?; + cherrypick_index.add_frombuffer(our, blob.content())?; + } else if let (Some(their), Some(our)) = (&conflict.their, &mut conflict.our) { + let their_path = std::str::from_utf8(&their.path).unwrap(); + let our_path = std::str::from_utf8(&our.path).unwrap(); + conflicted_files.push(our_path.to_string()); + let blob = repository.find_blob(our.id)?; + + let their_path = Path::new(their_path); + cherrypick_index.remove_path(their_path)?; + cherrypick_index.add_frombuffer(our, blob.content())?; + } + } + + Ok(conflicted_files) +} + pub fn gitbutler_merge_commits<'repository>( repository: &'repository git2::Repository, target_commit: git2::Commit<'repository>, @@ -269,45 +290,12 @@ pub fn gitbutler_merge_commits<'repository>( let target_tree = repository.find_real_tree(&target_commit, Default::default())?; let incoming_tree = repository.find_real_tree(&incoming_commit, ConflictedTreeKey::Theirs)?; - let merged_index = repository.merge_trees(&base_tree, &target_tree, &incoming_tree, None)?; + let mut merged_index = + repository.merge_trees(&base_tree, &target_tree, &incoming_tree, None)?; - let mut conflicted_files = vec![]; + let conflicted_files = resolve_index(repository, &mut merged_index)?; - // get a list of conflicted files from the index - let index_conflicts = merged_index.conflicts()?.flatten().collect::>(); - for conflict in index_conflicts { - // For some reason we have to resolve the index with the "their" side - // rather than the "our" side, so we then go and later overwrite the - // output tree with the "our" side. - let index_entry = conflict.their.or(conflict.our); - if let Some(entry) = index_entry { - let path = std::str::from_utf8(&entry.path).unwrap().to_string(); - conflicted_files.push(path) - } - } - - let mut resolved_index = repository.merge_trees( - &base_tree, - &target_tree, - &incoming_tree, - Some(git2::MergeOptions::default().file_favor(git2::FileFavor::Ours)), - )?; - - let resolved_index_conflicts = resolved_index.conflicts()?.flatten().collect::>(); - for conflict in resolved_index_conflicts { - if let (Some(their), None) = (&conflict.their, &conflict.our) { - let path = std::str::from_utf8(&their.path).unwrap(); - let path = Path::new(path); - resolved_index.remove_path(path)?; - } - if let (None, Some(our)) = (conflict.their, conflict.our) { - let path = std::str::from_utf8(&our.path).unwrap(); - let path = Path::new(path); - resolved_index.remove_path(path)?; - } - } - - let resolved_tree_id = resolved_index.write_tree_to(repository)?; + let resolved_tree_id = merged_index.write_tree_to(repository)?; let (author, committer) = repository.signatures()?;