forked from mirrors/jj
f1666375bd
I want to keep the index updated within the transaction. I tried doing that by adding a `trait Index`, implemented by `ReadonlyIndex` and `MutableIndex`. However, `ReadonlyRepo::index` is of type `Mutex<Option<Arc<IndexFile>>>` (because it is lazily initialized), and we cannot get a `&dyn Index` that lives long enough to be returned from a `Repo::index()` from that. It seems the best solution is to instead create an `Index` enum (instead of a trait), with one readonly and one mutable variant. This commit starts the migration to that design by replacing the `Repo` trait by an enum. I never intended for there there to be more implementations of `Repo` than `ReadonlyRepo` and `MutableRepo` anyway.
516 lines
20 KiB
Rust
516 lines
20 KiB
Rust
// 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 jujube_lib::commit_builder::CommitBuilder;
|
|
use jujube_lib::repo_path::FileRepoPath;
|
|
use jujube_lib::store::{Conflict, ConflictId, ConflictPart, TreeValue};
|
|
use jujube_lib::store_wrapper::StoreWrapper;
|
|
use jujube_lib::testutils;
|
|
use jujube_lib::view::View;
|
|
use std::sync::Arc;
|
|
use test_case::test_case;
|
|
|
|
// TODO Many of the tests here are not run with Git because they end up creating
|
|
// two commits with the same contents.
|
|
|
|
#[test_case(false ; "local store")]
|
|
// #[test_case(true ; "git store")]
|
|
fn test_checkout_open(use_git: bool) {
|
|
// Test that Transaction::check_out() uses the requested commit if it's open
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let requested_checkout = testutils::create_random_commit(&settings, &repo)
|
|
.set_open(true)
|
|
.write_to_transaction(&mut tx);
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let actual_checkout = tx.check_out(&settings, &requested_checkout);
|
|
assert_eq!(actual_checkout.id(), requested_checkout.id());
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
assert_eq!(repo.view().checkout(), actual_checkout.id());
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
// #[test_case(true ; "git store")]
|
|
fn test_checkout_closed(use_git: bool) {
|
|
// Test that Transaction::check_out() creates a child if the requested commit is
|
|
// closed
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let requested_checkout = testutils::create_random_commit(&settings, &repo)
|
|
.set_open(false)
|
|
.write_to_transaction(&mut tx);
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let actual_checkout = tx.check_out(&settings, &requested_checkout);
|
|
assert_eq!(actual_checkout.tree().id(), requested_checkout.tree().id());
|
|
assert_eq!(actual_checkout.parents().len(), 1);
|
|
assert_eq!(actual_checkout.parents()[0].id(), requested_checkout.id());
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
assert_eq!(repo.view().checkout(), actual_checkout.id());
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
// #[test_case(true ; "git store")]
|
|
fn test_checkout_open_with_conflict(use_git: bool) {
|
|
// Test that Transaction::check_out() creates a child if the requested
|
|
// commit is open and has conflicts
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
let store = repo.store();
|
|
|
|
let file_path = FileRepoPath::from("file");
|
|
let conflict_id = write_conflict(store, &file_path);
|
|
let mut tree_builder = repo
|
|
.store()
|
|
.tree_builder(repo.store().empty_tree_id().clone());
|
|
tree_builder.set(file_path.to_repo_path(), TreeValue::Conflict(conflict_id));
|
|
let tree_id = tree_builder.write_tree();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let requested_checkout = CommitBuilder::for_new_commit(&settings, store, tree_id)
|
|
.set_open(true)
|
|
.write_to_transaction(&mut tx);
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let actual_checkout = tx.check_out(&settings, &requested_checkout);
|
|
let file_value = actual_checkout.tree().path_value(&file_path.to_repo_path());
|
|
match file_value {
|
|
Some(TreeValue::Normal {
|
|
id: _,
|
|
executable: false,
|
|
}) => {}
|
|
_ => panic!("unexpected tree value: {:?}", file_value),
|
|
}
|
|
assert_eq!(actual_checkout.parents().len(), 1);
|
|
assert_eq!(actual_checkout.parents()[0].id(), requested_checkout.id());
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
assert_eq!(repo.view().checkout(), actual_checkout.id());
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
// #[test_case(true ; "git store")]
|
|
fn test_checkout_closed_with_conflict(use_git: bool) {
|
|
// Test that Transaction::check_out() creates a child if the requested commit is
|
|
// closed and has conflicts
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
let store = repo.store();
|
|
|
|
let file_path = FileRepoPath::from("file");
|
|
let conflict_id = write_conflict(store, &file_path);
|
|
let mut tree_builder = repo
|
|
.store()
|
|
.tree_builder(repo.store().empty_tree_id().clone());
|
|
tree_builder.set(file_path.to_repo_path(), TreeValue::Conflict(conflict_id));
|
|
let tree_id = tree_builder.write_tree();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let requested_checkout = CommitBuilder::for_new_commit(&settings, store, tree_id)
|
|
.set_open(false)
|
|
.write_to_transaction(&mut tx);
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let actual_checkout = tx.check_out(&settings, &requested_checkout);
|
|
let file_value = actual_checkout.tree().path_value(&file_path.to_repo_path());
|
|
match file_value {
|
|
Some(TreeValue::Normal {
|
|
id: _,
|
|
executable: false,
|
|
}) => {}
|
|
_ => panic!("unexpected tree value: {:?}", file_value),
|
|
}
|
|
assert_eq!(actual_checkout.parents().len(), 1);
|
|
assert_eq!(actual_checkout.parents()[0].id(), requested_checkout.id());
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
assert_eq!(repo.view().checkout(), actual_checkout.id());
|
|
}
|
|
|
|
fn write_conflict(store: &Arc<StoreWrapper>, file_path: &FileRepoPath) -> ConflictId {
|
|
let file_id1 = testutils::write_file(store, &file_path, "a\n");
|
|
let file_id2 = testutils::write_file(store, &file_path, "b\n");
|
|
let file_id3 = testutils::write_file(store, &file_path, "c\n");
|
|
let conflict = Conflict {
|
|
removes: vec![ConflictPart {
|
|
value: TreeValue::Normal {
|
|
id: file_id1,
|
|
executable: false,
|
|
},
|
|
}],
|
|
adds: vec![
|
|
ConflictPart {
|
|
value: TreeValue::Normal {
|
|
id: file_id2,
|
|
executable: false,
|
|
},
|
|
},
|
|
ConflictPart {
|
|
value: TreeValue::Normal {
|
|
id: file_id3,
|
|
executable: false,
|
|
},
|
|
},
|
|
],
|
|
};
|
|
store.write_conflict(&conflict).unwrap()
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
// #[test_case(true ; "git store")]
|
|
fn test_checkout_previous_not_empty(use_git: bool) {
|
|
// Test that Transaction::check_out() does not usually prune the previous
|
|
// commit.
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let old_checkout = testutils::create_random_commit(&settings, &repo)
|
|
.set_open(true)
|
|
.write_to_transaction(&mut tx);
|
|
tx.check_out(&settings, &old_checkout);
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let new_checkout = testutils::create_random_commit(&settings, &repo)
|
|
.set_open(true)
|
|
.write_to_transaction(&mut tx);
|
|
tx.check_out(&settings, &new_checkout);
|
|
assert!(!tx.as_repo_ref().evolution().is_obsolete(old_checkout.id()));
|
|
tx.discard();
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
// #[test_case(true ; "git store")]
|
|
fn test_checkout_previous_empty(use_git: bool) {
|
|
// Test that Transaction::check_out() prunes the previous commit if it was
|
|
// empty.
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let old_checkout = CommitBuilder::for_open_commit(
|
|
&settings,
|
|
repo.store(),
|
|
repo.store().root_commit_id().clone(),
|
|
repo.store().empty_tree_id().clone(),
|
|
)
|
|
.write_to_transaction(&mut tx);
|
|
tx.check_out(&settings, &old_checkout);
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let new_checkout = testutils::create_random_commit(&settings, &repo)
|
|
.set_open(true)
|
|
.write_to_transaction(&mut tx);
|
|
tx.check_out(&settings, &new_checkout);
|
|
assert!(tx.as_repo_ref().evolution().is_obsolete(old_checkout.id()));
|
|
tx.discard();
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
// #[test_case(true ; "git store")]
|
|
fn test_checkout_previous_empty_and_obsolete(use_git: bool) {
|
|
// Test that Transaction::check_out() does not unnecessarily prune the previous
|
|
// commit if it was empty but already obsolete.
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let old_checkout = CommitBuilder::for_open_commit(
|
|
&settings,
|
|
repo.store(),
|
|
repo.store().root_commit_id().clone(),
|
|
repo.store().empty_tree_id().clone(),
|
|
)
|
|
.write_to_transaction(&mut tx);
|
|
let successor = CommitBuilder::for_rewrite_from(&settings, repo.store(), &old_checkout)
|
|
.write_to_transaction(&mut tx);
|
|
tx.check_out(&settings, &old_checkout);
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let new_checkout = testutils::create_random_commit(&settings, &repo)
|
|
.set_open(true)
|
|
.write_to_transaction(&mut tx);
|
|
tx.check_out(&settings, &new_checkout);
|
|
let successors = tx.as_repo_ref().evolution().successors(old_checkout.id());
|
|
assert_eq!(successors.len(), 1);
|
|
assert_eq!(successors.iter().next().unwrap(), successor.id());
|
|
tx.discard();
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
// #[test_case(true ; "git store")]
|
|
fn test_checkout_previous_empty_and_pruned(use_git: bool) {
|
|
// Test that Transaction::check_out() does not unnecessarily prune the previous
|
|
// commit if it was empty but already obsolete.
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let old_checkout = testutils::create_random_commit(&settings, &repo)
|
|
.set_open(true)
|
|
.set_pruned(true)
|
|
.write_to_transaction(&mut tx);
|
|
tx.check_out(&settings, &old_checkout);
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let new_checkout = testutils::create_random_commit(&settings, &repo)
|
|
.set_open(true)
|
|
.write_to_transaction(&mut tx);
|
|
tx.check_out(&settings, &new_checkout);
|
|
assert!(tx
|
|
.as_repo_ref()
|
|
.evolution()
|
|
.successors(old_checkout.id())
|
|
.is_empty());
|
|
tx.discard();
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
// #[test_case(true ; "git store")]
|
|
fn test_add_head_success(use_git: bool) {
|
|
// Test that Transaction::add_head() adds the head, and that it's still there
|
|
// after commit.
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
// Create a commit outside of the repo by using a temporary transaction. Then
|
|
// add that as a head.
|
|
let mut tx = repo.start_transaction("test");
|
|
let new_commit =
|
|
testutils::create_random_commit(&settings, &repo).write_to_transaction(&mut tx);
|
|
tx.discard();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
assert!(!tx.as_repo_ref().view().heads().contains(new_commit.id()));
|
|
tx.add_head(&new_commit);
|
|
assert!(tx.as_repo_ref().view().heads().contains(new_commit.id()));
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
assert!(repo.view().heads().contains(new_commit.id()));
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
// #[test_case(true ; "git store")]
|
|
fn test_add_head_ancestor(use_git: bool) {
|
|
// Test that Transaction::add_head() does not add a head if it's an ancestor of
|
|
// an existing head.
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let commit1 = testutils::create_random_commit(&settings, &repo).write_to_transaction(&mut tx);
|
|
let commit2 = testutils::create_random_commit(&settings, &repo)
|
|
.set_parents(vec![commit1.id().clone()])
|
|
.write_to_transaction(&mut tx);
|
|
let _commit3 = testutils::create_random_commit(&settings, &repo)
|
|
.set_parents(vec![commit2.id().clone()])
|
|
.write_to_transaction(&mut tx);
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
tx.add_head(&commit1);
|
|
assert!(!tx.as_repo_ref().view().heads().contains(commit1.id()));
|
|
tx.discard();
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
// #[test_case(true ; "git store")]
|
|
fn test_remove_head(use_git: bool) {
|
|
// Test that Transaction::remove_head() removes the head, and that it's still
|
|
// removed after commit.
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let commit1 = testutils::create_random_commit(&settings, &repo).write_to_transaction(&mut tx);
|
|
let commit2 = testutils::create_random_commit(&settings, &repo)
|
|
.set_parents(vec![commit1.id().clone()])
|
|
.write_to_transaction(&mut tx);
|
|
let commit3 = testutils::create_random_commit(&settings, &repo)
|
|
.set_parents(vec![commit2.id().clone()])
|
|
.write_to_transaction(&mut tx);
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
assert!(tx.as_repo_ref().view().heads().contains(commit3.id()));
|
|
tx.remove_head(&commit3);
|
|
let heads = tx.as_repo_ref().view().heads().clone();
|
|
assert!(!heads.contains(commit3.id()));
|
|
assert!(!heads.contains(commit2.id()));
|
|
assert!(!heads.contains(commit1.id()));
|
|
tx.commit();
|
|
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
let heads = repo.view().heads().clone();
|
|
assert!(!heads.contains(commit3.id()));
|
|
assert!(!heads.contains(commit2.id()));
|
|
assert!(!heads.contains(commit1.id()));
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
// #[test_case(true ; "git store")]
|
|
fn test_remove_head_ancestor_git_ref(use_git: bool) {
|
|
// Test that Transaction::remove_head() does not leave the view with a git ref
|
|
// pointing to a commit that's not reachable by any head.
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let commit1 = testutils::create_random_commit(&settings, &repo).write_to_transaction(&mut tx);
|
|
let commit2 = testutils::create_random_commit(&settings, &repo)
|
|
.set_parents(vec![commit1.id().clone()])
|
|
.write_to_transaction(&mut tx);
|
|
let commit3 = testutils::create_random_commit(&settings, &repo)
|
|
.set_parents(vec![commit2.id().clone()])
|
|
.write_to_transaction(&mut tx);
|
|
tx.insert_git_ref("refs/heads/main".to_string(), commit1.id().clone());
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let heads = tx.as_repo_ref().view().heads().clone();
|
|
assert!(heads.contains(commit3.id()));
|
|
tx.remove_head(&commit3);
|
|
let heads = tx.as_repo_ref().view().heads().clone();
|
|
assert!(!heads.contains(commit3.id()));
|
|
assert!(!heads.contains(commit2.id()));
|
|
assert!(heads.contains(commit1.id()));
|
|
tx.commit();
|
|
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
let heads = repo.view().heads().clone();
|
|
assert!(!heads.contains(commit3.id()));
|
|
assert!(!heads.contains(commit2.id()));
|
|
assert!(heads.contains(commit1.id()));
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
// #[test_case(true ; "git store")]
|
|
fn test_add_public_head(use_git: bool) {
|
|
// Test that Transaction::add_public_head() adds the head, and that it's still
|
|
// there after commit.
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let commit1 = testutils::create_random_commit(&settings, &repo).write_to_transaction(&mut tx);
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
assert!(!tx
|
|
.as_repo_ref()
|
|
.view()
|
|
.public_heads()
|
|
.contains(commit1.id()));
|
|
tx.add_public_head(&commit1);
|
|
assert!(tx
|
|
.as_repo_ref()
|
|
.view()
|
|
.public_heads()
|
|
.contains(commit1.id()));
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
assert!(repo.view().public_heads().contains(commit1.id()));
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
// #[test_case(true ; "git store")]
|
|
fn test_add_public_head_ancestor(use_git: bool) {
|
|
// Test that Transaction::add_public_head() does not add a public head if it's
|
|
// an ancestor of an existing public head.
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let commit1 = testutils::create_random_commit(&settings, &repo).write_to_transaction(&mut tx);
|
|
let commit2 = testutils::create_random_commit(&settings, &repo)
|
|
.set_parents(vec![commit1.id().clone()])
|
|
.write_to_transaction(&mut tx);
|
|
tx.add_public_head(&commit2);
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
assert!(!tx
|
|
.as_repo_ref()
|
|
.view()
|
|
.public_heads()
|
|
.contains(commit1.id()));
|
|
tx.add_public_head(&commit1);
|
|
assert!(!tx
|
|
.as_repo_ref()
|
|
.view()
|
|
.public_heads()
|
|
.contains(commit1.id()));
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
assert!(!repo.view().public_heads().contains(commit1.id()));
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
// #[test_case(true ; "git store")]
|
|
fn test_remove_public_head(use_git: bool) {
|
|
// Test that Transaction::remove_public_head() removes the head, and that it's
|
|
// still removed after commit.
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let commit1 = testutils::create_random_commit(&settings, &repo).write_to_transaction(&mut tx);
|
|
tx.add_public_head(&commit1);
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
assert!(tx
|
|
.as_repo_ref()
|
|
.view()
|
|
.public_heads()
|
|
.contains(commit1.id()));
|
|
tx.remove_public_head(&commit1);
|
|
assert!(!tx
|
|
.as_repo_ref()
|
|
.view()
|
|
.public_heads()
|
|
.contains(commit1.id()));
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
assert!(!repo.view().public_heads().contains(commit1.id()));
|
|
}
|