From 05e91491574cb578b1d8d84dea7e11a7fec87ecc Mon Sep 17 00:00:00 2001 From: Martin von Zweigbergk Date: Thu, 15 Apr 2021 22:52:33 -0700 Subject: [PATCH] revsets: add a non_obsolete_heads() revset This change adds a `non_obsolete_heads()` revset, which walks up ancestors of the input set until it gets to a non-obsolete and non-pruned commit. That's what we do by default in `jj log` (i.e. without `--all`). Now we can make `jj log` use revsets and teach it a `-r` option! --- lib/src/index.rs | 7 +++++ lib/src/revset.rs | 57 ++++++++++++++++++++++++++++++++++++++++ lib/tests/test_revset.rs | 47 +++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+) diff --git a/lib/src/index.rs b/lib/src/index.rs index df781746e..00414f9d6 100644 --- a/lib/src/index.rs +++ b/lib/src/index.rs @@ -18,6 +18,7 @@ use std::cmp::{max, min, Ordering}; use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashSet}; use std::fmt::{Debug, Formatter}; use std::fs::File; +use std::hash::{Hash, Hasher}; use std::io; use std::io::{Cursor, Read, Write}; use std::ops::Bound; @@ -1327,6 +1328,12 @@ impl PartialEq for IndexEntry<'_> { } impl Eq for IndexEntry<'_> {} +impl Hash for IndexEntry<'_> { + fn hash(&self, state: &mut H) { + self.pos.hash(state) + } +} + impl<'a> IndexEntry<'a> { pub fn position(&self) -> u32 { self.pos diff --git a/lib/src/revset.rs b/lib/src/revset.rs index 4a8b3195b..bb2b94099 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -13,6 +13,7 @@ // limitations under the License. use std::cmp::Reverse; +use std::collections::HashSet; use pest::iterators::Pairs; use pest::Parser; @@ -100,6 +101,7 @@ pub enum RevsetExpression { Parents(Box), Ancestors(Box), AllHeads, + NonObsoleteHeads(Box), } fn parse_expression_rule(mut pairs: Pairs) -> Result { @@ -175,6 +177,25 @@ fn parse_function_expression( }) } } + "non_obsolete_heads" => { + if arg_count == 0 { + Ok(RevsetExpression::NonObsoleteHeads(Box::new( + RevsetExpression::AllHeads, + ))) + } else if arg_count == 1 { + Ok(RevsetExpression::NonObsoleteHeads(Box::new( + parse_function_argument_to_expression( + &name, + argument_pairs.next().unwrap().into_inner(), + )?, + ))) + } else { + Err(RevsetParseError::InvalidFunctionArguments { + name, + message: "Expected 0 or 1 argument".to_string(), + }) + } + } _ => Err(RevsetParseError::NoSuchFunction(name)), } } @@ -290,5 +311,41 @@ pub fn evaluate_expression<'repo>( index_entries.sort_by_key(|b| Reverse(b.position())); Ok(Box::new(EagerRevset { index_entries })) } + RevsetExpression::NonObsoleteHeads(base_expression) => { + let base_set = evaluate_expression(repo, base_expression.as_ref())?; + Ok(non_obsolete_heads(repo, base_set)) + } } } + +fn non_obsolete_heads<'repo>( + repo: RepoRef<'repo>, + heads: Box + 'repo>, +) -> Box + 'repo> { + let mut commit_ids = HashSet::new(); + let mut work: Vec<_> = heads.iter().collect(); + let evolution = repo.evolution(); + while !work.is_empty() { + let index_entry = work.pop().unwrap(); + let commit_id = index_entry.commit_id(); + if commit_ids.contains(&commit_id) { + continue; + } + if !index_entry.is_pruned() && !evolution.is_obsolete(&commit_id) { + commit_ids.insert(commit_id); + } else { + for parent_entry in index_entry.parents() { + work.push(parent_entry); + } + } + } + let index = repo.index(); + let commit_ids = index.heads(&commit_ids); + let mut index_entries: Vec<_> = commit_ids + .iter() + .map(|id| index.entry_by_id(id).unwrap()) + .collect(); + index_entries.sort_by_key(|b| Reverse(b.position())); + index_entries.sort_by_key(|b| Reverse(b.position())); + Box::new(EagerRevset { index_entries }) +} diff --git a/lib/tests/test_revset.rs b/lib/tests/test_revset.rs index e8a619886..e2840ba50 100644 --- a/lib/tests/test_revset.rs +++ b/lib/tests/test_revset.rs @@ -422,3 +422,50 @@ fn test_evaluate_expression_all_heads(use_git: bool) { tx.discard(); } + +#[test_case(false ; "local store")] +#[test_case(true ; "git store")] +fn test_evaluate_expression_obsolete(use_git: bool) { + let settings = testutils::user_settings(); + let (_temp_dir, repo) = testutils::init_repo(&settings, use_git); + + let mut tx = repo.start_transaction("test"); + let mut_repo = tx.mut_repo(); + + let root_commit = repo.store().root_commit(); + let wc_commit = repo.working_copy_locked().current_commit(); + let commit1 = testutils::create_random_commit(&settings, &repo).write_to_repo(mut_repo); + let commit2 = testutils::create_random_commit(&settings, &repo) + .set_predecessors(vec![commit1.id().clone()]) + .set_change_id(commit1.change_id().clone()) + .write_to_repo(mut_repo); + let commit3 = testutils::create_random_commit(&settings, &repo) + .set_predecessors(vec![commit2.id().clone()]) + .set_change_id(commit2.change_id().clone()) + .write_to_repo(mut_repo); + let commit4 = testutils::create_random_commit(&settings, &repo) + .set_parents(vec![commit3.id().clone()]) + .set_pruned(true) + .write_to_repo(mut_repo); + + assert_eq!( + resolve_commit_ids(mut_repo.as_repo_ref(), "non_obsolete_heads()"), + vec![commit3.id().clone(), wc_commit.id().clone()] + ); + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!("non_obsolete_heads({})", commit4.id().hex()) + ), + vec![commit3.id().clone()] + ); + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!("non_obsolete_heads({})", commit1.id().hex()) + ), + vec![root_commit.id().clone()] + ); + + tx.discard(); +}