use std::slice;
use assert_matches::assert_matches;
use itertools::Itertools as _;
use jj_lib::backend::CommitId;
use jj_lib::commit::Commit;
use jj_lib::config::ConfigLayer;
use jj_lib::config::ConfigSource;
use jj_lib::evolution::CommitEvolutionEntry;
use jj_lib::evolution::WalkPredecessorsError;
use jj_lib::evolution::accumulate_predecessors;
use jj_lib::evolution::walk_predecessors;
use jj_lib::repo::MutableRepo;
use jj_lib::repo::ReadonlyRepo;
use jj_lib::repo::Repo as _;
use jj_lib::settings::UserSettings;
use maplit::btreemap;
use pollster::FutureExt as _;
use testutils::CommitBuilderExt as _;
use testutils::TestRepo;
use testutils::TestResult;
use testutils::commit_transactions;
use testutils::write_random_commit;
fn collect_predecessors(repo: &ReadonlyRepo, start_commit: &CommitId) -> Vec<CommitEvolutionEntry> {
walk_predecessors(repo, slice::from_ref(start_commit))
.try_collect()
.unwrap()
}
#[test]
fn test_walk_predecessors_basic() -> TestResult {
let test_repo = TestRepo::init();
let repo0 = test_repo.repo;
let root_commit = repo0.store().root_commit();
let mut tx = repo0.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let repo1 = tx.commit("test").block_on()?;
let mut tx = repo1.start_transaction();
let commit2 = tx
.repo_mut()
.rewrite_commit(&commit1)
.set_description("rewritten")
.write_unwrap();
tx.repo_mut().rebase_descendants().block_on()?;
let repo2 = tx.commit("test").block_on()?;
let entries = collect_predecessors(&repo2, root_commit.id());
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].commit, root_commit);
assert_eq!(entries[0].operation.as_ref(), None);
assert_eq!(entries[0].predecessor_ids(), []);
let entries = collect_predecessors(&repo2, commit1.id());
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].commit, commit1);
assert_eq!(entries[0].operation.as_ref(), Some(repo1.operation()));
assert_eq!(entries[0].predecessor_ids(), []);
let entries = collect_predecessors(&repo2, commit2.id());
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].commit, commit2);
assert_eq!(entries[0].operation.as_ref(), Some(repo2.operation()));
assert_eq!(entries[0].predecessor_ids(), [commit1.id().clone()]);
assert_eq!(entries[1].commit, commit1);
assert_eq!(entries[1].operation.as_ref(), Some(repo1.operation()));
assert_eq!(entries[1].predecessor_ids(), []);
Ok(())
}
#[test]
fn test_walk_predecessors_basic_legacy_op() -> TestResult {
let test_repo = TestRepo::init();
let repo0 = test_repo.repo;
let loader = repo0.loader();
let mut tx = repo0.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let repo1 = tx.commit("test").block_on()?;
let mut tx = repo1.start_transaction();
let commit2 = tx
.repo_mut()
.rewrite_commit(&commit1)
.set_description("rewritten")
.write_unwrap();
tx.repo_mut().rebase_descendants().block_on()?;
let repo2 = tx.commit("test").block_on()?;
let repo2 = {
let mut data = repo2.operation().store_operation().clone();
data.commit_predecessors = None;
let op_id = loader.op_store().write_operation(&data).block_on()?;
let op = loader.load_operation(&op_id).block_on()?;
loader.load_at(&op).block_on()?
};
let entries = collect_predecessors(&repo2, commit2.id());
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].commit, commit2);
assert_eq!(entries[0].operation.as_ref(), None);
assert_eq!(entries[0].predecessor_ids(), [commit1.id().clone()]);
assert_eq!(entries[1].commit, commit1);
assert_eq!(entries[1].operation.as_ref(), None);
assert_eq!(entries[1].predecessor_ids(), []);
Ok(())
}
#[test]
fn test_walk_predecessors_concurrent_ops() -> TestResult {
let test_repo = TestRepo::init();
let repo0 = test_repo.repo;
let mut tx = repo0.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let repo1 = tx.commit("test").block_on()?;
let mut tx2 = repo1.start_transaction();
let commit2 = tx2
.repo_mut()
.rewrite_commit(&commit1)
.set_description("rewritten 2")
.write_unwrap();
tx2.repo_mut().rebase_descendants().block_on()?;
let mut tx3 = repo1.start_transaction();
let commit3 = tx3
.repo_mut()
.rewrite_commit(&commit1)
.set_description("rewritten 3")
.write_unwrap();
tx3.repo_mut().rebase_descendants().block_on()?;
let repo4 = commit_transactions(vec![tx2, tx3]);
let [op2, op3] = repo4
.operation()
.parents()
.block_on()
.unwrap()
.into_iter()
.collect_array()
.unwrap();
let mut tx = repo4.start_transaction();
let commit4 = tx
.repo_mut()
.rewrite_commit(&commit2)
.set_description("rewritten 4")
.write_unwrap();
let commit5 = tx
.repo_mut()
.rewrite_commit(&commit3)
.set_description("rewritten 5")
.write_unwrap();
tx.repo_mut().rebase_descendants().block_on()?;
let repo5 = tx.commit("test").block_on()?;
let entries = collect_predecessors(&repo5, commit4.id());
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].commit, commit4);
assert_eq!(entries[0].operation.as_ref(), Some(repo5.operation()));
assert_eq!(entries[0].predecessor_ids(), [commit2.id().clone()]);
assert_eq!(entries[1].commit, commit2);
assert_eq!(entries[1].operation.as_ref(), Some(&op2));
assert_eq!(entries[1].predecessor_ids(), [commit1.id().clone()]);
assert_eq!(entries[2].commit, commit1);
assert_eq!(entries[2].operation.as_ref(), Some(repo1.operation()));
assert_eq!(entries[2].predecessor_ids(), []);
let entries = collect_predecessors(&repo5, commit5.id());
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].commit, commit5);
assert_eq!(entries[0].operation.as_ref(), Some(repo5.operation()));
assert_eq!(entries[0].predecessor_ids(), [commit3.id().clone()]);
assert_eq!(entries[1].commit, commit3);
assert_eq!(entries[1].operation.as_ref(), Some(&op3));
assert_eq!(entries[1].predecessor_ids(), [commit1.id().clone()]);
assert_eq!(entries[2].commit, commit1);
assert_eq!(entries[2].operation.as_ref(), Some(repo1.operation()));
assert_eq!(entries[2].predecessor_ids(), []);
Ok(())
}
#[test]
fn test_walk_predecessors_multiple_predecessors_across_ops() -> TestResult {
let test_repo = TestRepo::init();
let repo0 = test_repo.repo;
let mut tx = repo0.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let repo1 = tx.commit("test").block_on()?;
let mut tx = repo1.start_transaction();
let commit2 = write_random_commit(tx.repo_mut());
let repo2 = tx.commit("test").block_on()?;
let mut tx = repo2.start_transaction();
let commit3 = tx
.repo_mut()
.rewrite_commit(&commit2)
.set_predecessors(vec![commit2.id().clone(), commit1.id().clone()])
.set_description("rewritten")
.write_unwrap();
tx.repo_mut().rebase_descendants().block_on()?;
let repo3 = tx.commit("test").block_on()?;
let entries = collect_predecessors(&repo3, commit3.id());
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].commit, commit3);
assert_eq!(entries[0].operation.as_ref(), Some(repo3.operation()));
assert_eq!(
entries[0].predecessor_ids(),
[commit2.id().clone(), commit1.id().clone()]
);
assert_eq!(entries[1].commit, commit2);
assert_eq!(entries[1].operation.as_ref(), Some(repo2.operation()));
assert_eq!(entries[1].predecessor_ids(), []);
assert_eq!(entries[2].commit, commit1);
assert_eq!(entries[2].operation.as_ref(), Some(repo1.operation()));
assert_eq!(entries[2].predecessor_ids(), []);
Ok(())
}
#[test]
fn test_walk_predecessors_multiple_predecessors_within_op() -> TestResult {
let test_repo = TestRepo::init();
let repo0 = test_repo.repo;
let mut tx = repo0.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let commit2 = write_random_commit(tx.repo_mut());
let repo1 = tx.commit("test").block_on()?;
let mut tx = repo1.start_transaction();
let commit3 = tx
.repo_mut()
.rewrite_commit(&commit1)
.set_predecessors(vec![commit1.id().clone(), commit2.id().clone()])
.set_description("rewritten")
.write_unwrap();
tx.repo_mut().rebase_descendants().block_on()?;
let repo2 = tx.commit("test").block_on()?;
let entries = collect_predecessors(&repo2, commit3.id());
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].commit, commit3);
assert_eq!(entries[0].operation.as_ref(), Some(repo2.operation()));
assert_eq!(
entries[0].predecessor_ids(),
[commit1.id().clone(), commit2.id().clone()]
);
assert_eq!(entries[1].commit, commit1);
assert_eq!(entries[1].operation.as_ref(), Some(repo1.operation()));
assert_eq!(entries[1].predecessor_ids(), []);
assert_eq!(entries[2].commit, commit2);
assert_eq!(entries[2].operation.as_ref(), Some(repo1.operation()));
assert_eq!(entries[2].predecessor_ids(), []);
Ok(())
}
#[test]
fn test_walk_predecessors_transitive() -> TestResult {
let test_repo = TestRepo::init();
let repo0 = test_repo.repo;
let mut tx = repo0.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let repo1 = tx.commit("test").block_on()?;
let mut tx = repo1.start_transaction();
let commit2 = tx
.repo_mut()
.rewrite_commit(&commit1)
.set_description("rewritten 2")
.write_unwrap();
let commit3 = tx
.repo_mut()
.rewrite_commit(&commit2)
.set_description("rewritten 3")
.write_unwrap();
tx.repo_mut().rebase_descendants().block_on()?;
let repo2 = tx.commit("test").block_on()?;
let entries = collect_predecessors(&repo2, commit3.id());
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].commit, commit3);
assert_eq!(entries[0].operation.as_ref(), Some(repo2.operation()));
assert_eq!(entries[0].predecessor_ids(), [commit2.id().clone()]);
assert_eq!(entries[1].commit, commit2);
assert_eq!(entries[1].operation.as_ref(), Some(repo2.operation()));
assert_eq!(entries[1].predecessor_ids(), [commit1.id().clone()]);
assert_eq!(entries[2].commit, commit1);
assert_eq!(entries[2].operation.as_ref(), Some(repo1.operation()));
assert_eq!(entries[2].predecessor_ids(), []);
Ok(())
}
#[test]
fn test_walk_predecessors_transitive_graph_order() -> TestResult {
let test_repo = TestRepo::init();
let repo0 = test_repo.repo;
let mut tx = repo0.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let commit2 = tx
.repo_mut()
.rewrite_commit(&commit1)
.set_description("rewritten 2")
.write_unwrap();
let commit3 = tx
.repo_mut()
.rewrite_commit(&commit2)
.set_description("rewritten 3")
.write_unwrap();
let commit4 = tx
.repo_mut()
.rewrite_commit(&commit1)
.set_description("rewritten 4")
.write_unwrap();
tx.repo_mut().rebase_descendants().block_on()?;
let repo1 = tx.commit("test").block_on()?;
let mut tx = repo1.start_transaction();
let commit5 = tx
.repo_mut()
.rewrite_commit(&commit4)
.set_predecessors(vec![commit4.id().clone(), commit3.id().clone()])
.set_description("rewritten 5")
.write_unwrap();
tx.repo_mut().rebase_descendants().block_on()?;
let repo2 = tx.commit("test").block_on()?;
let entries = collect_predecessors(&repo2, commit5.id());
assert_eq!(entries.len(), 5);
assert_eq!(entries[0].commit, commit5);
assert_eq!(entries[0].operation.as_ref(), Some(repo2.operation()));
assert_eq!(
entries[0].predecessor_ids(),
[commit4.id().clone(), commit3.id().clone()]
);
assert_eq!(entries[1].commit, commit4);
assert_eq!(entries[1].operation.as_ref(), Some(repo1.operation()));
assert_eq!(entries[1].predecessor_ids(), [commit1.id().clone()]);
assert_eq!(entries[2].commit, commit3);
assert_eq!(entries[2].operation.as_ref(), Some(repo1.operation()));
assert_eq!(entries[2].predecessor_ids(), [commit2.id().clone()]);
assert_eq!(entries[3].commit, commit2);
assert_eq!(entries[3].operation.as_ref(), Some(repo1.operation()));
assert_eq!(entries[3].predecessor_ids(), [commit1.id().clone()]);
assert_eq!(entries[4].commit, commit1);
assert_eq!(entries[4].operation.as_ref(), Some(repo1.operation()));
assert_eq!(entries[4].predecessor_ids(), []);
Ok(())
}
#[test]
fn test_walk_predecessors_unsimplified() -> TestResult {
let test_repo = TestRepo::init();
let repo0 = test_repo.repo;
let mut tx = repo0.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let repo1 = tx.commit("test").block_on()?;
let mut tx = repo1.start_transaction();
let commit2 = tx
.repo_mut()
.rewrite_commit(&commit1)
.set_description("rewritten 2")
.write_unwrap();
tx.repo_mut().rebase_descendants().block_on()?;
let repo2 = tx.commit("test").block_on()?;
let mut tx = repo2.start_transaction();
let commit3 = tx
.repo_mut()
.rewrite_commit(&commit1)
.set_predecessors(vec![commit1.id().clone(), commit2.id().clone()])
.set_description("rewritten 3")
.write_unwrap();
tx.repo_mut().rebase_descendants().block_on()?;
let repo3 = tx.commit("test").block_on()?;
let entries = collect_predecessors(&repo3, commit3.id());
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].commit, commit3);
assert_eq!(entries[0].operation.as_ref(), Some(repo3.operation()));
assert_eq!(
entries[0].predecessor_ids(),
[commit1.id().clone(), commit2.id().clone()]
);
assert_eq!(entries[1].commit, commit2);
assert_eq!(entries[1].operation.as_ref(), Some(repo2.operation()));
assert_eq!(entries[1].predecessor_ids(), [commit1.id().clone()]);
assert_eq!(entries[2].commit, commit1);
assert_eq!(entries[2].operation.as_ref(), Some(repo1.operation()));
assert_eq!(entries[2].predecessor_ids(), []);
Ok(())
}
#[test]
fn test_walk_predecessors_direct_cycle_within_op() -> TestResult {
let test_repo = TestRepo::init();
let repo0 = test_repo.repo;
let loader = repo0.loader();
let mut tx = repo0.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let repo1 = tx.commit("test").block_on()?;
let repo1 = {
let mut data = repo1.operation().store_operation().clone();
data.commit_predecessors = Some(btreemap! {
commit1.id().clone() => vec![commit1.id().clone()],
});
let op_id = loader.op_store().write_operation(&data).block_on()?;
let op = loader.load_operation(&op_id).block_on()?;
loader.load_at(&op).block_on()?
};
assert_matches!(
walk_predecessors(&repo1, slice::from_ref(commit1.id())).next(),
Some(Err(WalkPredecessorsError::CycleDetected(_)))
);
Ok(())
}
#[test]
fn test_walk_predecessors_indirect_cycle_within_op() -> TestResult {
let test_repo = TestRepo::init();
let repo0 = test_repo.repo;
let loader = repo0.loader();
let mut tx = repo0.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let commit2 = write_random_commit(tx.repo_mut());
let commit3 = write_random_commit(tx.repo_mut());
let repo1 = tx.commit("test").block_on()?;
let repo1 = {
let mut data = repo1.operation().store_operation().clone();
data.commit_predecessors = Some(btreemap! {
commit1.id().clone() => vec![commit3.id().clone()],
commit2.id().clone() => vec![commit1.id().clone()],
commit3.id().clone() => vec![commit2.id().clone()],
});
let op_id = loader.op_store().write_operation(&data).block_on()?;
let op = loader.load_operation(&op_id).block_on()?;
loader.load_at(&op).block_on()?
};
assert_matches!(
walk_predecessors(&repo1, slice::from_ref(commit3.id())).next(),
Some(Err(WalkPredecessorsError::CycleDetected(_)))
);
Ok(())
}
#[test]
fn test_accumulate_predecessors() -> TestResult {
let mut config = testutils::base_user_config();
let mut layer = ConfigLayer::empty(ConfigSource::User);
layer.set_value("debug.commit-timestamp", "2001-02-03T04:05:06+07:00")?;
config.add_layer(layer);
let settings = UserSettings::from_config(config)?;
let test_repo = TestRepo::init_with_settings(&settings);
let repo_0 = test_repo.repo;
fn new_commit(repo: &mut MutableRepo, desc: &str) -> Commit {
repo.new_commit(
vec![repo.store().root_commit_id().clone()],
repo.store().empty_merged_tree(),
)
.set_description(desc)
.write_unwrap()
}
fn rewrite_commit(repo: &mut MutableRepo, predecessors: &[&Commit], desc: &str) -> Commit {
repo.rewrite_commit(predecessors[0])
.set_predecessors(predecessors.iter().map(|c| c.id().clone()).collect())
.set_description(desc)
.write_unwrap()
}
let mut tx = repo_0.start_transaction();
let commit_a1 = new_commit(tx.repo_mut(), "a1");
let commit_a2 = new_commit(tx.repo_mut(), "a2");
let commit_a3 = new_commit(tx.repo_mut(), "a3");
let repo_a = tx.commit("a").block_on()?;
let mut tx = repo_a.start_transaction();
let commit_b1 = rewrite_commit(tx.repo_mut(), &[&commit_a1], "b1");
let commit_b2 = rewrite_commit(tx.repo_mut(), &[&commit_a2, &commit_a3], "b2");
tx.repo_mut().rebase_descendants().block_on()?;
let repo_b = tx.commit("b").block_on()?;
let mut tx = repo_b.start_transaction();
let commit_c1 = rewrite_commit(tx.repo_mut(), &[&commit_b1], "c1");
let commit_c2 = rewrite_commit(tx.repo_mut(), &[&commit_b2, &commit_a3], "c2");
let commit_c3 = rewrite_commit(tx.repo_mut(), &[&commit_c2], "c3");
tx.repo_mut().rebase_descendants().block_on()?;
let repo_c = tx.commit("c").block_on()?;
let mut tx = repo_a.start_transaction();
let commit_d1 = rewrite_commit(tx.repo_mut(), &[&commit_a1], "d1");
let commit_d2 = rewrite_commit(tx.repo_mut(), &[&commit_a2], "d2");
tx.repo_mut().rebase_descendants().block_on()?;
let repo_d = tx.commit("d").block_on()?;
let predecessors =
accumulate_predecessors(&[], slice::from_ref(repo_c.operation())).block_on()?;
assert!(predecessors.is_empty());
let predecessors =
accumulate_predecessors(slice::from_ref(repo_c.operation()), &[]).block_on()?;
assert!(predecessors.is_empty());
let predecessors = accumulate_predecessors(
slice::from_ref(repo_c.operation()),
slice::from_ref(repo_c.operation()),
)
.block_on()?;
assert!(predecessors.is_empty());
let predecessors = accumulate_predecessors(
slice::from_ref(repo_c.operation()),
slice::from_ref(repo_b.operation()),
)
.block_on()?;
assert_eq!(
predecessors,
btreemap! {
commit_c1.id().clone() => vec![commit_b1.id().clone()],
commit_c2.id().clone() => vec![commit_b2.id().clone(), commit_a3.id().clone()],
commit_c3.id().clone() => vec![commit_b2.id().clone(), commit_a3.id().clone()],
}
);
let predecessors = accumulate_predecessors(
slice::from_ref(repo_c.operation()),
slice::from_ref(repo_a.operation()),
)
.block_on()?;
assert_eq!(
predecessors,
btreemap! {
commit_b1.id().clone() => vec![commit_a1.id().clone()],
commit_b2.id().clone() => vec![commit_a2.id().clone(), commit_a3.id().clone()],
commit_c1.id().clone() => vec![commit_a1.id().clone()],
commit_c2.id().clone() => vec![commit_a2.id().clone(), commit_a3.id().clone()],
commit_c3.id().clone() => vec![commit_a2.id().clone(), commit_a3.id().clone()],
}
);
let predecessors = accumulate_predecessors(
slice::from_ref(repo_a.operation()),
slice::from_ref(repo_c.operation()),
)
.block_on()?;
assert_eq!(
predecessors,
btreemap! {
commit_a1.id().clone() => vec![commit_c1.id().clone()],
commit_a2.id().clone() => vec![commit_c3.id().clone()],
commit_a3.id().clone() => vec![commit_c3.id().clone()],
commit_b1.id().clone() => vec![commit_c1.id().clone()],
commit_b2.id().clone() => vec![commit_c3.id().clone()],
commit_c2.id().clone() => vec![commit_c3.id().clone()],
}
);
let predecessors = accumulate_predecessors(
slice::from_ref(repo_d.operation()),
slice::from_ref(repo_c.operation()),
)
.block_on()?;
assert_eq!(
predecessors,
btreemap! {
commit_a1.id().clone() => vec![commit_c1.id().clone()],
commit_a2.id().clone() => vec![commit_c3.id().clone()],
commit_b1.id().clone() => vec![commit_c1.id().clone()],
commit_b2.id().clone() => vec![commit_c3.id().clone()],
commit_c2.id().clone() => vec![commit_c3.id().clone()],
commit_d1.id().clone() => vec![commit_c1.id().clone()],
commit_d2.id().clone() => vec![commit_c3.id().clone()],
}
);
Ok(())
}