diff --git a/Cargo.lock b/Cargo.lock index d6c9da242f..4f36dd9c93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2111,9 +2111,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.18.3" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" dependencies = [ "bitflags 2.6.0", "libc", @@ -4797,9 +4797,9 @@ checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libgit2-sys" -version = "0.16.2+1.7.2" +version = "0.17.0+1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index f69f6f3a01..2b4a7defb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,8 +38,9 @@ resolver = "2" [workspace.dependencies] bstr = "1.10.0" # Add the `tracing` or `tracing-detail` features to see more of gitoxide in the logs. Useful to see which programs it invokes. -gix = { git = "https://github.com/Byron/gitoxide", rev = "72daa46bad9d397ef2cc48a3cffda23f414ccd8a", default-features = false, features = [] } -git2 = { version = "0.18.3", features = [ +gix = { git = "https://github.com/Byron/gitoxide", rev = "72daa46bad9d397ef2cc48a3cffda23f414ccd8a", default-features = false, features = [ +] } +git2 = { version = "0.19.0", features = [ "vendored-openssl", "vendored-libgit2", ] } @@ -95,4 +96,4 @@ debug = true # Enable debug symbols, for profiling [profile.bench] codegen-units = 256 lto = false -opt-level = 3 \ No newline at end of file +opt-level = 3 diff --git a/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs b/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs index 3401afd924..7e4571b019 100644 --- a/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs +++ b/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs @@ -308,7 +308,7 @@ impl BranchManager<'_> { // if not, we need to merge or rebase the branch to get it up to date let merge_base = repo - .merge_base(default_target.sha, dbg!(branch.head)) + .merge_base(default_target.sha, branch.head) .context(format!( "failed to find merge base between {} and {}", default_target.sha, branch.head diff --git a/crates/gitbutler-branch-actions/src/status.rs b/crates/gitbutler-branch-actions/src/status.rs index 971371797c..82ab988f3b 100644 --- a/crates/gitbutler-branch-actions/src/status.rs +++ b/crates/gitbutler-branch-actions/src/status.rs @@ -20,6 +20,7 @@ use gitbutler_project::access::WorktreeWritePermission; use tracing::instrument; /// Represents the uncommitted status of the applied virtual branches in the workspace. +#[derive(Debug)] pub struct VirtualBranchesStatus { /// A collection of branches and their associated uncommitted file changes. pub branches: Vec<(Branch, Vec)>, diff --git a/crates/gitbutler-branch-actions/src/upstream_integration.rs b/crates/gitbutler-branch-actions/src/upstream_integration.rs index 619c577987..34458cbdc1 100644 --- a/crates/gitbutler-branch-actions/src/upstream_integration.rs +++ b/crates/gitbutler-branch-actions/src/upstream_integration.rs @@ -478,44 +478,12 @@ fn compute_resolutions( #[cfg(test)] mod test { - use std::fs; - use gitbutler_branch::BranchOwnershipClaims; - use tempfile::tempdir; + use gitbutler_testsupport::testing_repository::TestingRepository; use uuid::Uuid; use super::*; - fn commit_file<'a>( - repository: &'a git2::Repository, - parent: Option<&git2::Commit>, - files: &[(&str, &str)], - ) -> git2::Commit<'a> { - for (file_name, contents) in files { - fs::write(repository.path().join("..").join(file_name), contents).unwrap(); - } - let mut index = repository.index().unwrap(); - // Make sure we're not having weird cached state - index.read(true).unwrap(); - index - .add_all(["*"], git2::IndexAddOption::DEFAULT, None) - .unwrap(); - - let signature = git2::Signature::now("Caleb", "caleb@gitbutler.com").unwrap(); - let commit = repository - .commit( - None, - &signature, - &signature, - "Committee", - &repository.find_tree(index.write_tree().unwrap()).unwrap(), - parent.map(|c| vec![c]).unwrap_or_default().as_slice(), - ) - .unwrap(); - - repository.find_commit(commit).unwrap() - } - fn make_branch(head: git2::Oid, tree: git2::Oid) -> Branch { Branch { id: Uuid::new_v4().into(), @@ -540,16 +508,15 @@ mod test { #[test] fn test_up_to_date_if_head_commits_equivalent() { - let tempdir = tempdir().unwrap(); - let repository = git2::Repository::init(tempdir.path()).unwrap(); - let initial_commit = commit_file(&repository, None, &[("foo.txt", "bar")]); - let head_commit = commit_file(&repository, Some(&initial_commit), &[("foo.txt", "baz")]); + let test_repository = TestingRepository::open(); + let initial_commit = test_repository.commit_tree(None, &[("foo.txt", "bar")]); + let head_commit = test_repository.commit_tree(Some(&initial_commit), &[("foo.txt", "baz")]); let context = UpstreamIntegrationContext { _permission: None, old_target: head_commit.clone(), new_target: head_commit, - repository: &repository, + repository: &test_repository.repository, virtual_branches_in_workspace: vec![], target_branch_name: "main".to_string(), }; @@ -562,17 +529,16 @@ mod test { #[test] fn test_updates_required_if_new_head_ahead() { - let tempdir = tempdir().unwrap(); - let repository = git2::Repository::init(tempdir.path()).unwrap(); - let initial_commit = commit_file(&repository, None, &[("foo.txt", "bar")]); - let old_target = commit_file(&repository, Some(&initial_commit), &[("foo.txt", "baz")]); - let new_target = commit_file(&repository, Some(&old_target), &[("foo.txt", "qux")]); + let test_repository = TestingRepository::open(); + let initial_commit = test_repository.commit_tree(None, &[("foo.txt", "bar")]); + let old_target = test_repository.commit_tree(Some(&initial_commit), &[("foo.txt", "baz")]); + let new_target = test_repository.commit_tree(Some(&old_target), &[("foo.txt", "qux")]); let context = UpstreamIntegrationContext { _permission: None, old_target, new_target, - repository: &repository, + repository: &test_repository.repository, virtual_branches_in_workspace: vec![], target_branch_name: "main".to_string(), }; @@ -585,11 +551,10 @@ mod test { #[test] fn test_empty_branch() { - let tempdir = tempdir().unwrap(); - let repository = git2::Repository::init(tempdir.path()).unwrap(); - let initial_commit = commit_file(&repository, None, &[("foo.txt", "bar")]); - let old_target = commit_file(&repository, Some(&initial_commit), &[("foo.txt", "baz")]); - let new_target = commit_file(&repository, Some(&old_target), &[("foo.txt", "qux")]); + let test_repository = TestingRepository::open(); + let initial_commit = test_repository.commit_tree(None, &[("foo.txt", "bar")]); + let old_target = test_repository.commit_tree(Some(&initial_commit), &[("foo.txt", "baz")]); + let new_target = test_repository.commit_tree(Some(&old_target), &[("foo.txt", "qux")]); let branch = make_branch(old_target.id(), old_target.tree_id()); @@ -597,7 +562,7 @@ mod test { _permission: None, old_target, new_target, - repository: &repository, + repository: &test_repository.repository, virtual_branches_in_workspace: vec![branch.clone()], target_branch_name: "main".to_string(), }; @@ -610,16 +575,11 @@ mod test { #[test] fn test_conflicted_head_branch() { - let tempdir = tempdir().unwrap(); - let repository = - git2::Repository::init_opts(tempdir.path(), &gitbutler_testsupport::init_opts()) - .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")]); + let test_repository = TestingRepository::open(); + let initial_commit = test_repository.commit_tree(None, &[("foo.txt", "bar")]); + let old_target = test_repository.commit_tree(Some(&initial_commit), &[("foo.txt", "baz")]); + let branch_head = test_repository.commit_tree(Some(&old_target), &[("foo.txt", "fux")]); + let new_target = test_repository.commit_tree(Some(&old_target), &[("foo.txt", "qux")]); let branch = make_branch(branch_head.id(), branch_head.tree_id()); @@ -627,7 +587,7 @@ mod test { _permission: None, old_target, new_target: new_target.clone(), - repository: &repository, + repository: &test_repository.repository, virtual_branches_in_workspace: vec![branch.clone()], target_branch_name: "main".to_string(), }; @@ -657,11 +617,12 @@ mod test { panic!("Should be variant UpdatedObjects") }; - let head_commit = repository.find_commit(head).unwrap(); + let head_commit = test_repository.repository.find_commit(head).unwrap(); assert_eq!(head_commit.parent(0).unwrap().id(), new_target.id()); assert!(head_commit.is_conflicted()); - let head_tree = repository + let head_tree = test_repository + .repository .find_real_tree(&head_commit, Default::default()) .unwrap(); assert_eq!(head_tree.id(), tree) @@ -669,12 +630,11 @@ mod test { #[test] fn test_conflicted_tree_branch() { - let tempdir = tempdir().unwrap(); - let repository = git2::Repository::init(tempdir.path()).unwrap(); - let initial_commit = commit_file(&repository, None, &[("foo.txt", "bar")]); - 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")]); + let test_repository = TestingRepository::open(); + let initial_commit = test_repository.commit_tree(None, &[("foo.txt", "bar")]); + let old_target = test_repository.commit_tree(Some(&initial_commit), &[("foo.txt", "baz")]); + let branch_head = test_repository.commit_tree(Some(&old_target), &[("foo.txt", "fux")]); + let new_target = test_repository.commit_tree(Some(&old_target), &[("foo.txt", "qux")]); let branch = make_branch(old_target.id(), branch_head.tree_id()); @@ -682,7 +642,7 @@ mod test { _permission: None, old_target, new_target, - repository: &repository, + repository: &test_repository.repository, virtual_branches_in_workspace: vec![branch.clone()], target_branch_name: "main".to_string(), }; @@ -700,13 +660,12 @@ mod test { #[test] fn test_conflicted_head_and_tree_branch() { - let tempdir = tempdir().unwrap(); - let repository = git2::Repository::init(tempdir.path()).unwrap(); - let initial_commit = commit_file(&repository, None, &[("foo.txt", "bar")]); - 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 branch_tree = commit_file(&repository, Some(&old_target), &[("foo.txt", "bax")]); - let new_target = commit_file(&repository, Some(&old_target), &[("foo.txt", "qux")]); + let test_repository = TestingRepository::open(); + let initial_commit = test_repository.commit_tree(None, &[("foo.txt", "bar")]); + let old_target = test_repository.commit_tree(Some(&initial_commit), &[("foo.txt", "baz")]); + let branch_head = test_repository.commit_tree(Some(&old_target), &[("foo.txt", "fux")]); + let branch_tree = test_repository.commit_tree(Some(&old_target), &[("foo.txt", "bax")]); + let new_target = test_repository.commit_tree(Some(&old_target), &[("foo.txt", "qux")]); let branch = make_branch(branch_head.id(), branch_tree.tree_id()); @@ -714,7 +673,7 @@ mod test { _permission: None, old_target, new_target, - repository: &repository, + repository: &test_repository.repository, virtual_branches_in_workspace: vec![branch.clone()], target_branch_name: "main".to_string(), }; @@ -732,11 +691,10 @@ mod test { #[test] fn test_integrated() { - let tempdir = tempdir().unwrap(); - let repository = git2::Repository::init(tempdir.path()).unwrap(); - let initial_commit = commit_file(&repository, None, &[("foo.txt", "bar")]); - let old_target = commit_file(&repository, Some(&initial_commit), &[("foo.txt", "baz")]); - let new_target = commit_file(&repository, Some(&old_target), &[("foo.txt", "qux")]); + let test_repository = TestingRepository::open(); + let initial_commit = test_repository.commit_tree(None, &[("foo.txt", "bar")]); + let old_target = test_repository.commit_tree(Some(&initial_commit), &[("foo.txt", "baz")]); + let new_target = test_repository.commit_tree(Some(&old_target), &[("foo.txt", "qux")]); let branch = make_branch(new_target.id(), new_target.tree_id()); @@ -744,7 +702,7 @@ mod test { _permission: None, old_target, new_target, - repository: &repository, + repository: &test_repository.repository, virtual_branches_in_workspace: vec![branch.clone()], target_branch_name: "main".to_string(), }; @@ -757,25 +715,17 @@ mod test { #[test] fn test_integrated_commit_with_uncommited_changes() { - let tempdir = tempdir().unwrap(); - let repository = git2::Repository::init(tempdir.path()).unwrap(); + let test_repository = TestingRepository::open(); let initial_commit = - commit_file(&repository, None, &[("foo.txt", "bar"), ("bar.txt", "bar")]); - let old_target = commit_file( - &repository, + test_repository.commit_tree(None, &[("foo.txt", "bar"), ("bar.txt", "bar")]); + let old_target = test_repository.commit_tree( Some(&initial_commit), &[("foo.txt", "baz"), ("bar.txt", "bar")], ); - let new_target = commit_file( - &repository, - Some(&old_target), - &[("foo.txt", "qux"), ("bar.txt", "bar")], - ); - let tree = commit_file( - &repository, - Some(&old_target), - &[("foo.txt", "baz"), ("bar.txt", "qux")], - ); + let new_target = test_repository + .commit_tree(Some(&old_target), &[("foo.txt", "qux"), ("bar.txt", "bar")]); + let tree = test_repository + .commit_tree(Some(&old_target), &[("foo.txt", "baz"), ("bar.txt", "qux")]); let branch = make_branch(new_target.id(), tree.tree_id()); @@ -783,7 +733,7 @@ mod test { _permission: None, old_target, new_target, - repository: &repository, + repository: &test_repository.repository, virtual_branches_in_workspace: vec![branch.clone()], target_branch_name: "main".to_string(), }; @@ -796,31 +746,23 @@ mod test { #[test] fn test_safly_updatable() { - let tempdir = tempdir().unwrap(); - let repository = git2::Repository::init(tempdir.path()).unwrap(); - let initial_commit = commit_file( - &repository, - None, - &[("files-one.txt", "foo"), ("file-two.txt", "foo")], - ); - let old_target = commit_file( - &repository, + let test_repository = TestingRepository::open(); + let initial_commit = + test_repository.commit_tree(None, &[("files-one.txt", "foo"), ("file-two.txt", "foo")]); + let old_target = test_repository.commit_tree( Some(&initial_commit), &[("file-one.txt", "bar"), ("file-two.txt", "foo")], ); - let new_target = commit_file( - &repository, + let new_target = test_repository.commit_tree( Some(&old_target), &[("file-one.txt", "baz"), ("file-two.txt", "foo")], ); - let branch_head = commit_file( - &repository, + let branch_head = test_repository.commit_tree( Some(&old_target), &[("file-one.txt", "bar"), ("file-two.txt", "bar")], ); - let branch_tree = commit_file( - &repository, + let branch_tree = test_repository.commit_tree( Some(&branch_head), &[("file-one.txt", "bar"), ("file-two.txt", "baz")], ); @@ -831,7 +773,7 @@ mod test { _permission: None, old_target, new_target, - repository: &repository, + repository: &test_repository.repository, virtual_branches_in_workspace: vec![branch.clone()], target_branch_name: "main".to_string(), }; diff --git a/crates/gitbutler-branch-actions/tests/extra/mod.rs b/crates/gitbutler-branch-actions/tests/extra/mod.rs index 8ff2e45920..a80097e3fb 100644 --- a/crates/gitbutler-branch-actions/tests/extra/mod.rs +++ b/crates/gitbutler-branch-actions/tests/extra/mod.rs @@ -11,6 +11,7 @@ use std::{ }; use anyhow::{Context, Result}; +use bstr::ByteSlice; use git2::TreeEntry; use gitbutler_branch::{ BranchCreateRequest, BranchOwnershipClaims, BranchUpdateRequest, Target, VirtualBranchesHandle, @@ -754,6 +755,27 @@ fn commit_id_can_be_generated_or_specified() -> Result<()> { Ok(()) } +/// This sets up the following scenario: +/// +/// Target commit: +/// test.txt: line1\nline2\nline3\nline4\n +/// +/// Make commit "last push": +/// test.txt: line1\nline2\nline3\nline4\nupstream\n +/// +/// "Server side" origin/master: +/// test.txt: line1\nline2\nline3\nline4\nupstream\ncoworker work\n +/// +/// Write uncommited: +/// test.txt: line1\nline2\nline3\nline4\nupstream\n +/// test2.txt: file2\n +/// +/// Create vbranch: +/// - set head to "last push" +/// +/// Inspect Virtual branch: +/// commited: test.txt: line1\nline2\nline3\nline4\n+upstream\n +/// uncommited: test2.txt: file2\n #[test] fn merge_vbranch_upstream_clean_rebase() -> Result<()> { let suite = Suite::default(); @@ -821,6 +843,7 @@ fn merge_vbranch_upstream_clean_rebase() -> Result<()> { let mut branch = branch_manager .create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission()) .expect("failed to create virtual branch"); + branch.upstream = Some(remote_branch.clone()); branch.head = last_push; vb_state.set_branch(branch.clone())?; @@ -829,13 +852,32 @@ fn merge_vbranch_upstream_clean_rebase() -> Result<()> { let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?; assert_eq!(branches.len(), 1); let branch1 = &branches[0]; + assert_eq!( branch1.files.len(), - 1 + 1, - "'test' (modified compared to index) and 'test2' (untracked).\ - This is actually correct when looking at the git repository" + 1, + "test2.txt contains uncommited changes" + ); + assert_eq!(branch1.files[0].path.to_str().unwrap(), "test2.txt"); + assert_eq!( + branch1.files[0].hunks[0].diff.to_str().unwrap(), + "@@ -0,0 +1 @@\n+file2\n" + ); + + assert_eq!( + branch1.commits.len(), + 1, + "test.txt is commited inside this commit" + ); + assert_eq!(branch1.commits[0].files.len(), 1); + assert_eq!( + branch1.commits[0].files[0].path.to_str().unwrap(), + "test.txt" + ); + assert_eq!( + branch1.commits[0].files[0].hunks[0].diff.to_str().unwrap(), + "@@ -2,3 +2,4 @@ line1\n line2\n line3\n line4\n+upstream\n" ); - assert_eq!(branch1.commits.len(), 1); // assert_eq!(branch1.upstream.as_ref().unwrap().commits.len(), 1); internal::integrate_upstream_commits(ctx, branch1.id)?; diff --git a/crates/gitbutler-repo/src/rebase.rs b/crates/gitbutler-repo/src/rebase.rs index 48659b1043..a0b799da4f 100644 --- a/crates/gitbutler-repo/src/rebase.rs +++ b/crates/gitbutler-repo/src/rebase.rs @@ -343,7 +343,7 @@ pub fn gitbutler_merge_commits<'repository>( /// 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, + index: &mut git2::Index, ) -> Result, anyhow::Error> { fn bytes_to_path(path: &[u8]) -> Result { let path = std::str::from_utf8(path)?; @@ -354,9 +354,9 @@ fn resolve_index( // Set the index on an in-memory repository let in_memory_repository = repository.in_memory_repo()?; - in_memory_repository.set_index(cherrypick_index)?; + in_memory_repository.set_index(index)?; - let index_conflicts = cherrypick_index.conflicts()?.flatten().collect::>(); + let index_conflicts = index.conflicts()?.flatten().collect::>(); for mut conflict in index_conflicts { // There may be a case when there is an ancestor in the index without @@ -364,19 +364,20 @@ fn resolve_index( // getting renamed and modified in the two commits. if let Some(ancestor) = &conflict.ancestor { let path = bytes_to_path(&ancestor.path)?; - cherrypick_index.remove_path(&path)?; + index.remove_path(&path)?; } if let (Some(their), None) = (&conflict.their, &conflict.our) { // Their (the commit we're rebasing)'s change gets dropped let their_path = bytes_to_path(&their.path)?; - cherrypick_index.remove_path(&their_path)?; + index.remove_path(&their_path)?; conflicted_files.push(their_path); } else if let (None, Some(our)) = (&conflict.their, &mut conflict.our) { // Our (the commit we're rebasing onto)'s gets kept let blob = repository.find_blob(our.id)?; - cherrypick_index.add_frombuffer(our, blob.content())?; + our.flags = 0; // For some unknown reason we need to set flags to 0 + index.add_frombuffer(our, blob.content())?; let our_path = bytes_to_path(&our.path)?; conflicted_files.push(our_path); @@ -386,8 +387,9 @@ fn resolve_index( let their_path = bytes_to_path(&their.path)?; let blob = repository.find_blob(our.id)?; - cherrypick_index.remove_path(&their_path)?; - cherrypick_index.add_frombuffer(our, blob.content())?; + index.remove_path(&their_path)?; + our.flags = 0; // For some unknown reason we need to set flags to 0 + index.add_frombuffer(our, blob.content())?; let our_path = bytes_to_path(&our.path)?; conflicted_files.push(our_path); @@ -396,3 +398,133 @@ fn resolve_index( Ok(conflicted_files) } + +#[cfg(test)] +mod test { + #[cfg(test)] + mod resolve_index { + use gitbutler_testsupport::testing_repository::TestingRepository; + + use crate::rebase::resolve_index; + + #[test] + fn test_same_file_twice() { + let test_repository = TestingRepository::open(); + + // Make some commits + let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); + let b = test_repository.commit_tree(None, &[("foo.txt", "b")]); + let c = test_repository.commit_tree(None, &[("foo.txt", "c")]); + test_repository.commit_tree(None, &[("foo.txt", "asdfasdf")]); + + // Merge the index + let mut index: git2::Index = test_repository + .repository + .merge_trees( + &a.tree().unwrap(), // Base + &b.tree().unwrap(), // Ours + &c.tree().unwrap(), // Theirs + None, + ) + .unwrap(); + + assert!(index.has_conflicts()); + + // Call our index resolution function + resolve_index(&test_repository.repository, &mut index).unwrap(); + + // Ensure there are no conflicts + assert!(!index.has_conflicts()); + + let tree = index.write_tree_to(&test_repository.repository).unwrap(); + let tree: git2::Tree = test_repository.repository.find_tree(tree).unwrap(); + + let blob = tree.get_name("foo.txt").unwrap().id(); // We fail here to get the entry because the tree is empty + let blob: git2::Blob = test_repository.repository.find_blob(blob).unwrap(); + + assert_eq!(blob.content(), b"b") + } + + #[test] + fn test_diverging_renames() { + let test_repository = TestingRepository::open(); + + // Make some commits + let a = test_repository.commit_tree(None, &[("foo.txt", "a")]); + let b = test_repository.commit_tree(None, &[("bar.txt", "a")]); + let c = test_repository.commit_tree(None, &[("baz.txt", "a")]); + test_repository.commit_tree(None, &[("foo.txt", "asdfasdf")]); + + // Merge the index + let mut index: git2::Index = test_repository + .repository + .merge_trees( + &a.tree().unwrap(), // Base + &b.tree().unwrap(), // Ours + &c.tree().unwrap(), // Theirs + None, + ) + .unwrap(); + + assert!(index.has_conflicts()); + + // Call our index resolution function + resolve_index(&test_repository.repository, &mut index).unwrap(); + + // Ensure there are no conflicts + assert!(!index.has_conflicts()); + + let tree = index.write_tree_to(&test_repository.repository).unwrap(); + let tree: git2::Tree = test_repository.repository.find_tree(tree).unwrap(); + + assert!(tree.get_name("foo.txt").is_none()); + assert!(tree.get_name("baz.txt").is_none()); + + let blob = tree.get_name("bar.txt").unwrap().id(); // We fail here to get the entry because the tree is empty + let blob: git2::Blob = test_repository.repository.find_blob(blob).unwrap(); + + assert_eq!(blob.content(), b"a") + } + + #[test] + fn test_converging_renames() { + let test_repository = TestingRepository::open(); + + // Make some commits + let a = test_repository.commit_tree(None, &[("foo.txt", "a"), ("bar.txt", "b")]); + let b = test_repository.commit_tree(None, &[("baz.txt", "a")]); + let c = test_repository.commit_tree(None, &[("baz.txt", "b")]); + test_repository.commit_tree(None, &[("foo.txt", "asdfasdf")]); + + // Merge the index + let mut index: git2::Index = test_repository + .repository + .merge_trees( + &a.tree().unwrap(), // Base + &b.tree().unwrap(), // Ours + &c.tree().unwrap(), // Theirs + None, + ) + .unwrap(); + + assert!(index.has_conflicts()); + + // Call our index resolution function + resolve_index(&test_repository.repository, &mut index).unwrap(); + + // Ensure there are no conflicts + assert!(!index.has_conflicts()); + + let tree = index.write_tree_to(&test_repository.repository).unwrap(); + let tree: git2::Tree = test_repository.repository.find_tree(tree).unwrap(); + + assert!(tree.get_name("foo.txt").is_none()); + assert!(tree.get_name("bar.txt").is_none()); + + let blob = tree.get_name("baz.txt").unwrap().id(); // We fail here to get the entry because the tree is empty + let blob: git2::Blob = test_repository.repository.find_blob(blob).unwrap(); + + assert_eq!(blob.content(), b"a") + } + } +} diff --git a/crates/gitbutler-testsupport/src/lib.rs b/crates/gitbutler-testsupport/src/lib.rs index 74838d1d27..0c734fef1e 100644 --- a/crates/gitbutler-testsupport/src/lib.rs +++ b/crates/gitbutler-testsupport/src/lib.rs @@ -7,6 +7,8 @@ pub use test_project::TestProject; mod suite; pub use suite::*; +pub mod testing_repository; + pub mod paths { use tempfile::TempDir; diff --git a/crates/gitbutler-testsupport/src/testing_repository.rs b/crates/gitbutler-testsupport/src/testing_repository.rs new file mode 100644 index 0000000000..b0a7f69422 --- /dev/null +++ b/crates/gitbutler-testsupport/src/testing_repository.rs @@ -0,0 +1,69 @@ +use std::fs; + +use tempfile::{tempdir, TempDir}; + +pub struct TestingRepository { + pub repository: git2::Repository, + pub tempdir: TempDir, +} + +impl TestingRepository { + pub fn open() -> Self { + let tempdir = tempdir().unwrap(); + let repository = git2::Repository::init(tempdir.path()).unwrap(); + + Self { + tempdir, + repository, + } + } + + pub fn commit_tree<'a>( + &'a self, + parent: Option<&git2::Commit<'a>>, + files: &[(&str, &str)], + ) -> git2::Commit<'a> { + // Remove everything other than the .git folder + for entry in fs::read_dir(self.tempdir.path()).unwrap() { + let entry = entry.unwrap(); + if entry.file_name() != ".git" { + let path = entry.path(); + if path.is_dir() { + fs::remove_dir_all(path).unwrap(); + } else { + fs::remove_file(path).unwrap(); + } + } + } + // Write any files + for (file_name, contents) in files { + fs::write(self.tempdir.path().join(file_name), contents).unwrap(); + } + + // Update the index + let mut index = self.repository.index().unwrap(); + // Make sure we're not having weird cached state + index.read(true).unwrap(); + index + .add_all(["*"], git2::IndexAddOption::DEFAULT, None) + .unwrap(); + + let signature = git2::Signature::now("Caleb", "caleb@gitbutler.com").unwrap(); + let commit = self + .repository + .commit( + None, + &signature, + &signature, + "Committee", + &self + .repository + .find_tree(index.write_tree().unwrap()) + .unwrap(), + parent.map(|c| vec![c]).unwrap_or_default().as_slice(), + ) + .unwrap(); + + self.repository.find_commit(commit).unwrap() + } +}