revset: parameterize default string pattern kind, add config knob

Glob patterns will be enabled by default globally. Since this will be a big
breaking change in revsets, this patch adds a config knob to turn the new
default on/off.
This commit is contained in:
Yuya Nishihara
2025-11-05 14:53:14 +09:00
parent 3b37ed102e
commit c93682f218
9 changed files with 116 additions and 39 deletions

View File

@@ -18,6 +18,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
are in revsets. and can be combined with logical operators: `jj bookmark
list`, `jj tag list`
* The default string pattern syntax in revsets will be changed to `glob:` in a
future release. You can opt in to the new default by setting
`ui.revsets-use-glob-by-default=true`.
* Upgraded `scm-record` from v0.8.0 to v0.9.0. See release notes at
<https://github.com/arxanas/scm-record/releases/tag/v0.9.0>.

View File

@@ -786,6 +786,7 @@ pub struct WorkspaceCommandEnvironment {
revset_aliases_map: RevsetAliasesMap,
template_aliases_map: TemplateAliasesMap,
default_ignored_remote: Option<&'static RemoteName>,
revsets_use_glob_by_default: bool,
path_converter: RepoPathUiConverter,
workspace_name: WorkspaceNameBuf,
immutable_heads_expression: Arc<UserRevsetExpression>,
@@ -810,6 +811,7 @@ impl WorkspaceCommandEnvironment {
revset_aliases_map,
template_aliases_map,
default_ignored_remote,
revsets_use_glob_by_default: settings.get("ui.revsets-use-glob-by-default")?,
path_converter,
workspace_name: workspace.workspace_name().to_owned(),
immutable_heads_expression: RevsetExpression::root(),
@@ -847,6 +849,7 @@ impl WorkspaceCommandEnvironment {
user_email: self.settings.user_email(),
date_pattern_context: now.into(),
default_ignored_remote: self.default_ignored_remote,
use_glob_by_default: self.revsets_use_glob_by_default,
extensions: self.command.revset_extensions(),
workspace: Some(workspace_context),
}

View File

@@ -2789,6 +2789,7 @@ mod tests {
user_email: "test.user@example.com",
date_pattern_context: chrono::DateTime::UNIX_EPOCH.fixed_offset().into(),
default_ignored_remote: None,
use_glob_by_default: false,
extensions: &self.revset_extensions,
workspace: Some(RevsetWorkspaceContext {
path_converter: &self.path_converter,

View File

@@ -237,6 +237,11 @@
"conflict-marker-style": {
"$ref": "#/properties/ui/definitions/conflict-marker-style"
},
"revsets-use-glob-by-default": {
"type": "boolean",
"default": false,
"description": "Whether to use glob string patterns in revsets by default"
},
"show-cryptographic-signatures": {
"type": "boolean",
"default": false,

View File

@@ -39,6 +39,9 @@ conflict-marker-style = "diff"
# signature verification is slow, disable by default
show-cryptographic-signatures = false
bookmark-list-sort-keys = ["name"]
# TODO: enable glob by default in jj 0.37+ or so, and deprecate this option
# https://github.com/jj-vcs/jj/issues/6971#issuecomment-3067038313
revsets-use-glob-by-default = false
[ui.movement]
edit = false

View File

@@ -511,6 +511,44 @@ fn test_bad_symbol_or_argument_should_not_be_optimized_out() {
"#);
}
#[test]
fn test_default_string_pattern() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
// substring match by default as of jj 0.35
let output = work_dir.run_jj(["log", "-rauthor('test.user')"]);
insta::assert_snapshot!(output.normalize_backslash(), @r"
@ qpvuntsm test.user@example.com 2001-02-03 08:05:07 e8849ae1
│ (empty) (no description set)
~
[EOF]
------- stderr -------
Warning: In revset expression
--> 1:8
|
1 | author('test.user')
| ^---------^
|
= Default pattern syntax will be changed to `glob:` in a future release; use `substring:` prefix or set ui.revsets-use-glob-by-default=true to suppress this warning
[EOF]
");
// with default flipped
let output = work_dir.run_jj([
"log",
"-rauthor('*test.user*')",
"--config=ui.revsets-use-glob-by-default=true",
]);
insta::assert_snapshot!(output.normalize_backslash(), @r"
@ qpvuntsm test.user@example.com 2001-02-03 08:05:07 e8849ae1
│ (empty) (no description set)
~
[EOF]
");
}
#[test]
fn test_alias() {
let test_env = TestEnvironment::default();

View File

@@ -476,14 +476,18 @@ revsets (expressions) as arguments.
## String patterns
Functions that perform string matching support the following pattern syntax (the
quotes are optional):
quotes are optional).
By default, `"string"` is parsed as a `substring:` pattern in revsets. The
default will be changed to `glob:` in a future release. The new behavior can be
enabled by: `ui.revsets-use-glob-by-default=true`.
* `"string"` or `substring:"string"`: Matches strings that contain `string`.
* `exact:"string"`: Matches strings exactly equal to `string`.
* `glob:"pattern"`: Matches strings with Unix-style shell [wildcard
`pattern`](https://docs.rs/globset/latest/globset/#syntax).
* `regex:"pattern"`: Matches substrings with [regular
expression `pattern`](https://docs.rs/regex/latest/regex/#syntax).
* `substring:"string"`: Matches strings that contain `string`.
You can append `-i` after the kind to match caseinsensitively (e.g.
`glob-i:"fix*jpeg*"`).

View File

@@ -889,22 +889,17 @@ static BUILTIN_FUNCTION_MAP: LazyLock<HashMap<&str, RevsetFunction>> = LazyLock:
})?;
Ok(RevsetExpression::commit_id_prefix(prefix))
});
map.insert("bookmarks", |diagnostics, function, _context| {
map.insert("bookmarks", |diagnostics, function, context| {
let ([], [opt_arg]) = function.expect_arguments()?;
let expr = if let Some(arg) = opt_arg {
expect_string_expression(diagnostics, arg)?
expect_string_expression(diagnostics, arg, context)?
} else {
StringExpression::all()
};
Ok(RevsetExpression::bookmarks(expr))
});
map.insert("remote_bookmarks", |diagnostics, function, context| {
parse_remote_bookmarks_arguments(
diagnostics,
function,
None,
context.default_ignored_remote,
)
parse_remote_bookmarks_arguments(diagnostics, function, None, context)
});
map.insert(
"tracked_remote_bookmarks",
@@ -913,7 +908,7 @@ static BUILTIN_FUNCTION_MAP: LazyLock<HashMap<&str, RevsetFunction>> = LazyLock:
diagnostics,
function,
Some(RemoteRefState::Tracked),
context.default_ignored_remote,
context,
)
},
);
@@ -924,14 +919,14 @@ static BUILTIN_FUNCTION_MAP: LazyLock<HashMap<&str, RevsetFunction>> = LazyLock:
diagnostics,
function,
Some(RemoteRefState::New),
context.default_ignored_remote,
context,
)
},
);
map.insert("tags", |diagnostics, function, _context| {
map.insert("tags", |diagnostics, function, context| {
let ([], [opt_arg]) = function.expect_arguments()?;
let expr = if let Some(arg) = opt_arg {
expect_string_expression(diagnostics, arg)?
expect_string_expression(diagnostics, arg, context)?
} else {
StringExpression::all()
};
@@ -977,35 +972,35 @@ static BUILTIN_FUNCTION_MAP: LazyLock<HashMap<&str, RevsetFunction>> = LazyLock:
RevsetFilterPredicate::ParentCount(2..u32::MAX),
))
});
map.insert("description", |diagnostics, function, _context| {
map.insert("description", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let expr = expect_string_expression(diagnostics, arg)?;
let expr = expect_string_expression(diagnostics, arg, context)?;
let predicate = RevsetFilterPredicate::Description(expr);
Ok(RevsetExpression::filter(predicate))
});
map.insert("subject", |diagnostics, function, _context| {
map.insert("subject", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let expr = expect_string_expression(diagnostics, arg)?;
let expr = expect_string_expression(diagnostics, arg, context)?;
let predicate = RevsetFilterPredicate::Subject(expr);
Ok(RevsetExpression::filter(predicate))
});
map.insert("author", |diagnostics, function, _context| {
map.insert("author", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let expr = expect_string_expression(diagnostics, arg)?;
let expr = expect_string_expression(diagnostics, arg, context)?;
let name_predicate = RevsetFilterPredicate::AuthorName(expr.clone());
let email_predicate = RevsetFilterPredicate::AuthorEmail(expr);
Ok(RevsetExpression::filter(name_predicate)
.union(&RevsetExpression::filter(email_predicate)))
});
map.insert("author_name", |diagnostics, function, _context| {
map.insert("author_name", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let expr = expect_string_expression(diagnostics, arg)?;
let expr = expect_string_expression(diagnostics, arg, context)?;
let predicate = RevsetFilterPredicate::AuthorName(expr);
Ok(RevsetExpression::filter(predicate))
});
map.insert("author_email", |diagnostics, function, _context| {
map.insert("author_email", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let expr = expect_string_expression(diagnostics, arg)?;
let expr = expect_string_expression(diagnostics, arg, context)?;
let predicate = RevsetFilterPredicate::AuthorEmail(expr);
Ok(RevsetExpression::filter(predicate))
});
@@ -1030,23 +1025,23 @@ static BUILTIN_FUNCTION_MAP: LazyLock<HashMap<&str, RevsetFunction>> = LazyLock:
let predicate = RevsetFilterPredicate::AuthorEmail(StringExpression::pattern(pattern));
Ok(RevsetExpression::filter(predicate))
});
map.insert("committer", |diagnostics, function, _context| {
map.insert("committer", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let expr = expect_string_expression(diagnostics, arg)?;
let expr = expect_string_expression(diagnostics, arg, context)?;
let name_predicate = RevsetFilterPredicate::CommitterName(expr.clone());
let email_predicate = RevsetFilterPredicate::CommitterEmail(expr);
Ok(RevsetExpression::filter(name_predicate)
.union(&RevsetExpression::filter(email_predicate)))
});
map.insert("committer_name", |diagnostics, function, _context| {
map.insert("committer_name", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let expr = expect_string_expression(diagnostics, arg)?;
let expr = expect_string_expression(diagnostics, arg, context)?;
let predicate = RevsetFilterPredicate::CommitterName(expr);
Ok(RevsetExpression::filter(predicate))
});
map.insert("committer_email", |diagnostics, function, _context| {
map.insert("committer_email", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let expr = expect_string_expression(diagnostics, arg)?;
let expr = expect_string_expression(diagnostics, arg, context)?;
let predicate = RevsetFilterPredicate::CommitterEmail(expr);
Ok(RevsetExpression::filter(predicate))
});
@@ -1074,7 +1069,7 @@ static BUILTIN_FUNCTION_MAP: LazyLock<HashMap<&str, RevsetFunction>> = LazyLock:
});
map.insert("diff_contains", |diagnostics, function, context| {
let ([text_arg], [files_opt_arg]) = function.expect_arguments()?;
let text = expect_string_expression(diagnostics, text_arg)?;
let text = expect_string_expression(diagnostics, text_arg, context)?;
let files = if let Some(files_arg) = files_opt_arg {
let ctx = context.workspace.as_ref().ok_or_else(|| {
RevsetParseError::with_span(
@@ -1150,8 +1145,13 @@ pub fn expect_fileset_expression(
pub fn expect_string_expression(
diagnostics: &mut RevsetDiagnostics,
node: &ExpressionNode,
context: &LoweringContext,
) -> Result<StringExpression, RevsetParseError> {
let default_kind = "substring";
let default_kind = if context.use_glob_by_default {
"glob"
} else {
"substring"
};
expect_string_expression_inner(diagnostics, node, default_kind)
}
@@ -1164,14 +1164,22 @@ fn expect_string_expression_inner(
revset_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
let expr_error = || RevsetParseError::expression("Invalid string expression", node.span);
let pattern_error = || RevsetParseError::expression("Invalid string pattern", node.span);
let default_pattern = |value: &str| {
let default_pattern = |diagnostics: &mut RevsetDiagnostics, value: &str| {
if default_kind == "substring" {
diagnostics.add_warning(RevsetParseError::expression(
"Default pattern syntax will be changed to `glob:` in a future release; use \
`substring:` prefix or set ui.revsets-use-glob-by-default=true to suppress \
this warning",
node.span,
));
}
let pattern = StringPattern::from_str_kind(value, default_kind)
.map_err(|err| pattern_error().with_source(err))?;
Ok(StringExpression::pattern(pattern))
};
match &node.kind {
ExpressionKind::Identifier(value) => default_pattern(value),
ExpressionKind::String(value) => default_pattern(value),
ExpressionKind::Identifier(value) => default_pattern(diagnostics, value),
ExpressionKind::String(value) => default_pattern(diagnostics, value),
ExpressionKind::StringPattern { kind, value } => {
let pattern = StringPattern::from_str_kind(value, kind)
.map_err(|err| pattern_error().with_source(err))?;
@@ -1236,18 +1244,18 @@ fn parse_remote_bookmarks_arguments(
diagnostics: &mut RevsetDiagnostics,
function: &FunctionCallNode,
remote_ref_state: Option<RemoteRefState>,
default_ignored_remote: Option<&RemoteName>,
context: &LoweringContext,
) -> Result<Arc<UserRevsetExpression>, RevsetParseError> {
let ([], [bookmark_opt_arg, remote_opt_arg]) =
function.expect_named_arguments(&["", "remote"])?;
let bookmark_expr = if let Some(bookmark_arg) = bookmark_opt_arg {
expect_string_expression(diagnostics, bookmark_arg)?
expect_string_expression(diagnostics, bookmark_arg, context)?
} else {
StringExpression::all()
};
let remote_expr = if let Some(remote_arg) = remote_opt_arg {
expect_string_expression(diagnostics, remote_arg)?
} else if let Some(remote) = default_ignored_remote {
expect_string_expression(diagnostics, remote_arg, context)?
} else if let Some(remote) = context.default_ignored_remote {
StringExpression::exact(remote).negated()
} else {
StringExpression::all()
@@ -3426,6 +3434,7 @@ pub struct RevsetParseContext<'a> {
pub date_pattern_context: DatePatternContext,
/// Special remote that should be ignored by default. (e.g. "git")
pub default_ignored_remote: Option<&'a RemoteName>,
pub use_glob_by_default: bool,
pub extensions: &'a RevsetExtensions,
pub workspace: Option<RevsetWorkspaceContext<'a>>,
}
@@ -3438,6 +3447,7 @@ impl<'a> RevsetParseContext<'a> {
user_email,
date_pattern_context,
default_ignored_remote,
use_glob_by_default,
extensions,
workspace,
} = *self;
@@ -3445,6 +3455,7 @@ impl<'a> RevsetParseContext<'a> {
user_email,
date_pattern_context,
default_ignored_remote,
use_glob_by_default,
extensions,
workspace,
}
@@ -3457,6 +3468,7 @@ pub struct LoweringContext<'a> {
user_email: &'a str,
date_pattern_context: DatePatternContext,
default_ignored_remote: Option<&'a RemoteName>,
use_glob_by_default: bool,
extensions: &'a RevsetExtensions,
workspace: Option<RevsetWorkspaceContext<'a>>,
}
@@ -3544,6 +3556,7 @@ mod tests {
user_email: "test.user@example.com",
date_pattern_context: chrono::Utc::now().fixed_offset().into(),
default_ignored_remote: Some("ignored".as_ref()),
use_glob_by_default: false,
extensions: &RevsetExtensions::default(),
workspace: None,
};
@@ -3574,6 +3587,7 @@ mod tests {
user_email: "test.user@example.com",
date_pattern_context: chrono::Utc::now().fixed_offset().into(),
default_ignored_remote: Some("ignored".as_ref()),
use_glob_by_default: false,
extensions: &RevsetExtensions::default(),
workspace: Some(workspace_ctx),
};
@@ -3600,6 +3614,7 @@ mod tests {
user_email: "test.user@example.com",
date_pattern_context: chrono::Utc::now().fixed_offset().into(),
default_ignored_remote: Some("ignored".as_ref()),
use_glob_by_default: false,
extensions: &RevsetExtensions::default(),
workspace: None,
};

View File

@@ -97,6 +97,7 @@ fn resolve_symbol(repo: &dyn Repo, symbol: &str) -> Result<Vec<CommitId>, Revset
user_email: "",
date_pattern_context: chrono::Local::now().into(),
default_ignored_remote: Some(git::REMOTE_NAME_FOR_LOCAL_GIT_REPO),
use_glob_by_default: false,
extensions: &RevsetExtensions::default(),
workspace: None,
};
@@ -230,6 +231,7 @@ fn test_resolve_symbol_commit_id() {
user_email: settings.user_email(),
date_pattern_context: chrono::Utc::now().fixed_offset().into(),
default_ignored_remote: Some(git::REMOTE_NAME_FOR_LOCAL_GIT_REPO),
use_glob_by_default: false,
extensions: &RevsetExtensions::default(),
workspace: None,
};
@@ -1020,6 +1022,7 @@ fn try_resolve_expression(
user_email: settings.user_email(),
date_pattern_context: chrono::Utc::now().fixed_offset().into(),
default_ignored_remote: Some(git::REMOTE_NAME_FOR_LOCAL_GIT_REPO),
use_glob_by_default: false,
extensions: &RevsetExtensions::default(),
workspace: None,
};
@@ -1070,6 +1073,7 @@ fn resolve_commit_ids_in_workspace(
user_email: settings.user_email(),
date_pattern_context: chrono::Utc::now().fixed_offset().into(),
default_ignored_remote: Some(git::REMOTE_NAME_FOR_LOCAL_GIT_REPO),
use_glob_by_default: false,
extensions: &RevsetExtensions::default(),
workspace: Some(workspace_ctx),
};