revset: add subject() predicate that matches first line of descriptions

It's generally useful, and we can get by without introducing weird special case
about newline terminator.

Closes #5227
This commit is contained in:
Yuya Nishihara 2025-01-20 14:28:59 +09:00
parent 3d7858df26
commit a3636d8a83
5 changed files with 64 additions and 3 deletions

View file

@ -107,6 +107,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* New `author_name`/`author_email`/`committer_name`/`committer_email(pattern)`
revset functions to match either name or email field explicitly.
* New `subject(pattern)` revset function that matches first line of commit
descriptions.
### Fixed bugs
* Fixed diff selection by external tools with `jj split`/`commit -i FILESETS`.

View file

@ -267,6 +267,14 @@ revsets (expressions) as arguments.
* `description(pattern)`: Commits that have a description matching the given
[string pattern](#string-patterns).
A non-empty description is usually terminated with newline character. For
example, `description(exact:"")` matches commits without description, and
`description(exact:"foo\n")` matches commits with description `"foo\n"`.
* `subject(pattern)`: Commits that have a subject matching the given [string
pattern](#string-patterns). A subject is the first line of the description
(without newline character.)
* `author(pattern)`: Commits with the author's name or email matching the given
[string pattern](#string-patterns). Equivalent to `author_name(pattern) |
author_email(pattern)`.

View file

@ -1161,6 +1161,14 @@ fn build_predicate_fn(
Ok(pattern.matches(commit.description()))
})
}
RevsetFilterPredicate::Subject(pattern) => {
let pattern = pattern.clone();
box_pure_predicate_fn(move |index, pos| {
let entry = index.entry_by_pos(pos);
let commit = store.get_commit(&entry.commit_id())?;
Ok(pattern.matches(commit.description().lines().next().unwrap_or_default()))
})
}
RevsetFilterPredicate::AuthorName(pattern) => {
let pattern = pattern.clone();
box_pure_predicate_fn(move |index, pos| {

View file

@ -156,6 +156,8 @@ pub enum RevsetFilterPredicate {
ParentCount(Range<u32>),
/// Commits with description matching the pattern.
Description(StringPattern),
/// Commits with first line of the description matching the pattern.
Subject(StringPattern),
/// Commits with author name matching the pattern.
AuthorName(StringPattern),
/// Commits with author email matching the pattern.
@ -834,6 +836,12 @@ static BUILTIN_FUNCTION_MAP: Lazy<HashMap<&'static str, RevsetFunction>> = Lazy:
RevsetFilterPredicate::Description(pattern),
))
});
map.insert("subject", |diagnostics, function, _context| {
let [arg] = function.expect_exact_arguments()?;
let pattern = expect_string_pattern(diagnostics, arg)?;
let predicate = RevsetFilterPredicate::Subject(pattern);
Ok(RevsetExpression::filter(predicate))
});
map.insert("author", |diagnostics, function, _context| {
let [arg] = function.expect_exact_arguments()?;
let pattern = expect_string_pattern(diagnostics, arg)?;

View file

@ -2654,17 +2654,17 @@ fn test_evaluate_expression_description() {
let mut_repo = tx.repo_mut();
let commit1 = create_random_commit(mut_repo)
.set_description("commit 1")
.set_description("commit 1\n")
.write()
.unwrap();
let commit2 = create_random_commit(mut_repo)
.set_parents(vec![commit1.id().clone()])
.set_description("commit 2")
.set_description("commit 2\n\nblah blah...\n")
.write()
.unwrap();
let commit3 = create_random_commit(mut_repo)
.set_parents(vec![commit2.id().clone()])
.set_description("commit 3")
.set_description("commit 3\n")
.write()
.unwrap();
@ -2687,6 +2687,40 @@ fn test_evaluate_expression_description() {
resolve_commit_ids(mut_repo, "visible_heads() & description(\"commit 2\")"),
vec![]
);
// Exact match
assert_eq!(
resolve_commit_ids(mut_repo, r#"description(exact:"commit 1\n")"#),
vec![commit1.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo, r#"description(exact:"commit 2\n")"#),
vec![]
);
assert_eq!(
resolve_commit_ids(mut_repo, "description(exact:'')"),
vec![mut_repo.store().root_commit_id().clone()]
);
// Match subject line
assert_eq!(
resolve_commit_ids(mut_repo, "subject(glob:'commit ?')"),
vec![
commit3.id().clone(),
commit2.id().clone(),
commit1.id().clone(),
]
);
assert_eq!(resolve_commit_ids(mut_repo, "subject('blah')"), vec![]);
assert_eq!(
resolve_commit_ids(mut_repo, "subject(exact:'commit 2')"),
vec![commit2.id().clone()]
);
// Empty description should have empty subject line
assert_eq!(
resolve_commit_ids(mut_repo, "subject(exact:'')"),
vec![mut_repo.store().root_commit_id().clone()]
);
}
#[test]