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

cli: add support for file kind:pattern syntax

This is basically the same as string kind:pattern syntax in CLI. This will
hopefully be superseded by filesets, but I'm not sure if that will work out.
A file name is more likely to contain whitespaces, which will have to be
quoted as '"Documents and Settings"'.
This commit is contained in:
Yuya Nishihara 2024-04-06 16:46:24 +09:00
parent 8b32a8a916
commit 07d027193b
7 changed files with 59 additions and 22 deletions

View file

@ -27,6 +27,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* `jj status` now supports filtering by paths. For example, `jj status .` will
only list changed files that are descendants of the current directory.
* File path arguments now support [file pattern
syntax](docs/filesets.md#file-patterns).
### Fixed bugs
## [0.16.0] - 2024-04-03

View file

@ -36,12 +36,12 @@ use indexmap::{IndexMap, IndexSet};
use itertools::Itertools;
use jj_lib::backend::{ChangeId, CommitId, MergedTreeId, TreeValue};
use jj_lib::commit::Commit;
use jj_lib::fileset::FilesetExpression;
use jj_lib::fileset::{FilePattern, FilesetExpression, FilesetParseContext};
use jj_lib::git_backend::GitBackend;
use jj_lib::gitignore::{GitIgnoreError, GitIgnoreFile};
use jj_lib::hex_util::to_reverse_hex;
use jj_lib::id_prefix::IdPrefixContext;
use jj_lib::matchers::{EverythingMatcher, Matcher, PrefixMatcher};
use jj_lib::matchers::Matcher;
use jj_lib::merge::MergedTreeValue;
use jj_lib::merged_tree::MergedTree;
use jj_lib::object_id::ObjectId;
@ -646,19 +646,36 @@ impl WorkspaceCommandHelper {
RepoPathBuf::parse_fs_path(&self.cwd, self.workspace_root(), input)
}
pub fn matcher_from_values(&self, values: &[String]) -> Result<Box<dyn Matcher>, CommandError> {
/// Parses the given strings as file patterns.
pub fn parse_file_patterns(
&self,
values: &[String],
) -> Result<FilesetExpression, CommandError> {
// TODO: This function might be superseded by parse_union_filesets(),
// but it would be weird if parse_union_*() had a special case for the
// empty arguments.
if values.is_empty() {
Ok(Box::new(EverythingMatcher))
Ok(FilesetExpression::all())
} else {
// TODO: Add support for globs and other formats
let paths: Vec<_> = values
let ctx = FilesetParseContext {
cwd: &self.cwd,
workspace_root: self.workspace.workspace_root(),
};
let expressions = values
.iter()
.map(|v| self.parse_file_path(v))
.try_collect()?;
Ok(Box::new(PrefixMatcher::new(paths)))
.map(|v| FilePattern::parse(&ctx, v))
.map_ok(FilesetExpression::pattern)
.try_collect()
.map_err(user_error)?;
Ok(FilesetExpression::union_all(expressions))
}
}
pub fn matcher_from_values(&self, values: &[String]) -> Result<Box<dyn Matcher>, CommandError> {
let expr = self.parse_file_patterns(values)?;
Ok(expr.to_matcher())
}
#[instrument(skip_all)]
pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
fn get_excludes_file_path(config: &gix::config::File) -> Option<PathBuf> {

View file

@ -487,8 +487,6 @@ impl From<TemplateParseError> for CommandError {
impl From<FsPathParseError> for CommandError {
fn from(err: FsPathParseError) -> Self {
// TODO: implement pattern prefix like "root:<path>" or "--cwd" option,
// and suggest it if the user input looks like repo-relative path #3216.
user_error(err)
}
}
@ -522,6 +520,8 @@ impl From<GitIgnoreError> for CommandError {
fn find_source_parse_error_hint(err: &dyn error::Error) -> Option<String> {
let source = err.source()?;
// TODO: For FilePatternParseError, suggest "root:<path>" if the user
// input looks like repo-relative path #3216.
if let Some(source) = source.downcast_ref() {
string_pattern_parse_error_hint(source)
} else {

View file

@ -43,6 +43,7 @@ pub(crate) fn cmd_cat(
let workspace_command = command.workspace_helper(ui)?;
let commit = workspace_command.resolve_single_rev(&args.revision)?;
let tree = commit.tree()?;
// TODO: migrate to .parse_file_patterns()?.to_matcher()?
let path = workspace_command.parse_file_path(&args.path)?;
let repo = workspace_command.repo();
let value = tree.path_value(&path);

View file

@ -60,6 +60,7 @@ pub(crate) fn cmd_chmod(
};
let mut workspace_command = command.workspace_helper(ui)?;
// TODO: migrate to .parse_file_patterns()?.to_matcher()
let repo_paths: Vec<_> = args
.paths
.iter()

View file

@ -12,9 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use itertools::Itertools;
use jj_lib::backend::CommitId;
use jj_lib::fileset::FilesetExpression;
use jj_lib::repo::Repo;
use jj_lib::revset::{self, RevsetExpression, RevsetFilterPredicate, RevsetIteratorExt};
use jj_lib::revset_graph::{
@ -78,6 +76,7 @@ pub(crate) fn cmd_log(
) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
let fileset_expression = workspace_command.parse_file_patterns(&args.paths)?;
let revset_expression = {
// only use default revset if neither revset nor path are specified
let mut expression = if args.revisions.is_empty() && args.paths.is_empty() {
@ -90,20 +89,16 @@ pub(crate) fn cmd_log(
workspace_command.attach_revset_evaluator(RevsetExpression::all())?
};
if !args.paths.is_empty() {
let file_expressions: Vec<_> = args
.paths
.iter()
.map(|path_arg| workspace_command.parse_file_path(path_arg))
.map_ok(FilesetExpression::prefix_path)
.try_collect()?;
let expr = FilesetExpression::union_all(file_expressions);
expression.intersect_with(&RevsetExpression::filter(RevsetFilterPredicate::File(expr)));
// Beware that args.paths = ["root:."] is not identical to []. The
// former will filter out empty commits.
let predicate = RevsetFilterPredicate::File(fileset_expression.clone());
expression.intersect_with(&RevsetExpression::filter(predicate));
}
expression
};
let repo = workspace_command.repo();
let matcher = workspace_command.matcher_from_values(&args.paths)?;
let matcher = fileset_expression.to_matcher();
let revset = revset_expression.evaluate()?;
let store = repo.store();

View file

@ -822,6 +822,26 @@ fn test_log_filtered_by_path() {
A file2
"###);
// "root:<path>" is resolved relative to the workspace root.
let stdout = test_env.jj_cmd_success(
test_env.env_root(),
&[
"log",
"-R",
repo_path.to_str().unwrap(),
"-Tdescription",
"-s",
"root:file1",
],
);
insta::assert_snapshot!(stdout.replace('\\', "/"), @r###"
@ second
M repo/file1
first
A repo/file1
~
"###);
// file() revset doesn't filter the diff.
let stdout = test_env.jj_cmd_success(
&repo_path,