From a81ebeb85eb4d6f53432441c19057d0434fdc31c Mon Sep 17 00:00:00 2001 From: Yuya Nishihara Date: Tue, 15 Nov 2022 17:12:37 +0900 Subject: [PATCH] revset: add empty() predicate to find commits with no file change The expression 'x ~ empty()' is identical to 'x & file(".")', but more intuitive. Note that 'x ~ empty()' is slower than 'x & file(".")' since the negative intersection isn't optimized right now. I think that can be handled as follows: 'x ~ filter(f)' -> 'x & filter(!f)' -> 'filter(!f, x)' --- CHANGELOG.md | 2 ++ docs/revsets.md | 2 ++ lib/src/revset.rs | 19 ++++++++++++++++++- lib/tests/test_revset.rs | 12 ++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0315d6c1d..4969a516d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * The new revset function `file(pattern..)` finds commits modifying the paths specified by the `pattern..`. +* The new revset function `empty()` finds commits modifying no files. + * It is now possible to specify configuration options on the command line with the new `--config-toml` global option. diff --git a/docs/revsets.md b/docs/revsets.md index 036f3a3a8..ebf350520 100644 --- a/docs/revsets.md +++ b/docs/revsets.md @@ -108,6 +108,8 @@ revsets (expressions) as arguments. email. * `committer(needle)`: 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`. * `file(pattern..)`: Commits modifying the paths specified by the `pattern..`. * `present(x)`: Same as `x`, but evaluated to `none()` if any of the commits in `x` doesn't exist (e.g. is an unknown branch name.) diff --git a/lib/src/revset.rs b/lib/src/revset.rs index bf5c2d5d6..3ea998ba8 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -31,7 +31,7 @@ use thiserror::Error; use crate::backend::{BackendError, BackendResult, CommitId}; use crate::commit::Commit; use crate::index::{HexPrefix, IndexEntry, IndexPosition, PrefixResolution, RevWalk}; -use crate::matchers::{Matcher, PrefixMatcher}; +use crate::matchers::{EverythingMatcher, Matcher, PrefixMatcher}; use crate::op_store::WorkspaceId; use crate::repo::RepoRef; use crate::repo_path::{FsPathParseError, RepoPath}; @@ -280,6 +280,8 @@ pub enum RevsetFilterPredicate { Author(String), /// Commits with committer's name or email containing the needle. Committer(String), + /// Commits modifying no files. Equivalent to `Not(File(["."]))`. + Empty, /// Commits modifying the paths specified by the pattern. File(Vec), } @@ -727,6 +729,10 @@ fn parse_function_expression( needle, ))) } + "empty" => { + expect_no_arguments(name, arguments_pair)?; + Ok(RevsetExpression::filter(RevsetFilterPredicate::Empty)) + } "file" => { if let Some(ctx) = workspace_ctx { let arguments_span = arguments_pair.as_span(); @@ -1589,6 +1595,12 @@ pub fn evaluate_expression<'repo>( }), })) } + RevsetFilterPredicate::Empty => Ok(Box::new(FilterRevset { + candidates, + predicate: Box::new(move |entry| { + !has_diff_from_parent(repo, entry, &EverythingMatcher) + }), + })), RevsetFilterPredicate::File(paths) => { // TODO: Add support for globs and other formats let matcher: Box = Box::new(PrefixMatcher::new(paths)); @@ -1911,6 +1923,11 @@ mod tests { RevsetFilterPredicate::Description("(foo)".to_string()) )) ); + assert_eq!( + parse("empty()"), + Ok(RevsetExpression::filter(RevsetFilterPredicate::Empty)) + ); + assert!(parse("empty(foo)").is_err()); assert!(parse("file()").is_err()); assert_eq!( parse("file(foo)"), diff --git a/lib/tests/test_revset.rs b/lib/tests/test_revset.rs index 429d29136..0e704cb03 100644 --- a/lib/tests/test_revset.rs +++ b/lib/tests/test_revset.rs @@ -1816,6 +1816,9 @@ fn test_filter_by_diff(use_git: bool) { let commit3 = CommitBuilder::for_new_commit(&settings, vec![commit2.id().clone()], tree3.id().clone()) .write_to_repo(mut_repo); + let commit4 = + CommitBuilder::for_new_commit(&settings, vec![commit3.id().clone()], tree3.id().clone()) + .write_to_repo(mut_repo); // matcher API: let resolve = |file_path: &RepoPath| -> Vec { @@ -1862,4 +1865,13 @@ fn test_filter_by_diff(use_git: bool) { ), vec![commit2.id().clone()] ); + + // empty() revset, which is identical to ~file(".") + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!("{}: & empty()", commit1.id().hex()) + ), + vec![commit4.id().clone()] + ); }