revset: insert StringPattern enum to add support for other kind of matching

This commit is contained in:
Yuya Nishihara 2023-08-16 11:35:43 +09:00
parent 313670d3c2
commit 5b3c73dfc4
3 changed files with 236 additions and 126 deletions

View file

@ -86,17 +86,17 @@ revsets (expressions) as arguments.
* `all()`: All visible commits in the repo.
* `none()`: No commits. This function is rarely useful; it is provided for
completeness.
* `branches([needle])`: All local branch targets. If `needle` is specified,
* `branches([pattern])`: All local branch targets. If `pattern` is specified,
branches whose name contains the given string are selected. For example,
`branches(push)` would match the branches `push-123` and `repushed` but not
the branch `main`. If a branch is in a conflicted state, all its possible
targets are included.
* `remote_branches([branch_needle[, [remote=]remote_needle]])`: All remote
branch targets across all remotes. If just the `branch_needle` is specified,
* `remote_branches([branch_pattern[, [remote=]remote_pattern]])`: All remote
branch targets across all remotes. If just the `branch_pattern` is specified,
branches whose name contains the given string across all remotes are
selected. If both `branch_needle` and `remote_needle` are specified, the
selected. If both `branch_pattern` and `remote_pattern` are specified, the
selection is further restricted to just the remotes whose name contains
`remote_needle`. For example, `remote_branches(push, ri)` would match the
`remote_pattern`. For example, `remote_branches(push, ri)` would match the
branches `push-123@origin` and `repushed@private` but not `push-123@upstream`
or `main@origin` or `main@upstream`. If a branch is in a conflicted state,
all its possible targets are included.
@ -118,13 +118,13 @@ revsets (expressions) as arguments.
* `latest(x[, count])`: Latest `count` commits in `x`, based on committer
timestamp. The default `count` is 1.
* `merges()`: Merge commits.
* `description(needle)`: Commits with the given string in their
* `description(pattern)`: Commits with the given string in their
description.
* `author(needle)`: Commits with the given string in the author's name or
* `author(pattern)`: Commits with the given string in the author's name or
email.
* `mine()`: Commits where the author's email matches the email of the current
user.
* `committer(needle)`: Commits with the given string in the committer's
* `committer(pattern)`: Commits with the given string in the committer's
name or email.
* `empty()`: Commits modifying no files. This also includes `merges()` without
user modifications and `root`.

View file

@ -831,33 +831,29 @@ fn build_predicate_fn<'index>(
let parent_count_range = parent_count_range.clone();
pure_predicate_fn(move |entry| parent_count_range.contains(&entry.num_parents()))
}
RevsetFilterPredicate::Description(needle) => {
let needle = needle.clone();
RevsetFilterPredicate::Description(pattern) => {
let pattern = pattern.clone();
pure_predicate_fn(move |entry| {
store
.get_commit(&entry.commit_id())
.unwrap()
.description()
.contains(needle.as_str())
let commit = store.get_commit(&entry.commit_id()).unwrap();
pattern.matches(commit.description())
})
}
RevsetFilterPredicate::Author(needle) => {
let needle = needle.clone();
RevsetFilterPredicate::Author(pattern) => {
let pattern = pattern.clone();
// TODO: Make these functions that take a needle to search for accept some
// syntax for specifying whether it's a regex and whether it's
// case-sensitive.
pure_predicate_fn(move |entry| {
let commit = store.get_commit(&entry.commit_id()).unwrap();
commit.author().name.contains(needle.as_str())
|| commit.author().email.contains(needle.as_str())
pattern.matches(&commit.author().name) || pattern.matches(&commit.author().email)
})
}
RevsetFilterPredicate::Committer(needle) => {
let needle = needle.clone();
RevsetFilterPredicate::Committer(pattern) => {
let pattern = pattern.clone();
pure_predicate_fn(move |entry| {
let commit = store.get_commit(&entry.commit_id()).unwrap();
commit.committer().name.contains(needle.as_str())
|| commit.committer().email.contains(needle.as_str())
pattern.matches(&commit.committer().name)
|| pattern.matches(&commit.committer().email)
})
}
RevsetFilterPredicate::File(paths) => {

View file

@ -208,15 +208,37 @@ impl error::Error for RevsetParseError {
pub const GENERATION_RANGE_FULL: Range<u64> = 0..u64::MAX;
pub const GENERATION_RANGE_EMPTY: Range<u64> = 0..0;
/// Pattern to be tested against string property like commit description or
/// branch name.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum StringPattern {
/// Matches strings that contain `substring`.
Substring(String),
}
impl StringPattern {
/// Pattern that matches any string.
pub fn everything() -> Self {
StringPattern::Substring(String::new())
}
/// Returns true if this pattern matches the `haystack`.
pub fn matches(&self, haystack: &str) -> bool {
match self {
StringPattern::Substring(needle) => haystack.contains(needle),
}
}
}
/// Symbol or function to be resolved to `CommitId`s.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RevsetCommitRef {
Symbol(String),
VisibleHeads,
Branches(String),
Branches(StringPattern),
RemoteBranches {
branch_needle: String,
remote_needle: String,
branch_pattern: StringPattern,
remote_pattern: StringPattern,
},
Tags,
GitRefs,
@ -228,11 +250,11 @@ pub enum RevsetFilterPredicate {
/// Commits with number of parents in the range.
ParentCount(Range<u32>),
/// Commits with description containing the needle.
Description(String),
Description(StringPattern),
/// Commits with author's name or email containing the needle.
Author(String),
Author(StringPattern),
/// Commits with committer's name or email containing the needle.
Committer(String),
Committer(StringPattern),
/// Commits modifying the paths specified by the pattern.
File(Option<Vec<RepoPath>>), // TODO: embed matcher expression?
/// Commits with conflicts
@ -309,17 +331,20 @@ impl RevsetExpression {
Rc::new(RevsetExpression::CommitRef(RevsetCommitRef::VisibleHeads))
}
pub fn branches(needle: String) -> Rc<RevsetExpression> {
pub fn branches(pattern: StringPattern) -> Rc<RevsetExpression> {
Rc::new(RevsetExpression::CommitRef(RevsetCommitRef::Branches(
needle,
pattern,
)))
}
pub fn remote_branches(branch_needle: String, remote_needle: String) -> Rc<RevsetExpression> {
pub fn remote_branches(
branch_pattern: StringPattern,
remote_pattern: StringPattern,
) -> Rc<RevsetExpression> {
Rc::new(RevsetExpression::CommitRef(
RevsetCommitRef::RemoteBranches {
branch_needle,
remote_needle,
branch_pattern,
remote_pattern,
},
))
}
@ -986,29 +1011,29 @@ static BUILTIN_FUNCTION_MAP: Lazy<HashMap<&'static str, RevsetFunction>> = Lazy:
});
map.insert("branches", |name, arguments_pair, state| {
let ([], [opt_arg]) = expect_arguments(name, arguments_pair)?;
let needle = if let Some(arg) = opt_arg {
parse_function_argument_to_string(name, arg, state)?
let pattern = if let Some(arg) = opt_arg {
parse_function_argument_to_string_pattern(name, arg, state)?
} else {
"".to_owned()
StringPattern::everything()
};
Ok(RevsetExpression::branches(needle))
Ok(RevsetExpression::branches(pattern))
});
map.insert("remote_branches", |name, arguments_pair, state| {
let ([], [branch_opt_arg, remote_opt_arg]) =
expect_named_arguments(name, &["", "remote"], arguments_pair)?;
let branch_needle = if let Some(branch_arg) = branch_opt_arg {
parse_function_argument_to_string(name, branch_arg, state)?
let branch_pattern = if let Some(branch_arg) = branch_opt_arg {
parse_function_argument_to_string_pattern(name, branch_arg, state)?
} else {
"".to_owned()
StringPattern::everything()
};
let remote_needle = if let Some(remote_arg) = remote_opt_arg {
parse_function_argument_to_string(name, remote_arg, state)?
let remote_pattern = if let Some(remote_arg) = remote_opt_arg {
parse_function_argument_to_string_pattern(name, remote_arg, state)?
} else {
"".to_owned()
StringPattern::everything()
};
Ok(RevsetExpression::remote_branches(
branch_needle,
remote_needle,
branch_pattern,
remote_pattern,
))
});
map.insert("tags", |name, arguments_pair, _state| {
@ -1041,29 +1066,30 @@ static BUILTIN_FUNCTION_MAP: Lazy<HashMap<&'static str, RevsetFunction>> = Lazy:
});
map.insert("description", |name, arguments_pair, state| {
let arg = expect_one_argument(name, arguments_pair)?;
let needle = parse_function_argument_to_string(name, arg, state)?;
let pattern = parse_function_argument_to_string_pattern(name, arg, state)?;
Ok(RevsetExpression::filter(
RevsetFilterPredicate::Description(needle),
RevsetFilterPredicate::Description(pattern),
))
});
map.insert("author", |name, arguments_pair, state| {
let arg = expect_one_argument(name, arguments_pair)?;
let needle = parse_function_argument_to_string(name, arg, state)?;
let pattern = parse_function_argument_to_string_pattern(name, arg, state)?;
Ok(RevsetExpression::filter(RevsetFilterPredicate::Author(
needle,
pattern,
)))
});
map.insert("mine", |name, arguments_pair, state| {
expect_no_arguments(name, arguments_pair)?;
Ok(RevsetExpression::filter(RevsetFilterPredicate::Author(
state.user_email.to_owned(),
// TODO: use exact match
StringPattern::Substring(state.user_email.to_owned()),
)))
});
map.insert("committer", |name, arguments_pair, state| {
let arg = expect_one_argument(name, arguments_pair)?;
let needle = parse_function_argument_to_string(name, arg, state)?;
let pattern = parse_function_argument_to_string_pattern(name, arg, state)?;
Ok(RevsetExpression::filter(RevsetFilterPredicate::Committer(
needle,
pattern,
)))
});
map.insert("empty", |name, arguments_pair, _state| {
@ -1249,6 +1275,15 @@ fn parse_function_argument_to_string(
parse_function_argument_as_literal("string", name, pair, state)
}
fn parse_function_argument_to_string_pattern(
name: &str,
pair: Pair<Rule>,
state: ParseState,
) -> Result<StringPattern, RevsetParseError> {
let needle = parse_function_argument_as_literal("string", name, pair, state)?;
Ok(StringPattern::Substring(needle))
}
fn parse_function_argument_as_literal<T: FromStr>(
type_name: &str,
name: &str,
@ -1941,10 +1976,10 @@ fn resolve_commit_ref(
match commit_ref {
RevsetCommitRef::Symbol(symbol) => symbol_resolver.resolve_symbol(symbol),
RevsetCommitRef::VisibleHeads => Ok(repo.view().heads().iter().cloned().collect_vec()),
RevsetCommitRef::Branches(needle) => {
RevsetCommitRef::Branches(pattern) => {
let mut commit_ids = vec![];
for (branch_name, branch_target) in repo.view().branches() {
if !branch_name.contains(needle) {
if !pattern.matches(branch_name) {
continue;
}
commit_ids.extend(branch_target.local_target.added_ids().cloned());
@ -1952,16 +1987,16 @@ fn resolve_commit_ref(
Ok(commit_ids)
}
RevsetCommitRef::RemoteBranches {
branch_needle,
remote_needle,
branch_pattern,
remote_pattern,
} => {
let mut commit_ids = vec![];
for (branch_name, branch_target) in repo.view().branches() {
if !branch_name.contains(branch_needle) {
if !branch_pattern.matches(branch_name) {
continue;
}
for (remote_name, remote_target) in branch_target.remote_targets.iter() {
if remote_name.contains(remote_needle) {
if remote_pattern.matches(remote_name) {
commit_ids.extend(remote_target.added_ids().cloned());
}
}
@ -2544,16 +2579,16 @@ mod tests {
// Space is allowed around infix operators and function arguments
assert_eq!(
parse(" description( arg1 ) ~ file( arg1 , arg2 ) ~ visible_heads( ) "),
Ok(
RevsetExpression::filter(RevsetFilterPredicate::Description("arg1".to_string()))
Ok(RevsetExpression::filter(RevsetFilterPredicate::Description(
StringPattern::Substring("arg1".to_string())
))
.minus(&RevsetExpression::filter(RevsetFilterPredicate::File(
Some(vec![
RepoPath::from_internal_string("arg1"),
RepoPath::from_internal_string("arg2"),
])
)))
.minus(&RevsetExpression::visible_heads())
)
.minus(&RevsetExpression::visible_heads()))
);
// Space is allowed around keyword arguments
assert_eq!(
@ -2695,13 +2730,13 @@ mod tests {
assert_eq!(
parse(r#"description("")"#),
Ok(RevsetExpression::filter(
RevsetFilterPredicate::Description("".to_string())
RevsetFilterPredicate::Description(StringPattern::Substring("".to_string()))
))
);
assert_eq!(
parse("description(foo)"),
Ok(RevsetExpression::filter(
RevsetFilterPredicate::Description("foo".to_string())
RevsetFilterPredicate::Description(StringPattern::Substring("foo".to_string()))
))
);
assert_eq!(
@ -2714,20 +2749,20 @@ mod tests {
assert_eq!(
parse("description((foo))"),
Ok(RevsetExpression::filter(
RevsetFilterPredicate::Description("foo".to_string())
RevsetFilterPredicate::Description(StringPattern::Substring("foo".to_string()))
))
);
assert_eq!(
parse("description(\"(foo)\")"),
Ok(RevsetExpression::filter(
RevsetFilterPredicate::Description("(foo)".to_string())
RevsetFilterPredicate::Description(StringPattern::Substring("(foo)".to_string()))
))
);
assert!(parse("mine(foo)").is_err());
assert_eq!(
parse("mine()"),
Ok(RevsetExpression::filter(RevsetFilterPredicate::Author(
"test.user@example.com".to_string()
StringPattern::Substring("test.user@example.com".to_string())
)))
);
assert_eq!(
@ -2960,42 +2995,44 @@ mod tests {
assert_eq!(
optimize(parse("parents(branches() & all())").unwrap()),
RevsetExpression::branches("".to_owned()).parents()
RevsetExpression::branches(StringPattern::everything()).parents()
);
assert_eq!(
optimize(parse("children(branches() & all())").unwrap()),
RevsetExpression::branches("".to_owned()).children()
RevsetExpression::branches(StringPattern::everything()).children()
);
assert_eq!(
optimize(parse("ancestors(branches() & all())").unwrap()),
RevsetExpression::branches("".to_owned()).ancestors()
RevsetExpression::branches(StringPattern::everything()).ancestors()
);
assert_eq!(
optimize(parse("descendants(branches() & all())").unwrap()),
RevsetExpression::branches("".to_owned()).descendants()
RevsetExpression::branches(StringPattern::everything()).descendants()
);
assert_eq!(
optimize(parse("(branches() & all())..(all() & tags())").unwrap()),
RevsetExpression::branches("".to_owned()).range(&RevsetExpression::tags())
RevsetExpression::branches(StringPattern::everything())
.range(&RevsetExpression::tags())
);
assert_eq!(
optimize(parse("(branches() & all()):(all() & tags())").unwrap()),
RevsetExpression::branches("".to_owned()).dag_range_to(&RevsetExpression::tags())
RevsetExpression::branches(StringPattern::everything())
.dag_range_to(&RevsetExpression::tags())
);
assert_eq!(
optimize(parse("heads(branches() & all())").unwrap()),
RevsetExpression::branches("".to_owned()).heads()
RevsetExpression::branches(StringPattern::everything()).heads()
);
assert_eq!(
optimize(parse("roots(branches() & all())").unwrap()),
RevsetExpression::branches("".to_owned()).roots()
RevsetExpression::branches(StringPattern::everything()).roots()
);
assert_eq!(
optimize(parse("latest(branches() & all(), 2)").unwrap()),
RevsetExpression::branches("".to_owned()).latest(2)
RevsetExpression::branches(StringPattern::everything()).latest(2)
);
assert_eq!(
@ -3008,25 +3045,28 @@ mod tests {
assert_eq!(
optimize(parse("present(branches() & all())").unwrap()),
Rc::new(RevsetExpression::Present(RevsetExpression::branches(
"".to_owned()
StringPattern::everything()
)))
);
assert_eq!(
optimize(parse("~branches() & all()").unwrap()),
RevsetExpression::branches("".to_owned()).negated()
RevsetExpression::branches(StringPattern::everything()).negated()
);
assert_eq!(
optimize(parse("(branches() & all()) | (all() & tags())").unwrap()),
RevsetExpression::branches("".to_owned()).union(&RevsetExpression::tags())
RevsetExpression::branches(StringPattern::everything())
.union(&RevsetExpression::tags())
);
assert_eq!(
optimize(parse("(branches() & all()) & (all() & tags())").unwrap()),
RevsetExpression::branches("".to_owned()).intersection(&RevsetExpression::tags())
RevsetExpression::branches(StringPattern::everything())
.intersection(&RevsetExpression::tags())
);
assert_eq!(
optimize(parse("(branches() & all()) ~ (all() & tags())").unwrap()),
RevsetExpression::branches("".to_owned()).minus(&RevsetExpression::tags())
RevsetExpression::branches(StringPattern::everything())
.minus(&RevsetExpression::tags())
);
}
@ -3059,7 +3099,7 @@ mod tests {
let optimized = optimize(parsed.clone());
assert_eq!(
unwrap_union(&optimized).0.as_ref(),
&RevsetExpression::CommitRef(RevsetCommitRef::Branches("".to_owned()))
&RevsetExpression::CommitRef(RevsetCommitRef::Branches(StringPattern::everything()))
);
assert!(Rc::ptr_eq(
unwrap_union(&parsed).1,
@ -3336,9 +3376,11 @@ mod tests {
),
Filter(
Author(
Substring(
"foo",
),
),
),
)
"###);
@ -3355,9 +3397,11 @@ mod tests {
),
Filter(
Author(
Substring(
"bar",
),
),
),
)
"###);
insta::assert_debug_snapshot!(
@ -3374,9 +3418,11 @@ mod tests {
Union(
Filter(
Author(
Substring(
"bar",
),
),
),
CommitRef(
Symbol(
"baz",
@ -3400,9 +3446,11 @@ mod tests {
),
Filter(
Author(
Substring(
"foo",
),
),
),
)
"###);
}
@ -3412,8 +3460,10 @@ mod tests {
insta::assert_debug_snapshot!(optimize(parse("author(foo)").unwrap()), @r###"
Filter(
Author(
Substring(
"foo",
),
),
)
"###);
@ -3426,9 +3476,11 @@ mod tests {
),
Filter(
Description(
Substring(
"bar",
),
),
),
)
"###);
insta::assert_debug_snapshot!(optimize(parse("author(foo) & bar").unwrap()), @r###"
@ -3440,9 +3492,11 @@ mod tests {
),
Filter(
Author(
Substring(
"foo",
),
),
),
)
"###);
insta::assert_debug_snapshot!(
@ -3450,14 +3504,18 @@ mod tests {
Intersection(
Filter(
Author(
Substring(
"foo",
),
),
),
Filter(
Committer(
Substring(
"bar",
),
),
),
)
"###);
@ -3472,15 +3530,19 @@ mod tests {
),
Filter(
Description(
Substring(
"bar",
),
),
),
),
Filter(
Author(
Substring(
"baz",
),
),
),
)
"###);
insta::assert_debug_snapshot!(
@ -3494,15 +3556,19 @@ mod tests {
),
Filter(
Committer(
Substring(
"foo",
),
),
),
),
Filter(
Author(
Substring(
"baz",
),
),
),
)
"###);
insta::assert_debug_snapshot!(
@ -3516,10 +3582,12 @@ mod tests {
),
Filter(
Committer(
Substring(
"foo",
),
),
),
),
Filter(
File(
Some(
@ -3537,9 +3605,11 @@ mod tests {
Intersection(
Filter(
Committer(
Substring(
"foo",
),
),
),
Filter(
File(
Some(
@ -3552,9 +3622,11 @@ mod tests {
),
Filter(
Author(
Substring(
"baz",
),
),
),
)
"###);
insta::assert_debug_snapshot!(optimize(parse("foo & file(bar) & baz").unwrap()), @r###"
@ -3601,15 +3673,19 @@ mod tests {
),
Filter(
Description(
Substring(
"bar",
),
),
),
),
Filter(
Author(
Substring(
"baz",
),
),
),
)
"###);
insta::assert_debug_snapshot!(
@ -3625,9 +3701,11 @@ mod tests {
Ancestors {
heads: Filter(
Author(
Substring(
"baz",
),
),
),
generation: 1..2,
is_legacy: false,
},
@ -3640,9 +3718,11 @@ mod tests {
),
Filter(
Description(
Substring(
"bar",
),
),
),
)
"###);
insta::assert_debug_snapshot!(
@ -3663,19 +3743,23 @@ mod tests {
),
Filter(
Author(
Substring(
"baz",
),
),
),
),
generation: 1..2,
is_legacy: false,
},
),
Filter(
Description(
Substring(
"bar",
),
),
),
)
"###);
@ -3706,21 +3790,27 @@ mod tests {
),
Filter(
Author(
Substring(
"A",
),
),
),
),
Filter(
Author(
Substring(
"B",
),
),
),
),
Filter(
Author(
Substring(
"C",
),
),
),
)
"###);
insta::assert_debug_snapshot!(
@ -3757,21 +3847,27 @@ mod tests {
),
Filter(
Author(
Substring(
"A",
),
),
),
),
Filter(
Author(
Substring(
"B",
),
),
),
),
Filter(
Author(
Substring(
"C",
),
),
),
)
"###);
@ -3788,15 +3884,19 @@ mod tests {
),
Filter(
Description(
Substring(
"bar",
),
),
),
),
Filter(
Author(
Substring(
"baz",
),
),
),
)
"###);
}
@ -3815,9 +3915,11 @@ mod tests {
Union(
Filter(
Author(
Substring(
"foo",
),
),
),
CommitRef(
Symbol(
"bar",
@ -3846,17 +3948,21 @@ mod tests {
),
Filter(
Committer(
Substring(
"bar",
),
),
),
),
),
),
Filter(
Description(
Substring(
"baz",
),
),
),
)
"###);
@ -3882,6 +3988,7 @@ mod tests {
),
Filter(
Author(
Substring(
"foo",
),
),
@ -3890,6 +3997,7 @@ mod tests {
),
),
),
),
CommitRef(
Symbol(
"baz",
@ -3931,9 +4039,11 @@ mod tests {
Union(
Filter(
Author(
Substring(
"A",
),
),
),
CommitRef(
Symbol(
"0",
@ -3946,9 +4056,11 @@ mod tests {
Union(
Filter(
Author(
Substring(
"B",
),
),
),
CommitRef(
Symbol(
"1",
@ -3961,9 +4073,11 @@ mod tests {
Union(
Filter(
Author(
Substring(
"C",
),
),
),
CommitRef(
Symbol(
"2",