Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

extract StringPattern to module, add glob: pattern to be used in CLI #2407

Merged
merged 4 commits into from
Oct 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion cli/src/commands/branch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
3 changes: 2 additions & 1 deletion cli/src/commands/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
1 change: 1 addition & 0 deletions docs/revsets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
1 change: 1 addition & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
85 changes: 13 additions & 72 deletions lib/src/revset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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};
Expand All @@ -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)]
Expand Down Expand Up @@ -305,41 +305,6 @@ fn rename_rules_in_pest_error(mut err: pest::error::Error<Rule>) -> pest::error:
pub const GENERATION_RANGE_FULL: Range<u64> = 0..u64::MAX;
pub const GENERATION_RANGE_EMPTY: Range<u64> = 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 {
Expand Down Expand Up @@ -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()),
};
Expand Down Expand Up @@ -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<String, V>,
pattern: &'b StringPattern,
) -> impl Iterator<Item = &'a V> + '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<Vec<CommitId>> {
let view = repo.view();
for git_ref_prefix in &["", "refs/"] {
Expand Down Expand Up @@ -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)
Expand All @@ -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 => {
Expand Down
101 changes: 101 additions & 0 deletions lib/src/str_util.rs
Original file line number Diff line number Diff line change
@@ -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<Self, StringPatternParseError> {
// 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<Self, StringPatternParseError> {
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<str> + Ord, V>(
&'b self,
map: &'a BTreeMap<K, V>,
) -> impl Iterator<Item = (&'a K, &'a V)> + '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())))
}
}
}
4 changes: 4 additions & 0 deletions lib/tests/test_revset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down