Skip to content

Commit a306692

Browse files
committed
Use gix-merge when merging any commits
1 parent 85de82a commit a306692

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
@@ -232,13 +229,13 @@ fn commit_conflicted_cherry_result<'repository>(
232229
}
233230

234231
fn extract_conflicted_files(
235-
tree_id: gix::Id<'_>,
232+
merged_tree_id: gix::Id<'_>,
236233
merge_result: gix::merge::tree::Outcome<'_>,
237234
treat_as_unresolved: gix::merge::tree::TreatAsUnresolved,
238235
) -> Result<ConflictEntries> {
239236
use gix::index::entry::Stage;
240-
let repo = tree_id.repo;
241-
let mut index = repo.index_from_tree(&tree_id)?;
237+
let repo = merged_tree_id.repo;
238+
let mut index = repo.index_from_tree(&merged_tree_id)?;
242239
merge_result.index_changed_after_applying_conflicts(&mut index, treat_as_unresolved);
243240
let (mut ancestor_entries, mut our_entries, mut their_entries) =
244241
(Vec::new(), Vec::new(), Vec::new());
@@ -304,7 +301,7 @@ fn extract_conflicted_files(
304301
///
305302
/// The `target_commit` and `incoming_commit` must have a common ancestor.
306303
///
307-
/// If there is a merge conflict, the
304+
/// If there is a merge conflict, we will **auto-resolve** to favor *our* side, the `incoming_commit`.
308305
pub fn gitbutler_merge_commits<'repository>(
309306
repository: &'repository git2::Repository,
310307
target_commit: git2::Commit<'repository>,
@@ -323,17 +320,28 @@ pub fn gitbutler_merge_commits<'repository>(
323320

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

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

338346
// convert files into a string and save as a blob
339347
let conflicted_files_string = toml::to_string(&conflicted_files)?;
@@ -348,7 +356,7 @@ pub fn gitbutler_merge_commits<'repository>(
348356
tree_writer.insert(&*ConflictedTreeKey::Base, base_tree.id(), 0o040000)?;
349357
tree_writer.insert(
350358
&*ConflictedTreeKey::AutoResolution,
351-
resolved_tree_id,
359+
gix_to_git2_oid(merged_tree_id),
352360
0o040000,
353361
)?;
354362
tree_writer.insert(
@@ -364,27 +372,13 @@ pub fn gitbutler_merge_commits<'repository>(
364372
tree_writer.insert("README.txt", readme_blob, 0o100644)?;
365373

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

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

0 commit comments

Comments
 (0)