// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::sync::Arc; use jujutsu_lib::commit_builder::CommitBuilder; use jujutsu_lib::op_store::{BranchTarget, RefTarget, WorkspaceId}; use jujutsu_lib::repo::ReadonlyRepo; use jujutsu_lib::settings::UserSettings; use jujutsu_lib::transaction::Transaction; use maplit::{btreemap, hashset}; use test_case::test_case; use testutils::{create_random_commit, CommitGraphBuilder, TestRepo}; #[test_case(false ; "local backend")] #[test_case(true ; "git backend")] fn test_heads_empty(use_git: bool) { let test_repo = TestRepo::init(use_git); let repo = &test_repo.repo; assert_eq!( *repo.view().heads(), hashset! {repo.store().root_commit_id().clone()} ); assert_eq!( *repo.view().public_heads(), hashset! {repo.store().root_commit_id().clone()} ); } #[test_case(false ; "local backend")] #[test_case(true ; "git backend")] fn test_heads_fork(use_git: bool) { let settings = testutils::user_settings(); let test_repo = TestRepo::init(use_git); let repo = &test_repo.repo; let mut tx = repo.start_transaction("test"); let mut graph_builder = CommitGraphBuilder::new(&settings, tx.mut_repo()); let initial = graph_builder.initial_commit(); let child1 = graph_builder.commit_with_parents(&[&initial]); let child2 = graph_builder.commit_with_parents(&[&initial]); let repo = tx.commit(); assert_eq!( *repo.view().heads(), hashset! { child1.id().clone(), child2.id().clone(), } ); } #[test_case(false ; "local backend")] #[test_case(true ; "git backend")] fn test_heads_merge(use_git: bool) { let settings = testutils::user_settings(); let test_repo = TestRepo::init(use_git); let repo = &test_repo.repo; let mut tx = repo.start_transaction("test"); let mut graph_builder = CommitGraphBuilder::new(&settings, tx.mut_repo()); let initial = graph_builder.initial_commit(); let child1 = graph_builder.commit_with_parents(&[&initial]); let child2 = graph_builder.commit_with_parents(&[&initial]); let merge = graph_builder.commit_with_parents(&[&child1, &child2]); let repo = tx.commit(); assert_eq!(*repo.view().heads(), hashset! {merge.id().clone()}); } #[test] fn test_merge_views_heads() { // Tests merging of the view's heads (by performing concurrent operations). let settings = testutils::user_settings(); let test_repo = TestRepo::init(false); let repo = &test_repo.repo; let mut tx = repo.start_transaction("test"); let mut_repo = tx.mut_repo(); let head_unchanged = create_random_commit(&settings, repo).write_to_repo(mut_repo); let head_remove_tx1 = create_random_commit(&settings, repo).write_to_repo(mut_repo); let head_remove_tx2 = create_random_commit(&settings, repo).write_to_repo(mut_repo); let public_head_unchanged = create_random_commit(&settings, repo).write_to_repo(mut_repo); mut_repo.add_public_head(&public_head_unchanged); let public_head_remove_tx1 = create_random_commit(&settings, repo).write_to_repo(mut_repo); mut_repo.add_public_head(&public_head_remove_tx1); let public_head_remove_tx2 = create_random_commit(&settings, repo).write_to_repo(mut_repo); mut_repo.add_public_head(&public_head_remove_tx2); let repo = tx.commit(); let mut tx1 = repo.start_transaction("test"); tx1.mut_repo().remove_head(head_remove_tx1.id()); tx1.mut_repo() .remove_public_head(public_head_remove_tx1.id()); let head_add_tx1 = create_random_commit(&settings, &repo).write_to_repo(tx1.mut_repo()); let public_head_add_tx1 = create_random_commit(&settings, &repo).write_to_repo(tx1.mut_repo()); tx1.mut_repo().add_public_head(&public_head_add_tx1); tx1.commit(); let mut tx2 = repo.start_transaction("test"); tx2.mut_repo().remove_head(head_remove_tx2.id()); tx2.mut_repo() .remove_public_head(public_head_remove_tx2.id()); let head_add_tx2 = create_random_commit(&settings, &repo).write_to_repo(tx2.mut_repo()); let public_head_add_tx2 = create_random_commit(&settings, &repo).write_to_repo(tx2.mut_repo()); tx2.mut_repo().add_public_head(&public_head_add_tx2); tx2.commit(); let repo = repo.reload_at_head(&settings).unwrap(); let expected_heads = hashset! { head_unchanged.id().clone(), head_add_tx1.id().clone(), head_add_tx2.id().clone(), public_head_unchanged.id().clone(), public_head_remove_tx1.id().clone(), public_head_remove_tx2.id().clone(), public_head_add_tx1.id().clone(), public_head_add_tx2.id().clone(), }; assert_eq!(repo.view().heads(), &expected_heads); let expected_public_heads = hashset! { public_head_unchanged.id().clone(), public_head_add_tx1.id().clone(), public_head_add_tx2.id().clone(), }; assert_eq!(repo.view().public_heads(), &expected_public_heads); } #[test] fn test_merge_views_checkout() { // Tests merging of the view's checkout (by performing concurrent operations). let settings = testutils::user_settings(); let test_repo = TestRepo::init(false); let repo = &test_repo.repo; // Workspace 1 gets updated in both transactions. // Workspace 2 gets updated only in tx1. // Workspace 3 gets updated only in tx2. // Workspace 4 gets deleted in tx1 and modified in tx2. // Workspace 5 gets deleted in tx2 and modified in tx1. // Workspace 6 gets added in tx1. // Workspace 7 gets added in tx2. let mut initial_tx = repo.start_transaction("test"); let commit1 = create_random_commit(&settings, repo).write_to_repo(initial_tx.mut_repo()); let commit2 = create_random_commit(&settings, repo).write_to_repo(initial_tx.mut_repo()); let commit3 = create_random_commit(&settings, repo).write_to_repo(initial_tx.mut_repo()); let ws1_id = WorkspaceId::new("ws1".to_string()); let ws2_id = WorkspaceId::new("ws2".to_string()); let ws3_id = WorkspaceId::new("ws3".to_string()); let ws4_id = WorkspaceId::new("ws4".to_string()); let ws5_id = WorkspaceId::new("ws5".to_string()); let ws6_id = WorkspaceId::new("ws6".to_string()); let ws7_id = WorkspaceId::new("ws7".to_string()); initial_tx .mut_repo() .set_wc_commit(ws1_id.clone(), commit1.id().clone()) .unwrap(); initial_tx .mut_repo() .set_wc_commit(ws2_id.clone(), commit1.id().clone()) .unwrap(); initial_tx .mut_repo() .set_wc_commit(ws3_id.clone(), commit1.id().clone()) .unwrap(); initial_tx .mut_repo() .set_wc_commit(ws4_id.clone(), commit1.id().clone()) .unwrap(); initial_tx .mut_repo() .set_wc_commit(ws5_id.clone(), commit1.id().clone()) .unwrap(); let repo = initial_tx.commit(); let mut tx1 = repo.start_transaction("test"); tx1.mut_repo() .set_wc_commit(ws1_id.clone(), commit2.id().clone()) .unwrap(); tx1.mut_repo() .set_wc_commit(ws2_id.clone(), commit2.id().clone()) .unwrap(); tx1.mut_repo().remove_wc_commit(&ws4_id); tx1.mut_repo() .set_wc_commit(ws5_id.clone(), commit2.id().clone()) .unwrap(); tx1.mut_repo() .set_wc_commit(ws6_id.clone(), commit2.id().clone()) .unwrap(); tx1.commit(); let mut tx2 = repo.start_transaction("test"); tx2.mut_repo() .set_wc_commit(ws1_id.clone(), commit3.id().clone()) .unwrap(); tx2.mut_repo() .set_wc_commit(ws3_id.clone(), commit3.id().clone()) .unwrap(); tx2.mut_repo() .set_wc_commit(ws4_id.clone(), commit3.id().clone()) .unwrap(); tx2.mut_repo().remove_wc_commit(&ws5_id); tx2.mut_repo() .set_wc_commit(ws7_id.clone(), commit3.id().clone()) .unwrap(); // Make sure the end time different, assuming the clock has sub-millisecond // precision. std::thread::sleep(std::time::Duration::from_millis(1)); tx2.commit(); let repo = repo.reload_at_head(&settings).unwrap(); // We currently arbitrarily pick the first transaction's checkout (first by // transaction end time). assert_eq!(repo.view().get_wc_commit_id(&ws1_id), Some(commit2.id())); assert_eq!(repo.view().get_wc_commit_id(&ws2_id), Some(commit2.id())); assert_eq!(repo.view().get_wc_commit_id(&ws3_id), Some(commit3.id())); assert_eq!(repo.view().get_wc_commit_id(&ws4_id), None); assert_eq!(repo.view().get_wc_commit_id(&ws5_id), None); assert_eq!(repo.view().get_wc_commit_id(&ws6_id), Some(commit2.id())); assert_eq!(repo.view().get_wc_commit_id(&ws7_id), Some(commit3.id())); } #[test] fn test_merge_views_branches() { // Tests merging of branches (by performing concurrent operations). See // test_refs.rs for tests of merging of individual ref targets. let settings = testutils::user_settings(); let test_repo = TestRepo::init(false); let repo = &test_repo.repo; let mut tx = repo.start_transaction("test"); let mut_repo = tx.mut_repo(); let main_branch_local_tx0 = create_random_commit(&settings, repo).write_to_repo(mut_repo); let main_branch_origin_tx0 = create_random_commit(&settings, repo).write_to_repo(mut_repo); let main_branch_origin_tx1 = create_random_commit(&settings, repo).write_to_repo(mut_repo); let main_branch_alternate_tx0 = create_random_commit(&settings, repo).write_to_repo(mut_repo); mut_repo.set_local_branch( "main".to_string(), RefTarget::Normal(main_branch_local_tx0.id().clone()), ); mut_repo.set_remote_branch( "main".to_string(), "origin".to_string(), RefTarget::Normal(main_branch_origin_tx0.id().clone()), ); mut_repo.set_remote_branch( "main".to_string(), "alternate".to_string(), RefTarget::Normal(main_branch_alternate_tx0.id().clone()), ); let feature_branch_local_tx0 = create_random_commit(&settings, repo).write_to_repo(mut_repo); mut_repo.set_git_ref( "feature".to_string(), RefTarget::Normal(feature_branch_local_tx0.id().clone()), ); let repo = tx.commit(); let mut tx1 = repo.start_transaction("test"); let main_branch_local_tx1 = create_random_commit(&settings, &repo).write_to_repo(tx1.mut_repo()); tx1.mut_repo().set_local_branch( "main".to_string(), RefTarget::Normal(main_branch_local_tx1.id().clone()), ); tx1.mut_repo().set_remote_branch( "main".to_string(), "origin".to_string(), RefTarget::Normal(main_branch_origin_tx1.id().clone()), ); let feature_branch_tx1 = create_random_commit(&settings, &repo).write_to_repo(tx1.mut_repo()); tx1.mut_repo().set_local_branch( "feature".to_string(), RefTarget::Normal(feature_branch_tx1.id().clone()), ); tx1.commit(); let mut tx2 = repo.start_transaction("test"); let main_branch_local_tx2 = create_random_commit(&settings, &repo).write_to_repo(tx2.mut_repo()); tx2.mut_repo().set_local_branch( "main".to_string(), RefTarget::Normal(main_branch_local_tx2.id().clone()), ); tx2.mut_repo().set_remote_branch( "main".to_string(), "origin".to_string(), RefTarget::Normal(main_branch_origin_tx1.id().clone()), ); tx2.commit(); let repo = repo.reload_at_head(&settings).unwrap(); let expected_main_branch = BranchTarget { local_target: Some(RefTarget::Conflict { removes: vec![main_branch_local_tx0.id().clone()], adds: vec![ main_branch_local_tx1.id().clone(), main_branch_local_tx2.id().clone(), ], }), remote_targets: btreemap! { "origin".to_string() => RefTarget::Normal(main_branch_origin_tx1.id().clone()), "alternate".to_string() => RefTarget::Normal(main_branch_alternate_tx0.id().clone()), }, }; let expected_feature_branch = BranchTarget { local_target: Some(RefTarget::Normal(feature_branch_tx1.id().clone())), remote_targets: btreemap! {}, }; assert_eq!( repo.view().branches(), &btreemap! { "main".to_string() => expected_main_branch, "feature".to_string() => expected_feature_branch, } ); } #[test] fn test_merge_views_tags() { // Tests merging of tags (by performing concurrent operations). See // test_refs.rs for tests of merging of individual ref targets. let settings = testutils::user_settings(); let test_repo = TestRepo::init(false); let repo = &test_repo.repo; let mut tx = repo.start_transaction("test"); let mut_repo = tx.mut_repo(); let v1_tx0 = create_random_commit(&settings, repo).write_to_repo(mut_repo); mut_repo.set_tag("v1.0".to_string(), RefTarget::Normal(v1_tx0.id().clone())); let v2_tx0 = create_random_commit(&settings, repo).write_to_repo(mut_repo); mut_repo.set_tag("v2.0".to_string(), RefTarget::Normal(v2_tx0.id().clone())); let repo = tx.commit(); let mut tx1 = repo.start_transaction("test"); let v1_tx1 = create_random_commit(&settings, &repo).write_to_repo(tx1.mut_repo()); tx1.mut_repo() .set_tag("v1.0".to_string(), RefTarget::Normal(v1_tx1.id().clone())); let v2_tx1 = create_random_commit(&settings, &repo).write_to_repo(tx1.mut_repo()); tx1.mut_repo() .set_tag("v2.0".to_string(), RefTarget::Normal(v2_tx1.id().clone())); tx1.commit(); let mut tx2 = repo.start_transaction("test"); let v1_tx2 = create_random_commit(&settings, &repo).write_to_repo(tx2.mut_repo()); tx2.mut_repo() .set_tag("v1.0".to_string(), RefTarget::Normal(v1_tx2.id().clone())); tx2.commit(); let repo = repo.reload_at_head(&settings).unwrap(); let expected_v1 = RefTarget::Conflict { removes: vec![v1_tx0.id().clone()], adds: vec![v1_tx1.id().clone(), v1_tx2.id().clone()], }; let expected_v2 = RefTarget::Normal(v2_tx1.id().clone()); assert_eq!( repo.view().tags(), &btreemap! { "v1.0".to_string() => expected_v1, "v2.0".to_string() => expected_v2, } ); } #[test] fn test_merge_views_git_refs() { // Tests merging of git refs (by performing concurrent operations). See // test_refs.rs for tests of merging of individual ref targets. let settings = testutils::user_settings(); let test_repo = TestRepo::init(false); let repo = &test_repo.repo; let mut tx = repo.start_transaction("test"); let mut_repo = tx.mut_repo(); let main_branch_tx0 = create_random_commit(&settings, repo).write_to_repo(mut_repo); mut_repo.set_git_ref( "refs/heads/main".to_string(), RefTarget::Normal(main_branch_tx0.id().clone()), ); let feature_branch_tx0 = create_random_commit(&settings, repo).write_to_repo(mut_repo); mut_repo.set_git_ref( "refs/heads/feature".to_string(), RefTarget::Normal(feature_branch_tx0.id().clone()), ); let repo = tx.commit(); let mut tx1 = repo.start_transaction("test"); let main_branch_tx1 = create_random_commit(&settings, &repo).write_to_repo(tx1.mut_repo()); tx1.mut_repo().set_git_ref( "refs/heads/main".to_string(), RefTarget::Normal(main_branch_tx1.id().clone()), ); let feature_branch_tx1 = create_random_commit(&settings, &repo).write_to_repo(tx1.mut_repo()); tx1.mut_repo().set_git_ref( "refs/heads/feature".to_string(), RefTarget::Normal(feature_branch_tx1.id().clone()), ); tx1.commit(); let mut tx2 = repo.start_transaction("test"); let main_branch_tx2 = create_random_commit(&settings, &repo).write_to_repo(tx2.mut_repo()); tx2.mut_repo().set_git_ref( "refs/heads/main".to_string(), RefTarget::Normal(main_branch_tx2.id().clone()), ); tx2.commit(); let repo = repo.reload_at_head(&settings).unwrap(); let expected_main_branch = RefTarget::Conflict { removes: vec![main_branch_tx0.id().clone()], adds: vec![main_branch_tx1.id().clone(), main_branch_tx2.id().clone()], }; let expected_feature_branch = RefTarget::Normal(feature_branch_tx1.id().clone()); assert_eq!( repo.view().git_refs(), &btreemap! { "refs/heads/main".to_string() => expected_main_branch, "refs/heads/feature".to_string() => expected_feature_branch, } ); } fn commit_transactions(settings: &UserSettings, txs: Vec) -> Arc { let repo_loader = txs[0].base_repo().loader(); let mut op_ids = vec![]; for tx in txs { op_ids.push(tx.commit().op_id().clone()); std::thread::sleep(std::time::Duration::from_millis(1)); } let repo = repo_loader.load_at_head().resolve(settings).unwrap(); // Test the setup. The assumption here is that the parent order matches the // order in which they were merged (which currently matches the transaction // commit order), so we want to know make sure they appear in a certain // order, so the caller can decide the order by passing them to this // function in a certain order. assert_eq!(*repo.operation().parent_ids(), op_ids); repo } #[test_case(false ; "rewrite first")] #[test_case(true ; "add child first")] fn test_merge_views_child_on_rewritten(child_first: bool) { // We start with just commit A. Operation 1 adds commit B on top. Operation 2 // rewrites A as A2. let settings = testutils::user_settings(); let test_repo = TestRepo::init(false); let mut tx = test_repo.repo.start_transaction("test"); let commit_a = create_random_commit(&settings, &test_repo.repo).write_to_repo(tx.mut_repo()); let repo = tx.commit(); let mut tx1 = repo.start_transaction("test"); let commit_b = create_random_commit(&settings, &repo) .set_parents(vec![commit_a.id().clone()]) .write_to_repo(tx1.mut_repo()); let mut tx2 = repo.start_transaction("test"); let commit_a2 = CommitBuilder::for_rewrite_from(&settings, &commit_a) .set_description("A2".to_string()) .write_to_repo(tx2.mut_repo()); tx2.mut_repo().rebase_descendants(&settings).unwrap(); let repo = if child_first { commit_transactions(&settings, vec![tx1, tx2]) } else { commit_transactions(&settings, vec![tx2, tx1]) }; // A new B2 commit (B rebased onto A2) should be the only head. let heads = repo.view().heads(); assert_eq!(heads.len(), 1); let b2_id = heads.iter().next().unwrap(); let commit_b2 = repo.store().get_commit(b2_id).unwrap(); assert_eq!(commit_b2.change_id(), commit_b.change_id()); assert_eq!(commit_b2.parent_ids(), vec![commit_a2.id().clone()]); } #[test_case(false, false ; "add child on unchanged, rewrite first")] #[test_case(false, true ; "add child on unchanged, add child first")] #[test_case(true, false ; "add child on rewritten, rewrite first")] #[test_case(true, true ; "add child on rewritten, add child first")] fn test_merge_views_child_on_rewritten_divergent(on_rewritten: bool, child_first: bool) { // We start with divergent commits A2 and A3. Operation 1 adds commit B on top // of A2 or A3. Operation 2 rewrites A2 as A4. The result should be that B // gets rebased onto A4 if it was based on A2 before, but if it was based on // A3, it should remain there. let settings = testutils::user_settings(); let test_repo = TestRepo::init(false); let mut tx = test_repo.repo.start_transaction("test"); let commit_a2 = create_random_commit(&settings, &test_repo.repo).write_to_repo(tx.mut_repo()); let commit_a3 = create_random_commit(&settings, &test_repo.repo) .set_change_id(commit_a2.change_id().clone()) .write_to_repo(tx.mut_repo()); let repo = tx.commit(); let mut tx1 = repo.start_transaction("test"); let parent = if on_rewritten { &commit_a2 } else { &commit_a3 }; let commit_b = create_random_commit(&settings, &repo) .set_parents(vec![parent.id().clone()]) .write_to_repo(tx1.mut_repo()); let mut tx2 = repo.start_transaction("test"); let commit_a4 = CommitBuilder::for_rewrite_from(&settings, &commit_a2) .set_description("A4".to_string()) .write_to_repo(tx2.mut_repo()); tx2.mut_repo().rebase_descendants(&settings).unwrap(); let repo = if child_first { commit_transactions(&settings, vec![tx1, tx2]) } else { commit_transactions(&settings, vec![tx2, tx1]) }; if on_rewritten { // A3 should remain as a head. The other head should be B2 (B rebased onto A4). let mut heads = repo.view().heads().clone(); assert_eq!(heads.len(), 2); assert!(heads.remove(commit_a3.id())); let b2_id = heads.iter().next().unwrap(); let commit_b2 = repo.store().get_commit(b2_id).unwrap(); assert_eq!(commit_b2.change_id(), commit_b.change_id()); assert_eq!(commit_b2.parent_ids(), vec![commit_a4.id().clone()]); } else { // No rebases should happen, so B and A4 should be the heads. let mut heads = repo.view().heads().clone(); assert_eq!(heads.len(), 2); assert!(heads.remove(commit_b.id())); assert!(heads.remove(commit_a4.id())); } } #[test_case(false ; "abandon first")] #[test_case(true ; "add child first")] fn test_merge_views_child_on_abandoned(child_first: bool) { // We start with commit B on top of commit A. Operation 1 adds commit C on top. // Operation 2 abandons B. let settings = testutils::user_settings(); let test_repo = TestRepo::init(false); let mut tx = test_repo.repo.start_transaction("test"); let commit_a = create_random_commit(&settings, &test_repo.repo).write_to_repo(tx.mut_repo()); let commit_b = create_random_commit(&settings, &test_repo.repo) .set_parents(vec![commit_a.id().clone()]) .write_to_repo(tx.mut_repo()); let repo = tx.commit(); let mut tx1 = repo.start_transaction("test"); let commit_c = create_random_commit(&settings, &repo) .set_parents(vec![commit_b.id().clone()]) .write_to_repo(tx1.mut_repo()); let mut tx2 = repo.start_transaction("test"); tx2.mut_repo() .record_abandoned_commit(commit_b.id().clone()); tx2.mut_repo().rebase_descendants(&settings).unwrap(); let repo = if child_first { commit_transactions(&settings, vec![tx1, tx2]) } else { commit_transactions(&settings, vec![tx2, tx1]) }; // A new C2 commit (C rebased onto A) should be the only head. let heads = repo.view().heads(); assert_eq!(heads.len(), 1); let id_c2 = heads.iter().next().unwrap(); let commit_c2 = repo.store().get_commit(id_c2).unwrap(); assert_eq!(commit_c2.change_id(), commit_c.change_id()); assert_eq!(commit_c2.parent_ids(), vec![commit_a.id().clone()]); }