// Copyright 2020 The Jujutsu Authors//// 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::path::Path;use std::slice;use std::sync::Arc;use std::time::SystemTime; use assert_matches::assert_matches;use futures::TryStreamExt as _;use itertools::Itertools as _;use jj_lib::backend::CommitId;use jj_lib::config::ConfigLayer;use jj_lib::config::ConfigSource;use jj_lib::evolution::walk_predecessors;use jj_lib::index::Index;use jj_lib::object_id::ObjectId as _;use jj_lib::op_store::OperationId;use jj_lib::op_walk;use jj_lib::op_walk::OpsetEvaluationError;use jj_lib::op_walk::OpsetResolutionError;use jj_lib::operation::Operation;use jj_lib::repo::ReadonlyRepo;use jj_lib::repo::Repo;use jj_lib::settings::UserSettings;use pollster::FutureExt as _;use testutils::CommitBuilderExt as _;use testutils::TestRepo;use testutils::TestResult;use testutils::write_random_commit;use testutils::write_random_commit_with_parents; fn get_predecessors(repo: &ReadonlyRepo, id: &CommitId) -> Vec<CommitId> { let entries: Vec<_> = walk_predecessors(repo, slice::from_ref(id)) .try_collect() .block_on() .expect("unreachable predecessors shouldn't be visited"); let first = entries .first() .expect("specified commit should be reachable"); first.predecessor_ids().to_vec()} fn list_dir(dir: &Path) -> Vec<String> { std::fs::read_dir(dir) .unwrap() .map(|entry| entry.unwrap().file_name().to_str().unwrap().to_owned()) .sorted() .collect()} fn index_has_id(index: &dyn Index, commit_id: &CommitId) -> bool { index.has_id(commit_id).unwrap()} #[test]fn test_unpublished_operation() -> TestResult { // Test that the operation doesn't get published until that's requested. let test_repo = TestRepo::init(); let repo = &test_repo.repo; let op_heads_dir = test_repo.repo_path().join("op_heads").join("heads"); let op_id0 = repo.op_id().clone(); assert_eq!(list_dir(&op_heads_dir), vec![repo.op_id().hex()]); let mut tx1 = repo.start_transaction(); write_random_commit(tx1.repo_mut()); let unpublished_op = tx1.write("transaction 1").block_on()?; let op_id1 = unpublished_op.operation().id().clone(); assert_ne!(op_id1, op_id0); assert_eq!(list_dir(&op_heads_dir), vec![op_id0.hex()]); unpublished_op.publish().block_on()?; assert_eq!(list_dir(&op_heads_dir), vec![op_id1.hex()]); Ok(())} #[test]fn test_consecutive_operations() -> TestResult { // Test that consecutive operations result in a single op-head on disk after // each operation let test_repo = TestRepo::init(); let repo = &test_repo.repo; let op_heads_dir = test_repo.repo_path().join("op_heads").join("heads"); let op_id0 = repo.op_id().clone(); assert_eq!(list_dir(&op_heads_dir), vec![repo.op_id().hex()]); let mut tx1 = repo.start_transaction(); write_random_commit(tx1.repo_mut()); let op_id1 = tx1 .commit("transaction 1") .block_on()? .operation() .id() .clone(); assert_ne!(op_id1, op_id0); assert_eq!(list_dir(&op_heads_dir), vec![op_id1.hex()]); let repo = repo.reload_at_head().block_on()?; let mut tx2 = repo.start_transaction(); write_random_commit(tx2.repo_mut()); let op_id2 = tx2 .commit("transaction 2") .block_on()? .operation() .id() .clone(); assert_ne!(op_id2, op_id0); assert_ne!(op_id2, op_id1); assert_eq!(list_dir(&op_heads_dir), vec![op_id2.hex()]); // Reloading the repo makes no difference (there are no conflicting operations // to resolve). let _repo = repo.reload_at_head().block_on()?; assert_eq!(list_dir(&op_heads_dir), vec![op_id2.hex()]); Ok(())} #[test]fn test_concurrent_operations() -> TestResult { // Test that consecutive operations result in multiple op-heads on disk until // the repo has been reloaded (which currently happens right away). let test_repo = TestRepo::init(); let repo = &test_repo.repo; let op_heads_dir = test_repo.repo_path().join("op_heads").join("heads"); let op_id0 = repo.op_id().clone(); assert_eq!(list_dir(&op_heads_dir), vec![repo.op_id().hex()]); let mut tx1 = repo.start_transaction(); write_random_commit(tx1.repo_mut()); let op_id1 = tx1 .commit("transaction 1") .block_on()? .operation() .id() .clone(); assert_ne!(op_id1, op_id0); assert_eq!(list_dir(&op_heads_dir), vec![op_id1.hex()]); // After both transactions have committed, we should have two op-heads on disk, // since they were run in parallel. let mut tx2 = repo.start_transaction(); write_random_commit(tx2.repo_mut()); let op_id2 = tx2 .commit("transaction 2") .block_on()? .operation() .id() .clone(); assert_ne!(op_id2, op_id0); assert_ne!(op_id2, op_id1); let mut actual_heads_on_disk = list_dir(&op_heads_dir); actual_heads_on_disk.sort(); let mut expected_heads_on_disk = vec![op_id1.hex(), op_id2.hex()]; expected_heads_on_disk.sort(); assert_eq!(actual_heads_on_disk, expected_heads_on_disk); // Reloading the repo causes the operations to be merged let repo = repo.reload_at_head().block_on()?; let merged_op_id = repo.op_id().clone(); assert_ne!(merged_op_id, op_id0); assert_ne!(merged_op_id, op_id1); assert_ne!(merged_op_id, op_id2); assert_eq!(list_dir(&op_heads_dir), vec![merged_op_id.hex()]); Ok(())} fn assert_heads(repo: &dyn Repo, expected: Vec<&CommitId>) { let expected = expected.iter().copied().cloned().collect(); assert_eq!(*repo.view().heads(), expected);} #[test]fn test_isolation() -> TestResult { // Test that two concurrent transactions don't see each other's changes. let test_repo = TestRepo::init(); let repo = &test_repo.repo; let mut tx = repo.start_transaction(); let initial = write_random_commit_with_parents(tx.repo_mut(), &[]); let repo = tx.commit("test").block_on()?; let mut tx1 = repo.start_transaction(); let mut_repo1 = tx1.repo_mut(); let mut tx2 = repo.start_transaction(); let mut_repo2 = tx2.repo_mut(); assert_heads(repo.as_ref(), vec![initial.id()]); assert_heads(mut_repo1, vec![initial.id()]); assert_heads(mut_repo2, vec![initial.id()]); let rewrite1 = mut_repo1 .rewrite_commit(&initial) .set_description("rewrite1") .write_unwrap(); mut_repo1.rebase_descendants().block_on()?; let rewrite2 = mut_repo2 .rewrite_commit(&initial) .set_description("rewrite2") .write_unwrap(); mut_repo2.rebase_descendants().block_on()?; // Neither transaction has committed yet, so each transaction sees its own // commit. assert_heads(repo.as_ref(), vec![initial.id()]); assert_heads(mut_repo1, vec![rewrite1.id()]); assert_heads(mut_repo2, vec![rewrite2.id()]); // The base repo and tx2 don't see the commits from tx1. tx1.commit("transaction 1").block_on()?; assert_heads(repo.as_ref(), vec![initial.id()]); assert_heads(mut_repo2, vec![rewrite2.id()]); // The base repo still doesn't see the commits after both transactions commit. tx2.commit("transaction 2").block_on()?; assert_heads(repo.as_ref(), vec![initial.id()]); // After reload, the base repo sees both rewrites. let repo = repo.reload_at_head().block_on()?; assert_heads(repo.as_ref(), vec![rewrite1.id(), rewrite2.id()]); Ok(())} #[test]fn test_stored_commit_predecessors() -> TestResult { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let loader = repo.loader(); let mut tx = repo.start_transaction(); let commit1 = write_random_commit(tx.repo_mut()); let commit2 = tx .repo_mut() .rewrite_commit(&commit1) .set_description("rewritten") .write_unwrap(); tx.repo_mut().rebase_descendants().block_on()?; let repo = tx.commit("test").block_on()?; // Reload operation from disk. let op = loader.load_operation(repo.op_id()).block_on()?; assert!(op.stores_commit_predecessors()); assert_matches!(op.predecessors_for_commit(commit1.id()), Some([])); assert_matches!(op.predecessors_for_commit(commit2.id()), Some([id]) if id == commit1.id()); // Save operation without the predecessors as old jj would do. let mut data = op.store_operation().clone(); data.commit_predecessors = None; let op_id = loader.op_store().write_operation(&data).block_on()?; assert_ne!(&op_id, op.id()); let op = loader.load_operation(&op_id).block_on()?; assert!(!op.stores_commit_predecessors()); Ok(())} #[test]fn test_reparent_range_linear() -> TestResult { let test_repo = TestRepo::init(); let repo_0 = test_repo.repo; let loader = repo_0.loader(); let op_store = repo_0.op_store(); let read_op = |id| loader.load_operation(id).block_on().unwrap(); fn op_parents<const N: usize>(op: &Operation) -> [Operation; N] { let parents = op.parents().block_on().unwrap(); parents.try_into().unwrap() } // Set up linear operation graph: // D // C // B // A // 0 (initial) let random_tx = |repo: &Arc<ReadonlyRepo>| { let mut tx = repo.start_transaction(); write_random_commit(tx.repo_mut()); tx }; let repo_a = random_tx(&repo_0).commit("op A").block_on()?; let repo_b = random_tx(&repo_a).commit("op B").block_on()?; let repo_c = random_tx(&repo_b).commit("op C").block_on()?; let repo_d = random_tx(&repo_c).commit("op D").block_on()?; // Reparent B..D (=C|D) onto A: // D' // C' // A // 0 (initial) let stats = op_walk::reparent_range( op_store.as_ref(), slice::from_ref(repo_b.operation()), slice::from_ref(repo_d.operation()), repo_a.operation(), ) .block_on()?; assert_eq!(stats.new_head_ids.len(), 1); assert_eq!(stats.rewritten_count, 2); assert_eq!(stats.unreachable_count, 1); let new_op_d = read_op(&stats.new_head_ids[0]); assert_eq!(new_op_d.metadata(), repo_d.operation().metadata()); assert_eq!(new_op_d.view_id(), repo_d.operation().view_id()); let [new_op_c] = op_parents(&new_op_d); assert_eq!(new_op_c.metadata(), repo_c.operation().metadata()); assert_eq!(new_op_c.view_id(), repo_c.operation().view_id()); assert_eq!(new_op_c.parent_ids(), slice::from_ref(repo_a.op_id())); // Reparent empty range onto A let stats = op_walk::reparent_range( op_store.as_ref(), slice::from_ref(repo_d.operation()), slice::from_ref(repo_d.operation()), repo_a.operation(), ) .block_on()?; assert_eq!(stats.new_head_ids, vec![repo_a.op_id().clone()]); assert_eq!(stats.rewritten_count, 0); assert_eq!(stats.unreachable_count, 3); Ok(())} #[test]fn test_reparent_range_branchy() -> TestResult { let test_repo = TestRepo::init(); let repo_0 = test_repo.repo; let loader = repo_0.loader(); let op_store = repo_0.op_store(); let read_op = |id| loader.load_operation(id).block_on().unwrap(); fn op_parents<const N: usize>(op: &Operation) -> [Operation; N] { let parents = op.parents().block_on().unwrap(); parents.try_into().unwrap() } // Set up branchy operation graph: // G // |\ // | F // E | // D | // |/ // C // B // A // 0 (initial) let random_tx = |repo: &Arc<ReadonlyRepo>| { let mut tx = repo.start_transaction(); write_random_commit(tx.repo_mut()); tx }; let repo_a = random_tx(&repo_0).commit("op A").block_on()?; let repo_b = random_tx(&repo_a).commit("op B").block_on()?; let repo_c = random_tx(&repo_b).commit("op C").block_on()?; let repo_d = random_tx(&repo_c).commit("op D").block_on()?; let tx_e = random_tx(&repo_d); let tx_f = random_tx(&repo_c); let repo_g = testutils::commit_transactions(vec![tx_e, tx_f]); let [op_e, op_f] = op_parents(repo_g.operation()); // Reparent D..G (= E|F|G) onto B: // G' // |\ // | F' // E'| // |/ // B // A // 0 (initial) let stats = op_walk::reparent_range( op_store.as_ref(), slice::from_ref(repo_d.operation()), slice::from_ref(repo_g.operation()), repo_b.operation(), ) .block_on()?; assert_eq!(stats.new_head_ids.len(), 1); assert_eq!(stats.rewritten_count, 3); assert_eq!(stats.unreachable_count, 2); let new_op_g = read_op(&stats.new_head_ids[0]); assert_eq!(new_op_g.metadata(), repo_g.operation().metadata()); assert_eq!(new_op_g.view_id(), repo_g.operation().view_id()); let [new_op_e, new_op_f] = op_parents(&new_op_g); assert_eq!(new_op_e.parent_ids(), slice::from_ref(repo_b.op_id())); assert_eq!(new_op_f.parent_ids(), slice::from_ref(repo_b.op_id())); // Reparent B..G (=C|D|E|F|G) onto A: // G' // |\ // | F' // E'| // D'| // |/ // C' // A // 0 (initial) let stats = op_walk::reparent_range( op_store.as_ref(), slice::from_ref(repo_b.operation()), slice::from_ref(repo_g.operation()), repo_a.operation(), ) .block_on()?; assert_eq!(stats.new_head_ids.len(), 1); assert_eq!(stats.rewritten_count, 5); assert_eq!(stats.unreachable_count, 1); let new_op_g = read_op(&stats.new_head_ids[0]); assert_eq!(new_op_g.metadata(), repo_g.operation().metadata()); assert_eq!(new_op_g.view_id(), repo_g.operation().view_id()); let [new_op_e, new_op_f] = op_parents(&new_op_g); let [new_op_d] = op_parents(&new_op_e); assert_eq!(new_op_d.parent_ids(), new_op_f.parent_ids()); let [new_op_c] = op_parents(&new_op_d); assert_eq!(new_op_c.parent_ids(), slice::from_ref(repo_a.op_id())); // Reparent (E|F)..G (=G) onto D: // G' // D // C // B // A // 0 (initial) let stats = op_walk::reparent_range( op_store.as_ref(), &[op_e.clone(), op_f.clone()], slice::from_ref(repo_g.operation()), repo_d.operation(), ) .block_on()?; assert_eq!(stats.new_head_ids.len(), 1); assert_eq!(stats.rewritten_count, 1); assert_eq!(stats.unreachable_count, 2); let new_op_g = read_op(&stats.new_head_ids[0]); assert_eq!(new_op_g.metadata(), repo_g.operation().metadata()); assert_eq!(new_op_g.view_id(), repo_g.operation().view_id()); assert_eq!(new_op_g.parent_ids(), slice::from_ref(repo_d.op_id())); // Reparent C..F (=F) onto D (ignoring G): // F' // D // C // B // A // 0 (initial) let stats = op_walk::reparent_range( op_store.as_ref(), slice::from_ref(repo_c.operation()), slice::from_ref(&op_f), repo_d.operation(), ) .block_on()?; assert_eq!(stats.new_head_ids.len(), 1); assert_eq!(stats.rewritten_count, 1); assert_eq!(stats.unreachable_count, 0); let new_op_f = read_op(&stats.new_head_ids[0]); assert_eq!(new_op_f.metadata(), op_f.metadata()); assert_eq!(new_op_f.view_id(), op_f.view_id()); assert_eq!(new_op_f.parent_ids(), slice::from_ref(repo_d.op_id())); Ok(())} #[test]fn test_reparent_discarding_predecessors() -> TestResult { let test_repo = TestRepo::init(); let repo_0 = test_repo.repo; let loader = repo_0.loader(); let op_store = repo_0.op_store(); let repo_at = |id: &OperationId| { let op = loader.load_operation(id).block_on().unwrap(); loader.load_at(&op).block_on().unwrap() }; let head_commits = |repo: &dyn Repo| { repo.view() .heads() .iter() .map(|id| repo.store().get_commit(id).unwrap()) .collect_vec() }; // Set up rewriting as follows: // // op1 op2 op3 op4 // B0 B0 B1 B1 // | | | | // A0 A0 A1 A0 A1 A2 let mut tx = repo_0.start_transaction(); let commit_a0 = write_random_commit(tx.repo_mut());Lines 1–500 of 1063. Load more
// Copyright 2020 The Jujutsu Authors // // 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::path::Path; use std::slice; use std::sync::Arc; use std::time::SystemTime; use assert_matches::assert_matches; use futures::TryStreamExt as _; use itertools::Itertools as _; use jj_lib::backend::CommitId; use jj_lib::config::ConfigLayer; use jj_lib::config::ConfigSource; use jj_lib::evolution::walk_predecessors; use jj_lib::index::Index; use jj_lib::object_id::ObjectId as _; use jj_lib::op_store::OperationId; use jj_lib::op_walk; use jj_lib::op_walk::OpsetEvaluationError; use jj_lib::op_walk::OpsetResolutionError; use jj_lib::operation::Operation; use jj_lib::repo::ReadonlyRepo; use jj_lib::repo::Repo; use jj_lib::settings::UserSettings; use pollster::FutureExt as _; use testutils::CommitBuilderExt as _; use testutils::TestRepo; use testutils::TestResult; use testutils::write_random_commit; use testutils::write_random_commit_with_parents; fn get_predecessors(repo: &ReadonlyRepo, id: &CommitId) -> Vec<CommitId> { let entries: Vec<_> = walk_predecessors(repo, slice::from_ref(id)) .try_collect() .block_on() .expect("unreachable predecessors shouldn't be visited"); let first = entries .first() .expect("specified commit should be reachable"); first.predecessor_ids().to_vec() } fn list_dir(dir: &Path) -> Vec<String> { std::fs::read_dir(dir) .unwrap() .map(|entry| entry.unwrap().file_name().to_str().unwrap().to_owned()) .sorted() .collect() } fn index_has_id(index: &dyn Index, commit_id: &CommitId) -> bool { index.has_id(commit_id).unwrap() } #[test] fn test_unpublished_operation() -> TestResult { // Test that the operation doesn't get published until that's requested. let test_repo = TestRepo::init(); let repo = &test_repo.repo; let op_heads_dir = test_repo.repo_path().join("op_heads").join("heads"); let op_id0 = repo.op_id().clone(); assert_eq!(list_dir(&op_heads_dir), vec![repo.op_id().hex()]); let mut tx1 = repo.start_transaction(); write_random_commit(tx1.repo_mut()); let unpublished_op = tx1.write("transaction 1").block_on()?; let op_id1 = unpublished_op.operation().id().clone(); assert_ne!(op_id1, op_id0); assert_eq!(list_dir(&op_heads_dir), vec![op_id0.hex()]); unpublished_op.publish().block_on()?; assert_eq!(list_dir(&op_heads_dir), vec![op_id1.hex()]); Ok(()) } #[test] fn test_consecutive_operations() -> TestResult { // Test that consecutive operations result in a single op-head on disk after // each operation let test_repo = TestRepo::init(); let repo = &test_repo.repo; let op_heads_dir = test_repo.repo_path().join("op_heads").join("heads"); let op_id0 = repo.op_id().clone(); assert_eq!(list_dir(&op_heads_dir), vec![repo.op_id().hex()]); let mut tx1 = repo.start_transaction(); write_random_commit(tx1.repo_mut()); let op_id1 = tx1 .commit("transaction 1") .block_on()? .operation() .id() .clone(); assert_ne!(op_id1, op_id0); assert_eq!(list_dir(&op_heads_dir), vec![op_id1.hex()]); let repo = repo.reload_at_head().block_on()?; let mut tx2 = repo.start_transaction(); write_random_commit(tx2.repo_mut()); let op_id2 = tx2 .commit("transaction 2") .block_on()? .operation() .id() .clone(); assert_ne!(op_id2, op_id0); assert_ne!(op_id2, op_id1); assert_eq!(list_dir(&op_heads_dir), vec![op_id2.hex()]); // Reloading the repo makes no difference (there are no conflicting operations // to resolve). let _repo = repo.reload_at_head().block_on()?; assert_eq!(list_dir(&op_heads_dir), vec![op_id2.hex()]); Ok(()) } #[test] fn test_concurrent_operations() -> TestResult { // Test that consecutive operations result in multiple op-heads on disk until // the repo has been reloaded (which currently happens right away). let test_repo = TestRepo::init(); let repo = &test_repo.repo; let op_heads_dir = test_repo.repo_path().join("op_heads").join("heads"); let op_id0 = repo.op_id().clone(); assert_eq!(list_dir(&op_heads_dir), vec![repo.op_id().hex()]); let mut tx1 = repo.start_transaction(); write_random_commit(tx1.repo_mut()); let op_id1 = tx1 .commit("transaction 1") .block_on()? .operation() .id() .clone(); assert_ne!(op_id1, op_id0); assert_eq!(list_dir(&op_heads_dir), vec![op_id1.hex()]); // After both transactions have committed, we should have two op-heads on disk, // since they were run in parallel. let mut tx2 = repo.start_transaction(); write_random_commit(tx2.repo_mut()); let op_id2 = tx2 .commit("transaction 2") .block_on()? .operation() .id() .clone(); assert_ne!(op_id2, op_id0); assert_ne!(op_id2, op_id1); let mut actual_heads_on_disk = list_dir(&op_heads_dir); actual_heads_on_disk.sort(); let mut expected_heads_on_disk = vec![op_id1.hex(), op_id2.hex()]; expected_heads_on_disk.sort(); assert_eq!(actual_heads_on_disk, expected_heads_on_disk); // Reloading the repo causes the operations to be merged let repo = repo.reload_at_head().block_on()?; let merged_op_id = repo.op_id().clone(); assert_ne!(merged_op_id, op_id0); assert_ne!(merged_op_id, op_id1); assert_ne!(merged_op_id, op_id2); assert_eq!(list_dir(&op_heads_dir), vec![merged_op_id.hex()]); Ok(()) } fn assert_heads(repo: &dyn Repo, expected: Vec<&CommitId>) { let expected = expected.iter().copied().cloned().collect(); assert_eq!(*repo.view().heads(), expected); } #[test] fn test_isolation() -> TestResult { // Test that two concurrent transactions don't see each other's changes. let test_repo = TestRepo::init(); let repo = &test_repo.repo; let mut tx = repo.start_transaction(); let initial = write_random_commit_with_parents(tx.repo_mut(), &[]); let repo = tx.commit("test").block_on()?; let mut tx1 = repo.start_transaction(); let mut_repo1 = tx1.repo_mut(); let mut tx2 = repo.start_transaction(); let mut_repo2 = tx2.repo_mut(); assert_heads(repo.as_ref(), vec![initial.id()]); assert_heads(mut_repo1, vec![initial.id()]); assert_heads(mut_repo2, vec![initial.id()]); let rewrite1 = mut_repo1 .rewrite_commit(&initial) .set_description("rewrite1") .write_unwrap(); mut_repo1.rebase_descendants().block_on()?; let rewrite2 = mut_repo2 .rewrite_commit(&initial) .set_description("rewrite2") .write_unwrap(); mut_repo2.rebase_descendants().block_on()?; // Neither transaction has committed yet, so each transaction sees its own // commit. assert_heads(repo.as_ref(), vec![initial.id()]); assert_heads(mut_repo1, vec![rewrite1.id()]); assert_heads(mut_repo2, vec![rewrite2.id()]); // The base repo and tx2 don't see the commits from tx1. tx1.commit("transaction 1").block_on()?; assert_heads(repo.as_ref(), vec![initial.id()]); assert_heads(mut_repo2, vec![rewrite2.id()]); // The base repo still doesn't see the commits after both transactions commit. tx2.commit("transaction 2").block_on()?; assert_heads(repo.as_ref(), vec![initial.id()]); // After reload, the base repo sees both rewrites. let repo = repo.reload_at_head().block_on()?; assert_heads(repo.as_ref(), vec![rewrite1.id(), rewrite2.id()]); Ok(()) } #[test] fn test_stored_commit_predecessors() -> TestResult { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let loader = repo.loader(); let mut tx = repo.start_transaction(); let commit1 = write_random_commit(tx.repo_mut()); let commit2 = tx .repo_mut() .rewrite_commit(&commit1) .set_description("rewritten") .write_unwrap(); tx.repo_mut().rebase_descendants().block_on()?; let repo = tx.commit("test").block_on()?; // Reload operation from disk. let op = loader.load_operation(repo.op_id()).block_on()?; assert!(op.stores_commit_predecessors()); assert_matches!(op.predecessors_for_commit(commit1.id()), Some([])); assert_matches!(op.predecessors_for_commit(commit2.id()), Some([id]) if id == commit1.id()); // Save operation without the predecessors as old jj would do. let mut data = op.store_operation().clone(); data.commit_predecessors = None; let op_id = loader.op_store().write_operation(&data).block_on()?; assert_ne!(&op_id, op.id()); let op = loader.load_operation(&op_id).block_on()?; assert!(!op.stores_commit_predecessors()); Ok(()) } #[test] fn test_reparent_range_linear() -> TestResult { let test_repo = TestRepo::init(); let repo_0 = test_repo.repo; let loader = repo_0.loader(); let op_store = repo_0.op_store(); let read_op = |id| loader.load_operation(id).block_on().unwrap(); fn op_parents<const N: usize>(op: &Operation) -> [Operation; N] { let parents = op.parents().block_on().unwrap(); parents.try_into().unwrap() } // Set up linear operation graph: // D // C // B // A // 0 (initial) let random_tx = |repo: &Arc<ReadonlyRepo>| { let mut tx = repo.start_transaction(); write_random_commit(tx.repo_mut()); tx }; let repo_a = random_tx(&repo_0).commit("op A").block_on()?; let repo_b = random_tx(&repo_a).commit("op B").block_on()?; let repo_c = random_tx(&repo_b).commit("op C").block_on()?; let repo_d = random_tx(&repo_c).commit("op D").block_on()?; // Reparent B..D (=C|D) onto A: // D' // C' // A // 0 (initial) let stats = op_walk::reparent_range( op_store.as_ref(), slice::from_ref(repo_b.operation()), slice::from_ref(repo_d.operation()), repo_a.operation(), ) .block_on()?; assert_eq!(stats.new_head_ids.len(), 1); assert_eq!(stats.rewritten_count, 2); assert_eq!(stats.unreachable_count, 1); let new_op_d = read_op(&stats.new_head_ids[0]); assert_eq!(new_op_d.metadata(), repo_d.operation().metadata()); assert_eq!(new_op_d.view_id(), repo_d.operation().view_id()); let [new_op_c] = op_parents(&new_op_d); assert_eq!(new_op_c.metadata(), repo_c.operation().metadata()); assert_eq!(new_op_c.view_id(), repo_c.operation().view_id()); assert_eq!(new_op_c.parent_ids(), slice::from_ref(repo_a.op_id())); // Reparent empty range onto A let stats = op_walk::reparent_range( op_store.as_ref(), slice::from_ref(repo_d.operation()), slice::from_ref(repo_d.operation()), repo_a.operation(), ) .block_on()?; assert_eq!(stats.new_head_ids, vec![repo_a.op_id().clone()]); assert_eq!(stats.rewritten_count, 0); assert_eq!(stats.unreachable_count, 3); Ok(()) } #[test] fn test_reparent_range_branchy() -> TestResult { let test_repo = TestRepo::init(); let repo_0 = test_repo.repo; let loader = repo_0.loader(); let op_store = repo_0.op_store(); let read_op = |id| loader.load_operation(id).block_on().unwrap(); fn op_parents<const N: usize>(op: &Operation) -> [Operation; N] { let parents = op.parents().block_on().unwrap(); parents.try_into().unwrap() } // Set up branchy operation graph: // G // |\ // | F // E | // D | // |/ // C // B // A // 0 (initial) let random_tx = |repo: &Arc<ReadonlyRepo>| { let mut tx = repo.start_transaction(); write_random_commit(tx.repo_mut()); tx }; let repo_a = random_tx(&repo_0).commit("op A").block_on()?; let repo_b = random_tx(&repo_a).commit("op B").block_on()?; let repo_c = random_tx(&repo_b).commit("op C").block_on()?; let repo_d = random_tx(&repo_c).commit("op D").block_on()?; let tx_e = random_tx(&repo_d); let tx_f = random_tx(&repo_c); let repo_g = testutils::commit_transactions(vec![tx_e, tx_f]); let [op_e, op_f] = op_parents(repo_g.operation()); // Reparent D..G (= E|F|G) onto B: // G' // |\ // | F' // E'| // |/ // B // A // 0 (initial) let stats = op_walk::reparent_range( op_store.as_ref(), slice::from_ref(repo_d.operation()), slice::from_ref(repo_g.operation()), repo_b.operation(), ) .block_on()?; assert_eq!(stats.new_head_ids.len(), 1); assert_eq!(stats.rewritten_count, 3); assert_eq!(stats.unreachable_count, 2); let new_op_g = read_op(&stats.new_head_ids[0]); assert_eq!(new_op_g.metadata(), repo_g.operation().metadata()); assert_eq!(new_op_g.view_id(), repo_g.operation().view_id()); let [new_op_e, new_op_f] = op_parents(&new_op_g); assert_eq!(new_op_e.parent_ids(), slice::from_ref(repo_b.op_id())); assert_eq!(new_op_f.parent_ids(), slice::from_ref(repo_b.op_id())); // Reparent B..G (=C|D|E|F|G) onto A: // G' // |\ // | F' // E'| // D'| // |/ // C' // A // 0 (initial) let stats = op_walk::reparent_range( op_store.as_ref(), slice::from_ref(repo_b.operation()), slice::from_ref(repo_g.operation()), repo_a.operation(), ) .block_on()?; assert_eq!(stats.new_head_ids.len(), 1); assert_eq!(stats.rewritten_count, 5); assert_eq!(stats.unreachable_count, 1); let new_op_g = read_op(&stats.new_head_ids[0]); assert_eq!(new_op_g.metadata(), repo_g.operation().metadata()); assert_eq!(new_op_g.view_id(), repo_g.operation().view_id()); let [new_op_e, new_op_f] = op_parents(&new_op_g); let [new_op_d] = op_parents(&new_op_e); assert_eq!(new_op_d.parent_ids(), new_op_f.parent_ids()); let [new_op_c] = op_parents(&new_op_d); assert_eq!(new_op_c.parent_ids(), slice::from_ref(repo_a.op_id())); // Reparent (E|F)..G (=G) onto D: // G' // D // C // B // A // 0 (initial) let stats = op_walk::reparent_range( op_store.as_ref(), &[op_e.clone(), op_f.clone()], slice::from_ref(repo_g.operation()), repo_d.operation(), ) .block_on()?; assert_eq!(stats.new_head_ids.len(), 1); assert_eq!(stats.rewritten_count, 1); assert_eq!(stats.unreachable_count, 2); let new_op_g = read_op(&stats.new_head_ids[0]); assert_eq!(new_op_g.metadata(), repo_g.operation().metadata()); assert_eq!(new_op_g.view_id(), repo_g.operation().view_id()); assert_eq!(new_op_g.parent_ids(), slice::from_ref(repo_d.op_id())); // Reparent C..F (=F) onto D (ignoring G): // F' // D // C // B // A // 0 (initial) let stats = op_walk::reparent_range( op_store.as_ref(), slice::from_ref(repo_c.operation()), slice::from_ref(&op_f), repo_d.operation(), ) .block_on()?; assert_eq!(stats.new_head_ids.len(), 1); assert_eq!(stats.rewritten_count, 1); assert_eq!(stats.unreachable_count, 0); let new_op_f = read_op(&stats.new_head_ids[0]); assert_eq!(new_op_f.metadata(), op_f.metadata()); assert_eq!(new_op_f.view_id(), op_f.view_id()); assert_eq!(new_op_f.parent_ids(), slice::from_ref(repo_d.op_id())); Ok(()) } #[test] fn test_reparent_discarding_predecessors() -> TestResult { let test_repo = TestRepo::init(); let repo_0 = test_repo.repo; let loader = repo_0.loader(); let op_store = repo_0.op_store(); let repo_at = |id: &OperationId| { let op = loader.load_operation(id).block_on().unwrap(); loader.load_at(&op).block_on().unwrap() }; let head_commits = |repo: &dyn Repo| { repo.view() .heads() .iter() .map(|id| repo.store().get_commit(id).unwrap()) .collect_vec() }; // Set up rewriting as follows: // // op1 op2 op3 op4 // B0 B0 B1 B1 // | | | | // A0 A0 A1 A0 A1 A2 let mut tx = repo_0.start_transaction(); let commit_a0 = write_random_commit(tx.repo_mut());