diff --git a/cli/testing/bench-revsets-git.txt b/cli/testing/bench-revsets-git.txt index 412011fed..45d308f56 100644 --- a/cli/testing/bench-revsets-git.txt +++ b/cli/testing/bench-revsets-git.txt @@ -70,6 +70,6 @@ tags()+++:: merges() ~merges() # These are unbearably slow, so only filter within small set -file(Makefile) & v1.0.0..v1.2.0 +file(root:"Makefile") & v1.0.0..v1.2.0 empty() & v1.0.0..v1.2.0 conflict() & v1.0.0..v1.2.0 diff --git a/cli/tests/test_revset_output.rs b/cli/tests/test_revset_output.rs index 9fd915ccb..7bf42054d 100644 --- a/cli/tests/test_revset_output.rs +++ b/cli/tests/test_revset_output.rs @@ -152,15 +152,28 @@ fn test_bad_function_call() { = Function "file": Expected at least 1 argument "###); - let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "file(a, not:a-string)"]); + let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "file(a, not@a-string)"]); insta::assert_snapshot!(stderr, @r###" - Error: Failed to parse revset: Function "file": Expected function argument of type string + Error: Failed to parse revset: Function "file": Expected function argument of file pattern Caused by: --> 1:9 | - 1 | file(a, not:a-string) + 1 | file(a, not@a-string) | ^----------^ | - = Function "file": Expected function argument of type string + = Function "file": Expected function argument of file pattern + "###); + + let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", r#"file(foo:"bar")"#]); + insta::assert_snapshot!(stderr, @r###" + Error: Failed to parse revset: Function "file": Invalid file pattern + Caused by: + 1: --> 1:6 + | + 1 | file(foo:"bar") + | ^-------^ + | + = Function "file": Invalid file pattern + 2: Invalid file pattern kind "foo:" "###); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", r#"file(a, "../out")"#]); diff --git a/docs/revsets.md b/docs/revsets.md index 26319bc39..5370a6b0e 100644 --- a/docs/revsets.md +++ b/docs/revsets.md @@ -156,9 +156,8 @@ given [string pattern](#string-patterns). * `empty()`: Commits modifying no files. This also includes `merges()` without user modifications and `root()`. -* `file(relativepath)` or `file("relativepath"[, "relativepath"]...)`: Commits - modifying one of the paths specified. Currently, string patterns are *not* - supported in the path arguments. +* `file(pattern[, pattern]...)`: Commits modifying paths matching one of the + given [file patterns](filesets.md#file-patterns). Paths are relative to the directory `jj` was invoked from. A directory name will match all files in that directory and its subdirectories. diff --git a/lib/src/revset.rs b/lib/src/revset.rs index 6b8927852..582c16322 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -33,13 +33,12 @@ use thiserror::Error; use crate::backend::{BackendError, BackendResult, ChangeId, CommitId}; use crate::commit::Commit; -use crate::fileset::FilesetExpression; +use crate::fileset::{FilePattern, FilesetExpression, FilesetParseContext}; use crate::git; use crate::hex_util::to_forward_hex; use crate::object_id::{HexPrefix, PrefixResolution}; use crate::op_store::WorkspaceId; use crate::repo::Repo; -use crate::repo_path::RepoPathBuf; use crate::revset_graph::RevsetGraphEdge; use crate::store::Store; use crate::str_util::StringPattern; @@ -1339,18 +1338,14 @@ static BUILTIN_FUNCTION_MAP: Lazy> = Lazy: map.insert("file", |name, arguments_pair, state| { let arguments_span = arguments_pair.as_span(); if let Some(ctx) = state.workspace_ctx { + let ctx = FilesetParseContext { + cwd: ctx.cwd, + workspace_root: ctx.workspace_root, + }; let file_expressions: Vec<_> = arguments_pair .into_inner() - .map(|arg| -> Result<_, RevsetParseError> { - let span = arg.as_span(); - let needle = parse_function_argument_to_string(name, arg, state)?; - let path = RepoPathBuf::parse_fs_path(ctx.cwd, ctx.workspace_root, needle) - .map_err(|e| { - RevsetParseError::invalid_arguments(name, "Invalid file pattern", span) - .with_source(e) - })?; - Ok(FilesetExpression::prefix_path(path)) - }) + .map(|arg| parse_function_argument_to_file_pattern(name, arg, state, &ctx)) + .map_ok(FilesetExpression::pattern) .try_collect()?; if file_expressions.is_empty() { Err(RevsetParseError::invalid_arguments( @@ -1497,12 +1492,17 @@ fn expect_named_arguments_vec<'i>( Ok((required, optional)) } -fn parse_function_argument_to_string( +fn parse_function_argument_to_file_pattern( name: &str, pair: Pair, state: ParseState, -) -> Result { - parse_function_argument_as_literal("string", name, pair, state) + ctx: &FilesetParseContext, +) -> Result { + let parse_pattern = |value: &str, kind: Option<&str>| match kind { + Some(kind) => FilePattern::from_str_kind(ctx, value, kind), + None => FilePattern::cwd_prefix_path(ctx, value), + }; + parse_function_argument_as_pattern("file pattern", name, pair, state, parse_pattern) } fn parse_function_argument_to_string_pattern( @@ -2587,6 +2587,7 @@ mod tests { use assert_matches::assert_matches; use super::*; + use crate::repo_path::RepoPathBuf; fn parse(revset_str: &str) -> Result, RevsetParseErrorKind> { parse_with_aliases(revset_str, [] as [(&str, &str); 0]) @@ -3343,6 +3344,12 @@ mod tests { FilesetExpression::prefix_path(RepoPathBuf::from_internal_string("foo")) ))) ); + assert_eq!( + parse_with_workspace(r#"file(file:"foo")"#, &WorkspaceId::default()), + Ok(RevsetExpression::filter(RevsetFilterPredicate::File( + FilesetExpression::file_path(RepoPathBuf::from_internal_string("foo")) + ))) + ); assert_eq!( parse_with_workspace("file(foo, bar, baz)", &WorkspaceId::default()), Ok(RevsetExpression::filter(RevsetFilterPredicate::File(