diff --git a/CHANGELOG.md b/CHANGELOG.md index f4b10fd6c..30478b2ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/docs/revsets.md b/docs/revsets.md index 556b4bc19..810fefd5d 100644 --- a/docs/revsets.md +++ b/docs/revsets.md @@ -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)`. diff --git a/lib/src/default_index/revset_engine.rs b/lib/src/default_index/revset_engine.rs index 876e870ec..80c95ebde 100644 --- a/lib/src/default_index/revset_engine.rs +++ b/lib/src/default_index/revset_engine.rs @@ -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| { diff --git a/lib/src/revset.rs b/lib/src/revset.rs index 27250684a..04ff8f9c8 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -156,6 +156,8 @@ pub enum RevsetFilterPredicate { ParentCount(Range), /// 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> = 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)?; diff --git a/lib/tests/test_revset.rs b/lib/tests/test_revset.rs index a728338c3..7ae3c7ae1 100644 --- a/lib/tests/test_revset.rs +++ b/lib/tests/test_revset.rs @@ -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]