mirror of
https://github.com/martinvonz/jj.git
synced 2025-02-01 00:50:57 +00:00
revset: parse "all:" prefix rule by pest
I had to use negative lookahead !":" because we still support a dummy ":" operator to provide a suggestion.
This commit is contained in:
parent
13dadadcdc
commit
bb87fac1a4
4 changed files with 197 additions and 10 deletions
|
@ -52,7 +52,7 @@ use jj_lib::repo::{
|
||||||
};
|
};
|
||||||
use jj_lib::repo_path::{FsPathParseError, RepoPath, RepoPathBuf};
|
use jj_lib::repo_path::{FsPathParseError, RepoPath, RepoPathBuf};
|
||||||
use jj_lib::revset::{
|
use jj_lib::revset::{
|
||||||
RevsetAliasesMap, RevsetExpression, RevsetFilterPredicate, RevsetIteratorExt,
|
RevsetAliasesMap, RevsetExpression, RevsetFilterPredicate, RevsetIteratorExt, RevsetModifier,
|
||||||
RevsetParseContext, RevsetWorkspaceContext,
|
RevsetParseContext, RevsetWorkspaceContext,
|
||||||
};
|
};
|
||||||
use jj_lib::rewrite::restore_tree;
|
use jj_lib::rewrite::restore_tree;
|
||||||
|
@ -771,7 +771,11 @@ impl WorkspaceCommandHelper {
|
||||||
) -> Result<IndexSet<Commit>, CommandError> {
|
) -> Result<IndexSet<Commit>, CommandError> {
|
||||||
let mut all_commits = IndexSet::new();
|
let mut all_commits = IndexSet::new();
|
||||||
for revision_str in revision_args {
|
for revision_str in revision_args {
|
||||||
let (expression, all) = self.parse_revset_with_all_prefix(revision_str)?;
|
let (expression, modifier) = self.parse_revset_with_modifier(revision_str)?;
|
||||||
|
let all = match modifier {
|
||||||
|
Some(RevsetModifier::All) => true,
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
if all {
|
if all {
|
||||||
for commit in expression.evaluate_to_commits()? {
|
for commit in expression.evaluate_to_commits()? {
|
||||||
all_commits.insert(commit?);
|
all_commits.insert(commit?);
|
||||||
|
@ -807,16 +811,15 @@ impl WorkspaceCommandHelper {
|
||||||
self.attach_revset_evaluator(expression)
|
self.attach_revset_evaluator(expression)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_revset_with_all_prefix(
|
// TODO: maybe better to parse all: prefix even if it is the default? It
|
||||||
|
// shouldn't be allowed in aliases, though.
|
||||||
|
fn parse_revset_with_modifier(
|
||||||
&self,
|
&self,
|
||||||
revision_str: &str,
|
revision_str: &str,
|
||||||
) -> Result<(RevsetExpressionEvaluator<'_>, bool), CommandError> {
|
) -> Result<(RevsetExpressionEvaluator<'_>, Option<RevsetModifier>), CommandError> {
|
||||||
// TODO: Let pest parse the prefix too once we've dropped support for `:`
|
let context = self.revset_parse_context();
|
||||||
if let Some(revision_str) = revision_str.strip_prefix("all:") {
|
let (expression, modifier) = revset::parse_with_modifier(revision_str, &context)?;
|
||||||
Ok((self.parse_revset(revision_str)?, true))
|
Ok((self.attach_revset_evaluator(expression)?, modifier))
|
||||||
} else {
|
|
||||||
Ok((self.parse_revset(revision_str)?, false))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses the given revset expressions and concatenates them all.
|
/// Parses the given revset expressions and concatenates them all.
|
||||||
|
|
|
@ -66,6 +66,18 @@ fn test_syntax_error() {
|
||||||
= '^' is not a postfix operator
|
= '^' is not a postfix operator
|
||||||
Hint: Did you mean '-' for parents?
|
Hint: Did you mean '-' for parents?
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
|
// "jj new" supports "all:" prefix
|
||||||
|
let stderr = test_env.jj_cmd_failure(&repo_path, &["new", "ale:x"]);
|
||||||
|
insta::assert_snapshot!(stderr, @r###"
|
||||||
|
Error: Failed to parse revset: Modifier "ale" doesn't exist
|
||||||
|
Caused by: --> 1:1
|
||||||
|
|
|
||||||
|
1 | ale:x
|
||||||
|
| ^-^
|
||||||
|
|
|
||||||
|
= Modifier "ale" doesn't exist
|
||||||
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -97,6 +97,10 @@ expression = {
|
||||||
}
|
}
|
||||||
|
|
||||||
program = _{ SOI ~ whitespace* ~ expression ~ whitespace* ~ EOI }
|
program = _{ SOI ~ whitespace* ~ expression ~ whitespace* ~ EOI }
|
||||||
|
program_modifier = { identifier ~ pattern_kind_op ~ !":" }
|
||||||
|
program_with_modifier = _{
|
||||||
|
SOI ~ whitespace* ~ (program_modifier ~ whitespace*)? ~ expression ~ whitespace* ~ EOI
|
||||||
|
}
|
||||||
|
|
||||||
alias_declaration_part = _{
|
alias_declaration_part = _{
|
||||||
function_name ~ "(" ~ whitespace* ~ formal_parameters ~ whitespace* ~ ")"
|
function_name ~ "(" ~ whitespace* ~ formal_parameters ~ whitespace* ~ ")"
|
||||||
|
|
|
@ -135,6 +135,8 @@ impl Rule {
|
||||||
Rule::range_expression => None,
|
Rule::range_expression => None,
|
||||||
Rule::expression => None,
|
Rule::expression => None,
|
||||||
Rule::program => None,
|
Rule::program => None,
|
||||||
|
Rule::program_modifier => None,
|
||||||
|
Rule::program_with_modifier => None,
|
||||||
Rule::alias_declaration_part => None,
|
Rule::alias_declaration_part => None,
|
||||||
Rule::alias_declaration => None,
|
Rule::alias_declaration => None,
|
||||||
}
|
}
|
||||||
|
@ -171,6 +173,8 @@ pub enum RevsetParseErrorKind {
|
||||||
similar_op: String,
|
similar_op: String,
|
||||||
description: String,
|
description: String,
|
||||||
},
|
},
|
||||||
|
#[error(r#"Modifier "{0}" doesn't exist"#)]
|
||||||
|
NoSuchModifier(String),
|
||||||
#[error(r#"Function "{name}" doesn't exist"#)]
|
#[error(r#"Function "{name}" doesn't exist"#)]
|
||||||
NoSuchFunction {
|
NoSuchFunction {
|
||||||
name: String,
|
name: String,
|
||||||
|
@ -271,6 +275,17 @@ fn rename_rules_in_pest_error(mut err: pest::error::Error<Rule>) -> pest::error:
|
||||||
pub const GENERATION_RANGE_FULL: Range<u64> = 0..u64::MAX;
|
pub const GENERATION_RANGE_FULL: Range<u64> = 0..u64::MAX;
|
||||||
pub const GENERATION_RANGE_EMPTY: Range<u64> = 0..0;
|
pub const GENERATION_RANGE_EMPTY: Range<u64> = 0..0;
|
||||||
|
|
||||||
|
/// Global flag applied to the entire expression.
|
||||||
|
///
|
||||||
|
/// The core revset engine doesn't use this value. It's up to caller to
|
||||||
|
/// interpret it to change the evaluation behavior.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum RevsetModifier {
|
||||||
|
/// Expression can be evaluated to multiple revisions even if a single
|
||||||
|
/// revision is expected by default.
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
/// Symbol or function to be resolved to `CommitId`s.
|
/// Symbol or function to be resolved to `CommitId`s.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum RevsetCommitRef {
|
pub enum RevsetCommitRef {
|
||||||
|
@ -842,6 +857,39 @@ fn parse_program(
|
||||||
parse_expression_rule(first.into_inner(), state)
|
parse_expression_rule(first.into_inner(), state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_program_with_modifier(
|
||||||
|
revset_str: &str,
|
||||||
|
state: ParseState,
|
||||||
|
) -> Result<(Rc<RevsetExpression>, Option<RevsetModifier>), RevsetParseError> {
|
||||||
|
let mut pairs = RevsetParser::parse(Rule::program_with_modifier, revset_str)?;
|
||||||
|
let first = pairs.next().unwrap();
|
||||||
|
match first.as_rule() {
|
||||||
|
Rule::expression => {
|
||||||
|
let expression = parse_expression_rule(first.into_inner(), state)?;
|
||||||
|
Ok((expression, None))
|
||||||
|
}
|
||||||
|
Rule::program_modifier => {
|
||||||
|
let (lhs, op) = first.into_inner().collect_tuple().unwrap();
|
||||||
|
let rhs = pairs.next().unwrap();
|
||||||
|
assert_eq!(lhs.as_rule(), Rule::identifier);
|
||||||
|
assert_eq!(op.as_rule(), Rule::pattern_kind_op);
|
||||||
|
assert_eq!(rhs.as_rule(), Rule::expression);
|
||||||
|
let modififer = match lhs.as_str() {
|
||||||
|
"all" => RevsetModifier::All,
|
||||||
|
name => {
|
||||||
|
return Err(RevsetParseError::with_span(
|
||||||
|
RevsetParseErrorKind::NoSuchModifier(name.to_owned()),
|
||||||
|
lhs.as_span(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let expression = parse_expression_rule(rhs.into_inner(), state)?;
|
||||||
|
Ok((expression, Some(modififer)))
|
||||||
|
}
|
||||||
|
r => panic!("unexpected revset parse rule: {r:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_expression_rule(
|
fn parse_expression_rule(
|
||||||
pairs: Pairs<Rule>,
|
pairs: Pairs<Rule>,
|
||||||
state: ParseState,
|
state: ParseState,
|
||||||
|
@ -1525,6 +1573,15 @@ pub fn parse(
|
||||||
parse_program(revset_str, state)
|
parse_program(revset_str, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_with_modifier(
|
||||||
|
revset_str: &str,
|
||||||
|
context: &RevsetParseContext,
|
||||||
|
) -> Result<(Rc<RevsetExpression>, Option<RevsetModifier>), RevsetParseError> {
|
||||||
|
let locals = HashMap::new();
|
||||||
|
let state = ParseState::new(context, &locals);
|
||||||
|
parse_program_with_modifier(revset_str, state)
|
||||||
|
}
|
||||||
|
|
||||||
/// `Some` for rewritten expression, or `None` to reuse the original expression.
|
/// `Some` for rewritten expression, or `None` to reuse the original expression.
|
||||||
type TransformedExpression = Option<Rc<RevsetExpression>>;
|
type TransformedExpression = Option<Rc<RevsetExpression>>;
|
||||||
|
|
||||||
|
@ -2570,6 +2627,29 @@ mod tests {
|
||||||
super::parse(revset_str, &context).map_err(|e| e.kind)
|
super::parse(revset_str, &context).map_err(|e| e.kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_with_modifier(
|
||||||
|
revset_str: &str,
|
||||||
|
) -> Result<(Rc<RevsetExpression>, Option<RevsetModifier>), RevsetParseErrorKind> {
|
||||||
|
parse_with_aliases_and_modifier(revset_str, [] as [(&str, &str); 0])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_with_aliases_and_modifier(
|
||||||
|
revset_str: &str,
|
||||||
|
aliases: impl IntoIterator<Item = (impl AsRef<str>, impl Into<String>)>,
|
||||||
|
) -> Result<(Rc<RevsetExpression>, Option<RevsetModifier>), RevsetParseErrorKind> {
|
||||||
|
let mut aliases_map = RevsetAliasesMap::new();
|
||||||
|
for (decl, defn) in aliases {
|
||||||
|
aliases_map.insert(decl, defn).unwrap();
|
||||||
|
}
|
||||||
|
let context = RevsetParseContext {
|
||||||
|
aliases_map: &aliases_map,
|
||||||
|
user_email: "test.user@example.com".to_string(),
|
||||||
|
workspace: None,
|
||||||
|
};
|
||||||
|
// Map error to comparable object
|
||||||
|
super::parse_with_modifier(revset_str, &context).map_err(|e| e.kind)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[allow(clippy::redundant_clone)] // allow symbol.clone()
|
#[allow(clippy::redundant_clone)] // allow symbol.clone()
|
||||||
fn test_revset_expression_building() {
|
fn test_revset_expression_building() {
|
||||||
|
@ -2902,6 +2982,83 @@ mod tests {
|
||||||
assert!(parse("remote_branches(a,,remote=b)").is_err());
|
assert!(parse("remote_branches(a,,remote=b)").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_revset_with_modifier() {
|
||||||
|
let all_symbol = RevsetExpression::symbol("all".to_owned());
|
||||||
|
let foo_symbol = RevsetExpression::symbol("foo".to_owned());
|
||||||
|
|
||||||
|
// all: is a program modifier, but all:: isn't
|
||||||
|
assert_eq!(
|
||||||
|
parse_with_modifier("all:"),
|
||||||
|
Err(RevsetParseErrorKind::SyntaxError)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_with_modifier("all:foo"),
|
||||||
|
Ok((foo_symbol.clone(), Some(RevsetModifier::All)))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_with_modifier("all::"),
|
||||||
|
Ok((all_symbol.descendants(), None))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_with_modifier("all::foo"),
|
||||||
|
Ok((all_symbol.dag_range_to(&foo_symbol), None))
|
||||||
|
);
|
||||||
|
|
||||||
|
// all::: could be parsed as all:(::), but rejected for simplicity
|
||||||
|
assert_eq!(
|
||||||
|
parse_with_modifier("all:::"),
|
||||||
|
Err(RevsetParseErrorKind::SyntaxError)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_with_modifier("all:::foo"),
|
||||||
|
Err(RevsetParseErrorKind::SyntaxError)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_with_modifier("all:(foo)"),
|
||||||
|
Ok((foo_symbol.clone(), Some(RevsetModifier::All)))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_with_modifier("all:all::foo"),
|
||||||
|
Ok((
|
||||||
|
all_symbol.dag_range_to(&foo_symbol),
|
||||||
|
Some(RevsetModifier::All)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_with_modifier("all:all | foo"),
|
||||||
|
Ok((all_symbol.union(&foo_symbol), Some(RevsetModifier::All)))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_with_modifier("all: ::foo"),
|
||||||
|
Ok((foo_symbol.ancestors(), Some(RevsetModifier::All)))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_with_modifier(" all: foo"),
|
||||||
|
Ok((foo_symbol.clone(), Some(RevsetModifier::All)))
|
||||||
|
);
|
||||||
|
assert_matches!(
|
||||||
|
parse_with_modifier("(all:foo)"),
|
||||||
|
Err(RevsetParseErrorKind::NotInfixOperator { .. })
|
||||||
|
);
|
||||||
|
assert_matches!(
|
||||||
|
parse_with_modifier("all :foo"),
|
||||||
|
Err(RevsetParseErrorKind::SyntaxError)
|
||||||
|
);
|
||||||
|
assert_matches!(
|
||||||
|
parse_with_modifier("all:all:all"),
|
||||||
|
Err(RevsetParseErrorKind::NotInfixOperator { .. })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Top-level string pattern can't be parsed, which is an error anyway
|
||||||
|
assert_eq!(
|
||||||
|
parse_with_modifier(r#"exact:"foo""#),
|
||||||
|
Err(RevsetParseErrorKind::NoSuchModifier("exact".to_owned()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_whitespace() {
|
fn test_parse_whitespace() {
|
||||||
let ascii_whitespaces: String = ('\x00'..='\x7f')
|
let ascii_whitespaces: String = ('\x00'..='\x7f')
|
||||||
|
@ -3286,6 +3443,12 @@ mod tests {
|
||||||
parse("a@B").unwrap()
|
parse("a@B").unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Modifier cannot be substituted.
|
||||||
|
assert_eq!(
|
||||||
|
parse_with_aliases_and_modifier("all:all", [("all", "ALL")]).unwrap(),
|
||||||
|
parse_with_modifier("all:ALL").unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
// Multi-level substitution.
|
// Multi-level substitution.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_with_aliases("A", [("A", "BC"), ("BC", "b|C"), ("C", "c")]).unwrap(),
|
parse_with_aliases("A", [("A", "BC"), ("BC", "b|C"), ("C", "c")]).unwrap(),
|
||||||
|
@ -3307,6 +3470,11 @@ mod tests {
|
||||||
parse_with_aliases("A", [("A", "a(")]),
|
parse_with_aliases("A", [("A", "a(")]),
|
||||||
Err(RevsetParseErrorKind::BadAliasExpansion("A".to_owned()))
|
Err(RevsetParseErrorKind::BadAliasExpansion("A".to_owned()))
|
||||||
);
|
);
|
||||||
|
// Modifier isn't allowed in alias definition.
|
||||||
|
assert_eq!(
|
||||||
|
parse_with_aliases_and_modifier("A", [("A", "all:a")]),
|
||||||
|
Err(RevsetParseErrorKind::BadAliasExpansion("A".to_owned()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
Loading…
Reference in a new issue