diff --git a/CHANGELOG.md b/CHANGELOG.md index abd0cf3649..91fe17d0d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `jj workspace forget` can now forget multiple workspaces at once. +* `branches()`/`remote_branches()`/`author()`/`committer()`/`description()` + revsets now support glob matching. + ### Fixed bugs * Updating the working copy to a commit where a file that's currently ignored diff --git a/Cargo.lock b/Cargo.lock index 9e3b11ba38..ee9f25edff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1051,6 +1051,7 @@ dependencies = [ "esl01-renderdag", "futures 0.3.28", "git2", + "glob", "hex", "insta", "itertools 0.11.0", diff --git a/cli/src/commands/branch.rs b/cli/src/commands/branch.rs index b1e5aeb3e7..b246344afb 100644 --- a/cli/src/commands/branch.rs +++ b/cli/src/commands/branch.rs @@ -9,7 +9,8 @@ use jj_lib::backend::{CommitId, ObjectId}; use jj_lib::git; use jj_lib::op_store::RefTarget; use jj_lib::repo::Repo; -use jj_lib::revset::{self, RevsetExpression, StringPattern}; +use jj_lib::revset::{self, RevsetExpression}; +use jj_lib::str_util::StringPattern; use jj_lib::view::View; use crate::cli_util::{user_error, user_error_with_hint, CommandError, CommandHelper, RevisionArg}; diff --git a/cli/src/commands/git.rs b/cli/src/commands/git.rs index 91b9c6f76b..2def802aee 100644 --- a/cli/src/commands/git.rs +++ b/cli/src/commands/git.rs @@ -20,9 +20,10 @@ use jj_lib::refs::{ }; use jj_lib::repo::Repo; use jj_lib::repo_path::RepoPath; -use jj_lib::revset::{self, RevsetExpression, RevsetIteratorExt as _, StringPattern}; +use jj_lib::revset::{self, RevsetExpression, RevsetIteratorExt as _}; use jj_lib::settings::{ConfigResultExt as _, UserSettings}; use jj_lib::store::Store; +use jj_lib::str_util::StringPattern; use jj_lib::workspace::Workspace; use maplit::hashset; diff --git a/docs/revsets.md b/docs/revsets.md index 26bf34d327..368ab1a6b4 100644 --- a/docs/revsets.md +++ b/docs/revsets.md @@ -145,6 +145,7 @@ Functions that perform string matching support the following pattern syntax. * `"string"`, `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`. ## Aliases diff --git a/lib/Cargo.toml b/lib/Cargo.toml index cce1d098f6..95d74bb23e 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -30,6 +30,7 @@ digest = { workspace = true } futures = { workspace = true } either = { workspace = true } git2 = { workspace = true } +glob = { workspace = true } hex = { workspace = true } itertools = { workspace = true } maplit = { workspace = true } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 303bbbb0f5..f0ac9314a4 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -62,6 +62,7 @@ pub mod simple_op_heads_store; pub mod simple_op_store; pub mod stacked_table; pub mod store; +pub mod str_util; pub mod submodule_store; pub mod transaction; pub mod tree; diff --git a/lib/src/revset.rs b/lib/src/revset.rs index 155d62ff30..ce67dc710c 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -14,7 +14,7 @@ #![allow(missing_docs)] -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{HashMap, HashSet}; use std::convert::Infallible; use std::ops::Range; use std::path::Path; @@ -23,7 +23,6 @@ use std::str::FromStr; use std::sync::Arc; use std::{error, fmt}; -use either::Either; use itertools::Itertools; use once_cell::sync::Lazy; use pest::iterators::{Pair, Pairs}; @@ -42,6 +41,7 @@ use crate::repo::Repo; use crate::repo_path::{FsPathParseError, RepoPath}; use crate::revset_graph::RevsetGraphEdge; use crate::store::Store; +use crate::str_util::StringPattern; /// Error occurred during symbol resolution. #[derive(Debug, Error)] @@ -305,41 +305,6 @@ fn rename_rules_in_pest_error(mut err: pest::error::Error) -> pest::error: pub const GENERATION_RANGE_FULL: Range = 0..u64::MAX; pub const GENERATION_RANGE_EMPTY: Range = 0..0; -/// Pattern to be tested against string property like commit description or -/// branch name. -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum StringPattern { - /// Matches strings exactly equal to `string`. - Exact(String), - /// Matches strings that contain `substring`. - Substring(String), -} - -impl StringPattern { - /// Pattern that matches any string. - pub fn everything() -> Self { - StringPattern::Substring(String::new()) - } - - /// Returns true if this pattern matches the `haystack`. - pub fn matches(&self, haystack: &str) -> bool { - match self { - StringPattern::Exact(literal) => haystack == literal, - StringPattern::Substring(needle) => haystack.contains(needle), - } - } - - /// Returns a literal pattern if this should match input strings exactly. - /// - /// This can be used to optimize map lookup by exact key. - pub fn as_exact(&self) -> Option<&str> { - match self { - StringPattern::Exact(literal) => Some(literal), - StringPattern::Substring(_) => None, - } - } -} - /// Symbol or function to be resolved to `CommitId`s. #[derive(Clone, Debug, Eq, PartialEq)] pub enum RevsetCommitRef { @@ -1512,16 +1477,8 @@ fn parse_function_argument_to_string_pattern( else { return Err(make_type_error()); }; - match kind.as_ref() { - "exact" => StringPattern::Exact(needle.clone()), - "substring" => StringPattern::Substring(needle.clone()), - _ => { - // TODO: error span can be narrowed to the lhs node - return Err(make_error(format!( - r#"Invalid string pattern kind "{kind}""# - ))); - } - } + // TODO: error span can be narrowed to the lhs node + StringPattern::from_str_kind(needle, kind).map_err(|err| make_error(err.to_string()))? } _ => return Err(make_type_error()), }; @@ -1980,21 +1937,6 @@ pub fn walk_revs<'index>( .evaluate(repo) } -fn filter_map_values_by_key_pattern<'a: 'b, 'b, V>( - map: &'a BTreeMap, - pattern: &'b StringPattern, -) -> impl Iterator + 'b { - if let Some(key) = pattern.as_exact() { - Either::Left(map.get(key).into_iter()) - } else { - Either::Right( - map.iter() - .filter(|(key, _)| pattern.matches(key)) - .map(|(_, value)| value), - ) - } -} - fn resolve_git_ref(repo: &dyn Repo, symbol: &str) -> Option> { let view = repo.view(); for git_ref_prefix in &["", "refs/"] { @@ -2194,8 +2136,9 @@ fn resolve_commit_ref( RevsetCommitRef::Root => Ok(vec![repo.store().root_commit_id().clone()]), RevsetCommitRef::Branches(pattern) => { let view_data = repo.view().store_view(); - let commit_ids = filter_map_values_by_key_pattern(&view_data.local_branches, pattern) - .flat_map(|target| target.added_ids()) + let commit_ids = pattern + .filter_btree_map(&view_data.local_branches) + .flat_map(|(_, target)| target.added_ids()) .cloned() .collect(); Ok(commit_ids) @@ -2205,14 +2148,12 @@ fn resolve_commit_ref( remote_pattern, } => { let view_data = repo.view().store_view(); - let commit_ids = - filter_map_values_by_key_pattern(&view_data.remote_views, remote_pattern) - .flat_map(|remote_view| { - filter_map_values_by_key_pattern(&remote_view.branches, branch_pattern) - }) - .flat_map(|remote_ref| remote_ref.target.added_ids()) - .cloned() - .collect(); + let commit_ids = remote_pattern + .filter_btree_map(&view_data.remote_views) + .flat_map(|(_, remote_view)| branch_pattern.filter_btree_map(&remote_view.branches)) + .flat_map(|(_, remote_ref)| remote_ref.target.added_ids()) + .cloned() + .collect(); Ok(commit_ids) } RevsetCommitRef::Tags => { diff --git a/lib/src/str_util.rs b/lib/src/str_util.rs new file mode 100644 index 0000000000..e3b8476e0f --- /dev/null +++ b/lib/src/str_util.rs @@ -0,0 +1,101 @@ +// Copyright 2021-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! String helpers. + +use std::borrow::Borrow; +use std::collections::BTreeMap; + +use either::Either; +use thiserror::Error; + +/// Error occurred during pattern string parsing. +#[derive(Debug, Error)] +pub enum StringPatternParseError { + /// Unknown pattern kind is specified. + #[error(r#"Invalid string pattern kind "{0}""#)] + InvalidKind(String), + /// Failed to parse glob pattern. + #[error(transparent)] + GlobPattern(glob::PatternError), +} + +/// Pattern to be tested against string property like commit description or +/// branch name. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum StringPattern { + /// Matches strings exactly equal to `string`. + Exact(String), + /// Unix-style shell wildcard pattern. + Glob(glob::Pattern), + /// Matches strings that contain `substring`. + Substring(String), +} + +impl StringPattern { + /// Pattern that matches any string. + pub const fn everything() -> Self { + StringPattern::Substring(String::new()) + } + + /// Parses the given string as glob pattern. + pub fn glob(src: &str) -> Result { + // TODO: might be better to do parsing and compilation separately since + // not all backends would use the compiled pattern object. + // TODO: if no meta character found, it can be mapped to Exact. + let pattern = glob::Pattern::new(src).map_err(StringPatternParseError::GlobPattern)?; + Ok(StringPattern::Glob(pattern)) + } + + /// Parses the given string as pattern of the specified `kind`. + pub fn from_str_kind(src: &str, kind: &str) -> Result { + match kind { + "exact" => Ok(StringPattern::Exact(src.to_owned())), + "glob" => StringPattern::glob(src), + "substring" => Ok(StringPattern::Substring(src.to_owned())), + _ => Err(StringPatternParseError::InvalidKind(kind.to_owned())), + } + } + + /// Returns a literal pattern if this should match input strings exactly. + /// + /// This can be used to optimize map lookup by exact key. + pub fn as_exact(&self) -> Option<&str> { + match self { + StringPattern::Exact(literal) => Some(literal), + StringPattern::Glob(_) | StringPattern::Substring(_) => None, + } + } + + /// Returns true if this pattern matches the `haystack`. + pub fn matches(&self, haystack: &str) -> bool { + match self { + StringPattern::Exact(literal) => haystack == literal, + StringPattern::Glob(pattern) => pattern.matches(haystack), + StringPattern::Substring(needle) => haystack.contains(needle), + } + } + + /// Iterates entries of the given `map` whose keys matches this pattern. + pub fn filter_btree_map<'a: 'b, 'b, K: Borrow + Ord, V>( + &'b self, + map: &'a BTreeMap, + ) -> impl Iterator + 'b { + if let Some(key) = self.as_exact() { + Either::Left(map.get_key_value(key).into_iter()) + } else { + Either::Right(map.iter().filter(|&(key, _)| self.matches(key.borrow()))) + } + } +} diff --git a/lib/tests/test_revset.rs b/lib/tests/test_revset.rs index ae521bb200..53360bab70 100644 --- a/lib/tests/test_revset.rs +++ b/lib/tests/test_revset.rs @@ -1775,6 +1775,10 @@ fn test_evaluate_expression_branches() { resolve_commit_ids(mut_repo, "branches(exact:branch1)"), vec![commit1.id().clone()] ); + assert_eq!( + resolve_commit_ids(mut_repo, r#"branches(glob:"branch?")"#), + vec![commit2.id().clone(), commit1.id().clone()] + ); // Can silently resolve to an empty set if there's no matches assert_eq!(resolve_commit_ids(mut_repo, "branches(branch3)"), vec![]); assert_eq!(