ok/jj
1
0
Fork 0
forked from mirrors/jj

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)'
This commit is contained in:
Yuya Nishihara 2022-11-15 17:12:37 +09:00
parent 230ac043ff
commit a81ebeb85e
4 changed files with 34 additions and 1 deletions

View file

@ -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.

View file

@ -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.)

View file

@ -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<RepoPath>),
}
@ -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<dyn Matcher> = 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)"),

View file

@ -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<CommitId> {
@ -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()]
);
}