forked from mirrors/jj
1f27a78957
I think it's better to let the caller decide if the parents should be added. One use case for removing a head is when fetching from a Git remote where a branch has been rewritten. In that case, it's probably the best user experience to remove the old head. With the current semantics of `View::remove_head()`, we would need to walk up the graph to find a commit that's an ancestor and for each commit we remove as head, its parents get temporarily added as heads. It's much easier for callers that want to add the parents as heads to do that.
389 lines
14 KiB
Rust
389 lines
14 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::Commit;
|
|
use jujube_lib::commit_builder::CommitBuilder;
|
|
use jujube_lib::index::CompositeIndex;
|
|
use jujube_lib::repo::ReadonlyRepo;
|
|
use jujube_lib::settings::UserSettings;
|
|
use jujube_lib::store::CommitId;
|
|
use jujube_lib::testutils;
|
|
use std::sync::Arc;
|
|
use test_case::test_case;
|
|
|
|
#[must_use]
|
|
fn child_commit(settings: &UserSettings, repo: &ReadonlyRepo, commit: &Commit) -> CommitBuilder {
|
|
testutils::create_random_commit(&settings, repo).set_parents(vec![commit.id().clone()])
|
|
}
|
|
|
|
// Helper just to reduce line wrapping
|
|
fn generation_number(index: &CompositeIndex, commit_id: &CommitId) -> u32 {
|
|
index.entry_by_id(commit_id).unwrap().generation_number()
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
#[test_case(true ; "git store")]
|
|
fn test_index_commits_empty_repo(use_git: bool) {
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
let index = repo.index();
|
|
let index = index.as_composite();
|
|
// There should be the root commit and the working copy commit
|
|
assert_eq!(index.num_commits(), 2);
|
|
|
|
// Check the generation numbers of the root and the working copy
|
|
assert_eq!(generation_number(&index, repo.store().root_commit_id()), 0);
|
|
assert_eq!(
|
|
generation_number(&index, &repo.working_copy_locked().current_commit_id()),
|
|
1
|
|
);
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
#[test_case(true ; "git store")]
|
|
fn test_index_commits_standard_cases(use_git: bool) {
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
// o H
|
|
// o | G
|
|
// o | F
|
|
// |\|
|
|
// | o E
|
|
// | o D
|
|
// | o C
|
|
// o | B
|
|
// |/
|
|
// o A
|
|
// | o working copy
|
|
// |/
|
|
// o root
|
|
|
|
let root_commit = repo.store().root_commit();
|
|
let wc_commit = repo.working_copy_locked().current_commit();
|
|
let mut tx = repo.start_transaction("test");
|
|
let commit_a = child_commit(&settings, &repo, &root_commit).write_to_transaction(&mut tx);
|
|
let commit_b = child_commit(&settings, &repo, &commit_a).write_to_transaction(&mut tx);
|
|
let commit_c = child_commit(&settings, &repo, &commit_a).write_to_transaction(&mut tx);
|
|
let commit_d = child_commit(&settings, &repo, &commit_c).write_to_transaction(&mut tx);
|
|
let commit_e = child_commit(&settings, &repo, &commit_d).write_to_transaction(&mut tx);
|
|
let commit_f = testutils::create_random_commit(&settings, &repo)
|
|
.set_parents(vec![commit_b.id().clone(), commit_e.id().clone()])
|
|
.write_to_transaction(&mut tx);
|
|
let commit_g = child_commit(&settings, &repo, &commit_f).write_to_transaction(&mut tx);
|
|
let commit_h = child_commit(&settings, &repo, &commit_e).write_to_transaction(&mut tx);
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let index = repo.index();
|
|
let index = index.as_composite();
|
|
// There should be the root commit and the working copy commit, plus
|
|
// 8 more
|
|
assert_eq!(index.num_commits(), 2 + 8);
|
|
|
|
let stats = index.stats();
|
|
assert_eq!(stats.num_commits, 2 + 8);
|
|
assert_eq!(stats.num_merges, 1);
|
|
assert_eq!(stats.max_generation_number, 6);
|
|
|
|
assert_eq!(generation_number(&index, root_commit.id()), 0);
|
|
assert_eq!(generation_number(&index, wc_commit.id()), 1);
|
|
assert_eq!(generation_number(&index, commit_a.id()), 1);
|
|
assert_eq!(generation_number(&index, commit_b.id()), 2);
|
|
assert_eq!(generation_number(&index, commit_c.id()), 2);
|
|
assert_eq!(generation_number(&index, commit_d.id()), 3);
|
|
assert_eq!(generation_number(&index, commit_e.id()), 4);
|
|
assert_eq!(generation_number(&index, commit_f.id()), 5);
|
|
assert_eq!(generation_number(&index, commit_g.id()), 6);
|
|
assert_eq!(generation_number(&index, commit_h.id()), 5);
|
|
|
|
assert!(index.is_ancestor(root_commit.id(), commit_a.id()));
|
|
assert!(!index.is_ancestor(commit_a.id(), root_commit.id()));
|
|
|
|
assert!(index.is_ancestor(root_commit.id(), commit_b.id()));
|
|
assert!(!index.is_ancestor(commit_b.id(), root_commit.id()));
|
|
|
|
assert!(!index.is_ancestor(commit_b.id(), commit_c.id()));
|
|
|
|
assert!(index.is_ancestor(commit_a.id(), commit_b.id()));
|
|
assert!(index.is_ancestor(commit_a.id(), commit_e.id()));
|
|
assert!(index.is_ancestor(commit_a.id(), commit_f.id()));
|
|
assert!(index.is_ancestor(commit_a.id(), commit_g.id()));
|
|
assert!(index.is_ancestor(commit_a.id(), commit_h.id()));
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
#[test_case(true ; "git store")]
|
|
fn test_index_commits_criss_cross(use_git: bool) {
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
let num_generations = 50;
|
|
let root_commit = repo.store().root_commit();
|
|
|
|
// Create a long chain of criss-crossed merges. If they were traversed without
|
|
// keeping track of visited nodes, it would be 2^50 visits, so if this test
|
|
// finishes in reasonable time, we know that we don't do a naive traversal.
|
|
let mut tx = repo.start_transaction("test");
|
|
let mut left_commits =
|
|
vec![child_commit(&settings, &repo, &root_commit).write_to_transaction(&mut tx)];
|
|
let mut right_commits =
|
|
vec![child_commit(&settings, &repo, &root_commit).write_to_transaction(&mut tx)];
|
|
for gen in 1..num_generations {
|
|
let new_left = testutils::create_random_commit(&settings, &repo)
|
|
.set_parents(vec![
|
|
left_commits[gen - 1].id().clone(),
|
|
right_commits[gen - 1].id().clone(),
|
|
])
|
|
.write_to_transaction(&mut tx);
|
|
let new_right = testutils::create_random_commit(&settings, &repo)
|
|
.set_parents(vec![
|
|
left_commits[gen - 1].id().clone(),
|
|
right_commits[gen - 1].id().clone(),
|
|
])
|
|
.write_to_transaction(&mut tx);
|
|
left_commits.push(new_left);
|
|
right_commits.push(new_right);
|
|
}
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let index = repo.index();
|
|
let index = index.as_composite();
|
|
// There should the root commit and the working copy commit, plus 2 for each
|
|
// generation
|
|
assert_eq!(index.num_commits(), 2 + 2 * (num_generations as u32));
|
|
|
|
let stats = index.stats();
|
|
assert_eq!(stats.num_commits, 2 + 2 * (num_generations as u32));
|
|
// The first generations are not merges
|
|
assert_eq!(stats.num_merges, 2 * (num_generations as u32 - 1));
|
|
assert_eq!(stats.max_generation_number, num_generations as u32);
|
|
|
|
// Check generation numbers
|
|
for gen in 0..num_generations {
|
|
assert_eq!(
|
|
generation_number(&index, left_commits[gen].id()),
|
|
(gen as u32) + 1
|
|
);
|
|
assert_eq!(
|
|
generation_number(&index, right_commits[gen].id()),
|
|
(gen as u32) + 1
|
|
);
|
|
}
|
|
|
|
// The left and right commits of the same generation should not be ancestors of
|
|
// each other
|
|
for gen in 0..num_generations {
|
|
assert!(!index.is_ancestor(left_commits[gen].id(), right_commits[gen].id()));
|
|
assert!(!index.is_ancestor(right_commits[gen].id(), left_commits[gen].id()));
|
|
}
|
|
|
|
// Both sides of earlier generations should be ancestors. Check a few different
|
|
// earlier generations.
|
|
for gen in 1..num_generations {
|
|
for ancestor_side in &[&left_commits, &right_commits] {
|
|
for descendant_side in &[&left_commits, &right_commits] {
|
|
assert!(index.is_ancestor(ancestor_side[0].id(), descendant_side[gen].id()));
|
|
assert!(index.is_ancestor(ancestor_side[gen - 1].id(), descendant_side[gen].id()));
|
|
assert!(index.is_ancestor(ancestor_side[gen / 2].id(), descendant_side[gen].id()));
|
|
}
|
|
}
|
|
}
|
|
|
|
assert_eq!(
|
|
index
|
|
.walk_revs(&[left_commits[num_generations - 1].id().clone()], &[])
|
|
.count(),
|
|
2 * num_generations
|
|
);
|
|
assert_eq!(
|
|
index
|
|
.walk_revs(&[right_commits[num_generations - 1].id().clone()], &[])
|
|
.count(),
|
|
2 * num_generations
|
|
);
|
|
assert_eq!(
|
|
index
|
|
.walk_revs(
|
|
&[left_commits[num_generations - 1].id().clone()],
|
|
&[left_commits[num_generations - 2].id().clone()]
|
|
)
|
|
.count(),
|
|
2
|
|
);
|
|
assert_eq!(
|
|
index
|
|
.walk_revs(
|
|
&[right_commits[num_generations - 1].id().clone()],
|
|
&[right_commits[num_generations - 2].id().clone()]
|
|
)
|
|
.count(),
|
|
2
|
|
);
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
#[test_case(true ; "git store")]
|
|
fn test_index_commits_previous_operations(use_git: bool) {
|
|
// Test that commits visible only in previous operations are indexed.
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
// Remove commit B and C in one operation and make sure they're still
|
|
// visible in the index after that operation.
|
|
// o C
|
|
// o B
|
|
// o A
|
|
// | o working copy
|
|
// |/
|
|
// o root
|
|
|
|
let root_commit = repo.store().root_commit();
|
|
let mut tx = repo.start_transaction("test");
|
|
let commit_a = child_commit(&settings, &repo, &root_commit).write_to_transaction(&mut tx);
|
|
let commit_b = child_commit(&settings, &repo, &commit_a).write_to_transaction(&mut tx);
|
|
let commit_c = child_commit(&settings, &repo, &commit_b).write_to_transaction(&mut tx);
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
tx.remove_head(&commit_c);
|
|
tx.commit();
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
// Delete index from disk
|
|
let index_operations_dir = repo
|
|
.working_copy_path()
|
|
.join(".jj")
|
|
.join("index")
|
|
.join("operations");
|
|
assert!(index_operations_dir.is_dir());
|
|
std::fs::remove_dir_all(&index_operations_dir).unwrap();
|
|
std::fs::create_dir(&index_operations_dir).unwrap();
|
|
|
|
let repo = ReadonlyRepo::load(&settings, repo.working_copy_path().clone()).unwrap();
|
|
let index = repo.index();
|
|
let index = index.as_composite();
|
|
// There should be the root commit and the working copy commit, plus
|
|
// 3 more
|
|
assert_eq!(index.num_commits(), 2 + 3);
|
|
|
|
let stats = index.stats();
|
|
assert_eq!(stats.num_commits, 2 + 3);
|
|
assert_eq!(stats.num_merges, 0);
|
|
assert_eq!(stats.max_generation_number, 3);
|
|
|
|
assert_eq!(generation_number(&index, commit_a.id()), 1);
|
|
assert_eq!(generation_number(&index, commit_b.id()), 2);
|
|
assert_eq!(generation_number(&index, commit_c.id()), 3);
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
#[test_case(true ; "git store")]
|
|
fn test_index_commits_incremental(use_git: bool) {
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
// Create A in one operation, then B and C in another. Check that the index is
|
|
// valid after.
|
|
// o C
|
|
// o B
|
|
// o A
|
|
// | o working copy
|
|
// |/
|
|
// o root
|
|
|
|
let root_commit = repo.store().root_commit();
|
|
let commit_a =
|
|
child_commit(&settings, &repo, &root_commit).write_to_new_transaction(&repo, "test");
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let index = repo.index();
|
|
let index = index.as_composite();
|
|
// There should be the root commit and the working copy commit, plus
|
|
// 1 more
|
|
assert_eq!(index.num_commits(), 2 + 1);
|
|
|
|
let mut tx = repo.start_transaction("test");
|
|
let commit_b = child_commit(&settings, &repo, &commit_a).write_to_transaction(&mut tx);
|
|
let commit_c = child_commit(&settings, &repo, &commit_b).write_to_transaction(&mut tx);
|
|
tx.commit();
|
|
|
|
let repo = ReadonlyRepo::load(&settings, repo.working_copy_path().clone()).unwrap();
|
|
let index = repo.index();
|
|
let index = index.as_composite();
|
|
// There should be the root commit and the working copy commit, plus
|
|
// 3 more
|
|
assert_eq!(index.num_commits(), 2 + 3);
|
|
|
|
let stats = index.stats();
|
|
assert_eq!(stats.num_commits, 2 + 3);
|
|
assert_eq!(stats.num_merges, 0);
|
|
assert_eq!(stats.max_generation_number, 3);
|
|
assert_eq!(stats.levels.len(), 2);
|
|
assert_eq!(stats.levels[0].num_commits, 2);
|
|
assert_eq!(stats.levels[1].num_commits, 3);
|
|
|
|
assert_eq!(generation_number(&index, root_commit.id()), 0);
|
|
assert_eq!(generation_number(&index, commit_a.id()), 1);
|
|
assert_eq!(generation_number(&index, commit_b.id()), 2);
|
|
assert_eq!(generation_number(&index, commit_c.id()), 3);
|
|
}
|
|
|
|
#[test_case(false ; "local store")]
|
|
#[test_case(true ; "git store")]
|
|
fn test_index_commits_incremental_empty_transaction(use_git: bool) {
|
|
let settings = testutils::user_settings();
|
|
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
|
|
|
|
// Create A in one operation, then just an empty transaction. Check that the
|
|
// index is valid after.
|
|
// o A
|
|
// | o working copy
|
|
// |/
|
|
// o root
|
|
|
|
let root_commit = repo.store().root_commit();
|
|
let commit_a =
|
|
child_commit(&settings, &repo, &root_commit).write_to_new_transaction(&repo, "test");
|
|
Arc::get_mut(&mut repo).unwrap().reload();
|
|
|
|
let index = repo.index();
|
|
let index = index.as_composite();
|
|
// There should be the root commit and the working copy commit, plus
|
|
// 1 more
|
|
assert_eq!(index.num_commits(), 2 + 1);
|
|
|
|
repo.start_transaction("test").commit();
|
|
|
|
let repo = ReadonlyRepo::load(&settings, repo.working_copy_path().clone()).unwrap();
|
|
let index = repo.index();
|
|
let index = index.as_composite();
|
|
// There should be the root commit and the working copy commit, plus
|
|
// 1 more
|
|
assert_eq!(index.num_commits(), 2 + 1);
|
|
|
|
let stats = index.stats();
|
|
assert_eq!(stats.num_commits, 2 + 1);
|
|
assert_eq!(stats.num_merges, 0);
|
|
assert_eq!(stats.max_generation_number, 1);
|
|
assert_eq!(stats.levels.len(), 2);
|
|
assert_eq!(stats.levels[0].num_commits, 0);
|
|
assert_eq!(stats.levels[1].num_commits, 3);
|
|
|
|
assert_eq!(generation_number(&index, root_commit.id()), 0);
|
|
assert_eq!(generation_number(&index, commit_a.id()), 1);
|
|
}
|