view: add tracking of "public" heads (copying Mercurial's phase concept)

Mercurial's "phase" concept is important for evolution, and it's also
useful for filtering out uninteresting commits from log
output. Commits are typically marked "public" when they are pushed to
a remote. The CLI prevents public commits from being rewritten. Public
commits cannot be obsolete (even if they have a successor, they won't
be considered obsolete like non-public commits would).

This commits just makes space for tracking the public heads in the
View.
This commit is contained in:
Martin von Zweigbergk 2021-01-16 10:42:22 -08:00
parent 265f90185e
commit 4db3d8d3a6
6 changed files with 131 additions and 1 deletions

View File

@ -23,6 +23,7 @@ message GitRef {
message View {
repeated bytes head_ids = 1;
repeated bytes public_head_ids = 4;
bytes checkout = 2;
// Only a subset of the refs. For example, does not include refs/notes/.
repeated GitRef git_refs = 3;

View File

@ -52,6 +52,8 @@ impl OperationId {
pub struct View {
/// All head commits
pub head_ids: HashSet<CommitId>,
/// Heads of the set of public commits.
pub public_head_ids: HashSet<CommitId>,
pub git_refs: BTreeMap<String, CommitId>,
// The commit that *should be* checked out in the (default) working copy. Note that the
// working copy (.jj/working_copy/) has the source of truth about which commit *is* checked out
@ -64,6 +66,7 @@ impl View {
pub fn new(checkout: CommitId) -> Self {
Self {
head_ids: HashSet::new(),
public_head_ids: HashSet::new(),
git_refs: BTreeMap::new(),
checkout,
}

View File

@ -198,6 +198,9 @@ fn view_to_proto(view: &View) -> crate::protos::op_store::View {
for head_id in &view.head_ids {
proto.head_ids.push(head_id.0.clone());
}
for head_id in &view.public_head_ids {
proto.public_head_ids.push(head_id.0.clone());
}
for (git_ref_name, commit_id) in &view.git_refs {
let mut git_ref_proto = crate::protos::op_store::GitRef::new();
git_ref_proto.set_name(git_ref_name.clone());
@ -212,6 +215,10 @@ fn view_from_proto(proto: &crate::protos::op_store::View) -> View {
for head_id_bytes in proto.head_ids.iter() {
view.head_ids.insert(CommitId(head_id_bytes.to_vec()));
}
for head_id_bytes in proto.public_head_ids.iter() {
view.public_head_ids
.insert(CommitId(head_id_bytes.to_vec()));
}
for git_ref in proto.git_refs.iter() {
view.git_refs
.insert(git_ref.name.clone(), CommitId(git_ref.commit_id.to_vec()));

View File

@ -171,6 +171,18 @@ impl<'r> Transaction<'r> {
mut_repo.evolution.as_mut().unwrap().invalidate();
}
pub fn add_public_head(&mut self, head: &Commit) {
let mut_repo = Arc::get_mut(self.repo.as_mut().unwrap()).unwrap();
mut_repo.view.as_mut().unwrap().add_public_head(head);
mut_repo.evolution.as_mut().unwrap().add_commit(head);
}
pub fn remove_public_head(&mut self, head: &Commit) {
let mut_repo = Arc::get_mut(self.repo.as_mut().unwrap()).unwrap();
mut_repo.view.as_mut().unwrap().remove_public_head(head);
mut_repo.evolution.as_mut().unwrap().invalidate();
}
pub fn insert_git_ref(&mut self, name: String, commit_id: CommitId) {
let mut_repo = Arc::get_mut(self.repo.as_mut().unwrap()).unwrap();
mut_repo

View File

@ -32,6 +32,7 @@ use crate::store_wrapper::StoreWrapper;
pub trait View {
fn checkout(&self) -> &CommitId;
fn heads<'a>(&'a self) -> Box<dyn Iterator<Item = &'a CommitId> + 'a>;
fn public_heads<'a>(&'a self) -> Box<dyn Iterator<Item = &'a CommitId> + 'a>;
fn git_refs(&self) -> &BTreeMap<String, CommitId>;
fn op_store(&self) -> Arc<dyn OpStore>;
fn base_op_head_id(&self) -> &OperationId;
@ -66,6 +67,8 @@ fn enforce_invariants(store: &StoreWrapper, view: &mut op_store::View) {
// TODO: This is surely terribly slow on large repos, at least in its current
// form. We should make it faster (using the index) and avoid calling it in
// most cases (avoid adding a head that's already reachable in the view).
view.public_head_ids = heads_of_set(store, view.public_head_ids.iter().cloned());
view.head_ids.extend(view.public_head_ids.iter().cloned());
view.head_ids.extend(view.git_refs.values().cloned());
view.head_ids = heads_of_set(store, view.head_ids.iter().cloned());
}
@ -150,6 +153,14 @@ pub fn merge_views(
// and emit a warning?
}
for removed_head in base.public_head_ids.difference(&right.public_head_ids) {
result.public_head_ids.remove(removed_head);
}
for added_head in right.public_head_ids.difference(&base.public_head_ids) {
result.public_head_ids.insert(added_head.clone());
}
result.public_head_ids = heads_of_set(store, result.public_head_ids.into_iter());
for removed_head in base.head_ids.difference(&right.head_ids) {
result.head_ids.remove(removed_head);
}
@ -316,6 +327,10 @@ impl View for ReadonlyView {
Box::new(self.data.head_ids.iter())
}
fn public_heads<'a>(&'a self) -> Box<dyn Iterator<Item = &'a CommitId> + 'a> {
Box::new(self.data.public_head_ids.iter())
}
fn git_refs(&self) -> &BTreeMap<String, CommitId> {
&self.data.git_refs
}
@ -407,6 +422,10 @@ impl View for MutableView {
Box::new(self.data.head_ids.iter())
}
fn public_heads<'a>(&'a self) -> Box<dyn Iterator<Item = &'a CommitId> + 'a> {
Box::new(self.data.public_head_ids.iter())
}
fn git_refs(&self) -> &BTreeMap<String, CommitId> {
&self.data.git_refs
}
@ -435,10 +454,18 @@ impl MutableView {
pub fn remove_head(&mut self, head: &Commit) {
self.data.head_ids.remove(head.id());
// To potentially add back heads based on git refs
enforce_invariants(&self.store, &mut self.data);
}
pub fn add_public_head(&mut self, head: &Commit) {
self.data.public_head_ids.insert(head.id().clone());
enforce_invariants(&self.store, &mut self.data);
}
pub fn remove_public_head(&mut self, head: &Commit) {
self.data.public_head_ids.remove(head.id());
}
pub fn insert_git_ref(&mut self, name: String, commit_id: CommitId) {
self.data.git_refs.insert(name, commit_id);
}

View File

@ -428,3 +428,83 @@ fn test_remove_head_ancestor_git_ref(use_git: bool) {
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");
let public_heads: HashSet<_> = tx.as_repo().view().public_heads().cloned().collect();
assert!(!public_heads.contains(commit1.id()));
tx.add_public_head(&commit1);
let public_heads: HashSet<_> = tx.as_repo().view().public_heads().cloned().collect();
assert!(public_heads.contains(commit1.id()));
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let public_heads: HashSet<_> = repo.view().public_heads().cloned().collect();
assert!(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");
let public_heads: HashSet<_> = tx.as_repo().view().public_heads().cloned().collect();
assert!(!public_heads.contains(commit1.id()));
tx.add_public_head(&commit1);
let public_heads: HashSet<_> = tx.as_repo().view().public_heads().cloned().collect();
assert!(!public_heads.contains(commit1.id()));
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let public_heads: HashSet<_> = repo.view().public_heads().cloned().collect();
assert!(!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");
let public_heads: HashSet<_> = tx.as_repo().view().public_heads().cloned().collect();
assert!(public_heads.contains(commit1.id()));
tx.remove_public_head(&commit1);
let public_heads: HashSet<_> = tx.as_repo().view().public_heads().cloned().collect();
assert!(!public_heads.contains(commit1.id()));
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let public_heads: HashSet<_> = repo.view().public_heads().cloned().collect();
assert!(!public_heads.contains(commit1.id()));
}