Skip to content

Commit 8149b2a

Browse files
committed
Use gix-merge when merging any commits
1 parent 859983e commit 8149b2a

File tree

1 file changed

+30
-242
lines changed

1 file changed

+30
-242
lines changed

crates/gitbutler-repo/src/rebase.rs

Lines changed: 30 additions & 242 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
use std::{
2-
collections::HashSet,
3-
path::{Path, PathBuf},
4-
};
1+
use std::{collections::HashSet, path::PathBuf};
52

63
use crate::{LogUntil, RepositoryExt as _};
74
use anyhow::{Context, Result};
@@ -12,7 +9,7 @@ use gitbutler_commit::{
129
commit_ext::CommitExt,
1310
commit_headers::{CommitHeadersV2, HasCommitHeaders},
1411
};
15-
use gitbutler_oxidize::gix_to_git2_oid;
12+
use gitbutler_oxidize::{git2_to_gix_object_id, gix_to_git2_oid};
1613
use serde::{Deserialize, Serialize};
1714

1815
/// cherry-pick based rebase, which handles empty commits
@@ -231,13 +228,13 @@ fn commit_conflicted_cherry_result<'repository>(
231228
}
232229

233230
fn extract_conflicted_files(
234-
tree_id: gix::Id<'_>,
231+
merged_tree_id: gix::Id<'_>,
235232
merge_result: gix::merge::tree::Outcome<'_>,
236233
treat_as_unresolved: gix::merge::tree::TreatAsUnresolved,
237234
) -> Result<ConflictEntries> {
238235
use gix::index::entry::Stage;
239-
let repo = tree_id.repo;
240-
let mut index = repo.index_from_tree(&tree_id)?;
236+
let repo = merged_tree_id.repo;
237+
let mut index = repo.index_from_tree(&merged_tree_id)?;
241238
merge_result.index_changed_after_applying_conflicts(&mut index, treat_as_unresolved);
242239
let (mut ancestor_entries, mut our_entries, mut their_entries) =
243240
(Vec::new(), Vec::new(), Vec::new());
@@ -303,7 +300,7 @@ fn extract_conflicted_files(
303300
///
304301
/// The `target_commit` and `incoming_commit` must have a common ancestor.
305302
///
306-
/// If there is a merge conflict, the
303+
/// If there is a merge conflict, we will **auto-resolve** to favor *our* side, the `incoming_commit`.
307304
pub fn gitbutler_merge_commits<'repository>(
308305
repository: &'repository git2::Repository,
309306
target_commit: git2::Commit<'repository>,
@@ -322,17 +319,28 @@ pub fn gitbutler_merge_commits<'repository>(
322319

323320
let target_merge_tree = repository.find_real_tree(&target_commit, Default::default())?;
324321
let incoming_merge_tree = repository.find_real_tree(&incoming_commit, Default::default())?;
325-
let mut merged_index =
326-
repository.merge_trees(&base_tree, &incoming_merge_tree, &target_merge_tree, None)?;
322+
let gix_repo = gix_repository_for_merging(repository.path())?;
323+
let mut merge_result = gix_repo.merge_trees(
324+
git2_to_gix_object_id(base_tree.id()),
325+
git2_to_gix_object_id(incoming_merge_tree.id()),
326+
git2_to_gix_object_id(target_merge_tree.id()),
327+
gix::merge::blob::builtin_driver::text::Labels {
328+
ancestor: Some("base".into()),
329+
current: Some("ours".into()),
330+
other: Some("theirs".into()),
331+
},
332+
gix_repo
333+
.tree_merge_options()?
334+
.with_tree_favor(Some(gix::merge::tree::TreeFavor::Ours))
335+
.with_file_favor(Some(gix::merge::tree::FileFavor::Ours)),
336+
)?;
337+
let merged_tree_id = merge_result.tree.write()?;
327338

328339
let tree_oid;
329-
let conflicted_files;
330-
331-
if merged_index.has_conflicts() {
332-
conflicted_files = resolve_index(repository, &mut merged_index)?;
333-
334-
// Index gets resolved from the `resolve_index` call above, so we can safly write it out
335-
let resolved_tree_id = merged_index.write_tree_to(repository)?;
340+
let forced_resolution = gix::merge::tree::TreatAsUnresolved::forced_resolution();
341+
let commit_headers = if merge_result.has_unresolved_conflicts(forced_resolution) {
342+
let conflicted_files =
343+
extract_conflicted_files(merged_tree_id, merge_result, forced_resolution)?;
336344

337345
// convert files into a string and save as a blob
338346
let conflicted_files_string = toml::to_string(&conflicted_files)?;
@@ -347,7 +355,7 @@ pub fn gitbutler_merge_commits<'repository>(
347355
tree_writer.insert(&*ConflictedTreeKey::Base, base_tree.id(), 0o040000)?;
348356
tree_writer.insert(
349357
&*ConflictedTreeKey::AutoResolution,
350-
resolved_tree_id,
358+
gix_to_git2_oid(merged_tree_id),
351359
0o040000,
352360
)?;
353361
tree_writer.insert(
@@ -363,27 +371,13 @@ pub fn gitbutler_merge_commits<'repository>(
363371
tree_writer.insert("README.txt", readme_blob, 0o100644)?;
364372

365373
tree_oid = tree_writer.write().context("failed to write tree")?;
374+
conflicted_files.to_headers()
366375
} else {
367-
conflicted_files = Default::default();
368-
tree_oid = merged_index.write_tree_to(repository)?;
369-
}
370-
371-
let conflicted_file_count = conflicted_files.total_entries() as u64;
372-
373-
let commit_headers = if conflicted_file_count > 0 {
374-
CommitHeadersV2 {
375-
conflicted: Some(conflicted_file_count),
376-
..Default::default()
377-
}
378-
} else {
379-
CommitHeadersV2 {
380-
conflicted: None,
381-
..Default::default()
382-
}
376+
tree_oid = gix_to_git2_oid(merged_tree_id);
377+
CommitHeadersV2::default()
383378
};
384379

385380
let (author, committer) = repository.signatures()?;
386-
387381
let commit_oid = crate::RepositoryExt::commit_with_signature(
388382
repository,
389383
None,
@@ -449,209 +443,3 @@ impl ConflictEntries {
449443
}
450444
}
451445
}
452-
453-
/// Automatically resolves an index with a preferences for the "our" side
454-
///
455-
/// Within our rebasing and merging logic, "their" is the commit that is getting
456-
/// cherry picked, and "our" is the commit that it is getting cherry picked on
457-
/// to.
458-
///
459-
/// This means that if we experience a conflict, we drop the changes that are
460-
/// in the commit that is getting cherry picked in favor of what came before it
461-
fn resolve_index(
462-
repository: &git2::Repository,
463-
index: &mut git2::Index,
464-
) -> Result<ConflictEntries, anyhow::Error> {
465-
fn bytes_to_path(path: &[u8]) -> Result<PathBuf> {
466-
let path = std::str::from_utf8(path)?;
467-
Ok(Path::new(path).to_owned())
468-
}
469-
470-
let mut ancestor_entries = vec![];
471-
let mut our_entries = vec![];
472-
let mut their_entries = vec![];
473-
474-
// Set the index on an in-memory repository
475-
let in_memory_repository = repository.in_memory_repo()?;
476-
in_memory_repository.set_index(index)?;
477-
478-
let index_conflicts = index.conflicts()?.flatten().collect::<Vec<_>>();
479-
480-
for mut conflict in index_conflicts {
481-
// There may be a case when there is an ancestor in the index without
482-
// a "their" OR "our" side. This is probably caused by the same file
483-
// getting renamed and modified in the two commits.
484-
if let Some(ancestor) = &conflict.ancestor {
485-
let path = bytes_to_path(&ancestor.path)?;
486-
index.remove_path(&path)?;
487-
488-
ancestor_entries.push(path);
489-
}
490-
491-
if let (Some(their), None) = (&conflict.their, &conflict.our) {
492-
// Their (the commit we're rebasing)'s change gets dropped
493-
let their_path = bytes_to_path(&their.path)?;
494-
index.remove_path(&their_path)?;
495-
496-
their_entries.push(their_path);
497-
} else if let (None, Some(our)) = (&conflict.their, &mut conflict.our) {
498-
// Our (the commit we're rebasing onto)'s gets kept
499-
let blob = repository.find_blob(our.id)?;
500-
our.flags = 0; // For some unknown reason we need to set flags to 0
501-
index.add_frombuffer(our, blob.content())?;
502-
503-
let our_path = bytes_to_path(&our.path)?;
504-
505-
our_entries.push(our_path);
506-
} else if let (Some(their), Some(our)) = (&conflict.their, &mut conflict.our) {
507-
// We keep our (the commit we're rebasing onto)'s side of the
508-
// conflict
509-
let their_path = bytes_to_path(&their.path)?;
510-
let blob = repository.find_blob(our.id)?;
511-
512-
index.remove_path(&their_path)?;
513-
our.flags = 0; // For some unknown reason we need to set flags to 0
514-
index.add_frombuffer(our, blob.content())?;
515-
516-
let our_path = bytes_to_path(&our.path)?;
517-
518-
their_entries.push(their_path);
519-
our_entries.push(our_path);
520-
}
521-
}
522-
523-
Ok(ConflictEntries {
524-
ancestor_entries,
525-
our_entries,
526-
their_entries,
527-
})
528-
}
529-
530-
#[cfg(test)]
531-
mod test {
532-
#[cfg(test)]
533-
mod resolve_index {
534-
use crate::rebase::resolve_index;
535-
use gitbutler_testsupport::testing_repository::TestingRepository;
536-
537-
#[test]
538-
fn test_same_file_twice() {
539-
let test_repository = TestingRepository::open();
540-
541-
// Make some commits
542-
let a = test_repository.commit_tree(None, &[("foo.txt", "a")]);
543-
let b = test_repository.commit_tree(None, &[("foo.txt", "b")]);
544-
let c = test_repository.commit_tree(None, &[("foo.txt", "c")]);
545-
test_repository.commit_tree(None, &[("foo.txt", "asdfasdf")]);
546-
547-
// Merge the index
548-
let mut index: git2::Index = test_repository
549-
.repository
550-
.merge_trees(
551-
&a.tree().unwrap(), // Base
552-
&b.tree().unwrap(), // Ours
553-
&c.tree().unwrap(), // Theirs
554-
None,
555-
)
556-
.unwrap();
557-
558-
assert!(index.has_conflicts());
559-
560-
// Call our index resolution function
561-
resolve_index(&test_repository.repository, &mut index).unwrap();
562-
563-
// Ensure there are no conflicts
564-
assert!(!index.has_conflicts());
565-
566-
let tree = index.write_tree_to(&test_repository.repository).unwrap();
567-
let tree: git2::Tree = test_repository.repository.find_tree(tree).unwrap();
568-
569-
let blob = tree.get_name("foo.txt").unwrap().id(); // We fail here to get the entry because the tree is empty
570-
let blob: git2::Blob = test_repository.repository.find_blob(blob).unwrap();
571-
572-
assert_eq!(blob.content(), b"b")
573-
}
574-
575-
#[test]
576-
fn test_diverging_renames() {
577-
let test_repository = TestingRepository::open();
578-
579-
// Make some commits
580-
let a = test_repository.commit_tree(None, &[("foo.txt", "a")]);
581-
let b = test_repository.commit_tree(None, &[("bar.txt", "a")]);
582-
let c = test_repository.commit_tree(None, &[("baz.txt", "a")]);
583-
test_repository.commit_tree(None, &[("foo.txt", "asdfasdf")]);
584-
585-
// Merge the index
586-
let mut index: git2::Index = test_repository
587-
.repository
588-
.merge_trees(
589-
&a.tree().unwrap(), // Base
590-
&b.tree().unwrap(), // Ours
591-
&c.tree().unwrap(), // Theirs
592-
None,
593-
)
594-
.unwrap();
595-
596-
assert!(index.has_conflicts());
597-
598-
// Call our index resolution function
599-
resolve_index(&test_repository.repository, &mut index).unwrap();
600-
601-
// Ensure there are no conflicts
602-
assert!(!index.has_conflicts());
603-
604-
let tree = index.write_tree_to(&test_repository.repository).unwrap();
605-
let tree: git2::Tree = test_repository.repository.find_tree(tree).unwrap();
606-
607-
assert!(tree.get_name("foo.txt").is_none());
608-
assert!(tree.get_name("baz.txt").is_none());
609-
610-
let blob = tree.get_name("bar.txt").unwrap().id(); // We fail here to get the entry because the tree is empty
611-
let blob: git2::Blob = test_repository.repository.find_blob(blob).unwrap();
612-
613-
assert_eq!(blob.content(), b"a")
614-
}
615-
616-
#[test]
617-
fn test_converging_renames() {
618-
let test_repository = TestingRepository::open();
619-
620-
// Make some commits
621-
let a = test_repository.commit_tree(None, &[("foo.txt", "a"), ("bar.txt", "b")]);
622-
let b = test_repository.commit_tree(None, &[("baz.txt", "a")]);
623-
let c = test_repository.commit_tree(None, &[("baz.txt", "b")]);
624-
test_repository.commit_tree(None, &[("foo.txt", "asdfasdf")]);
625-
626-
// Merge the index
627-
let mut index: git2::Index = test_repository
628-
.repository
629-
.merge_trees(
630-
&a.tree().unwrap(), // Base
631-
&b.tree().unwrap(), // Ours
632-
&c.tree().unwrap(), // Theirs
633-
None,
634-
)
635-
.unwrap();
636-
637-
assert!(index.has_conflicts());
638-
639-
// Call our index resolution function
640-
resolve_index(&test_repository.repository, &mut index).unwrap();
641-
642-
// Ensure there are no conflicts
643-
assert!(!index.has_conflicts());
644-
645-
let tree = index.write_tree_to(&test_repository.repository).unwrap();
646-
let tree: git2::Tree = test_repository.repository.find_tree(tree).unwrap();
647-
648-
assert!(tree.get_name("foo.txt").is_none());
649-
assert!(tree.get_name("bar.txt").is_none());
650-
651-
let blob = tree.get_name("baz.txt").unwrap().id(); // We fail here to get the entry because the tree is empty
652-
let blob: git2::Blob = test_repository.repository.find_blob(blob).unwrap();
653-
654-
assert_eq!(blob.content(), b"a")
655-
}
656-
}
657-
}

0 commit comments

Comments
 (0)