Skip to content

Commit e22846b

Browse files
committed
Use gix-merge when merging any commits
1 parent 1573c37 commit e22846b

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(
242239
&mut index,
243240
treat_as_unresolved,
@@ -307,7 +304,7 @@ fn extract_conflicted_files(
307304
///
308305
/// The `target_commit` and `incoming_commit` must have a common ancestor.
309306
///
310-
/// If there is a merge conflict, the
307+
/// If there is a merge conflict, we will **auto-resolve** to favor *our* side, the `incoming_commit`.
311308
pub fn gitbutler_merge_commits<'repository>(
312309
repository: &'repository git2::Repository,
313310
target_commit: git2::Commit<'repository>,
@@ -326,17 +323,28 @@ pub fn gitbutler_merge_commits<'repository>(
326323

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

332343
let tree_oid;
333-
let conflicted_files;
334-
335-
if merged_index.has_conflicts() {
336-
conflicted_files = resolve_index(repository, &mut merged_index)?;
337-
338-
// Index gets resolved from the `resolve_index` call above, so we can safly write it out
339-
let resolved_tree_id = merged_index.write_tree_to(repository)?;
344+
let forced_resolution = gix::merge::tree::TreatAsUnresolved::forced_resolution();
345+
let commit_headers = if merge_result.has_unresolved_conflicts(forced_resolution) {
346+
let conflicted_files =
347+
extract_conflicted_files(merged_tree_id, merge_result, forced_resolution)?;
340348

341349
// convert files into a string and save as a blob
342350
let conflicted_files_string = toml::to_string(&conflicted_files)?;
@@ -351,7 +359,7 @@ pub fn gitbutler_merge_commits<'repository>(
351359
tree_writer.insert(&*ConflictedTreeKey::Base, base_tree.id(), 0o040000)?;
352360
tree_writer.insert(
353361
&*ConflictedTreeKey::AutoResolution,
354-
resolved_tree_id,
362+
gix_to_git2_oid(merged_tree_id),
355363
0o040000,
356364
)?;
357365
tree_writer.insert(
@@ -367,27 +375,13 @@ pub fn gitbutler_merge_commits<'repository>(
367375
tree_writer.insert("README.txt", readme_blob, 0o100644)?;
368376

369377
tree_oid = tree_writer.write().context("failed to write tree")?;
378+
conflicted_files.to_headers()
370379
} else {
371-
conflicted_files = Default::default();
372-
tree_oid = merged_index.write_tree_to(repository)?;
373-
}
374-
375-
let conflicted_file_count = conflicted_files.total_entries() as u64;
376-
377-
let commit_headers = if conflicted_file_count > 0 {
378-
CommitHeadersV2 {
379-
conflicted: Some(conflicted_file_count),
380-
..Default::default()
381-
}
382-
} else {
383-
CommitHeadersV2 {
384-
conflicted: None,
385-
..Default::default()
386-
}
380+
tree_oid = gix_to_git2_oid(merged_tree_id);
381+
CommitHeadersV2::default()
387382
};
388383

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

0 commit comments

Comments
 (0)