diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index 42782313a8..32cc17c5cb 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -69,7 +69,7 @@ use jj_lib::working_copy::{ use jj_lib::workspace::{ default_working_copy_factories, LockedWorkspace, Workspace, WorkspaceLoadError, WorkspaceLoader, }; -use jj_lib::{dag_walk, file_util, git, op_heads_store, op_walk, revset}; +use jj_lib::{dag_walk, file_util, fileset, git, op_heads_store, op_walk, revset}; use once_cell::unsync::OnceCell; use tracing::instrument; use tracing_chrome::ChromeLayerBuilder; @@ -657,10 +657,7 @@ impl WorkspaceCommandHelper { if values.is_empty() { Ok(FilesetExpression::all()) } else { - let ctx = FilesetParseContext { - cwd: &self.cwd, - workspace_root: self.workspace.workspace_root(), - }; + let ctx = self.fileset_parse_context(); let expressions = values .iter() .map(|v| FilePattern::parse(&ctx, v)) @@ -671,6 +668,26 @@ impl WorkspaceCommandHelper { } } + /// Parses the given fileset expressions and concatenates them all. + pub fn parse_union_filesets( + &self, + file_args: &[String], // TODO: introduce FileArg newtype? + ) -> Result { + let ctx = self.fileset_parse_context(); + let expressions: Vec<_> = file_args + .iter() + .map(|arg| fileset::parse(arg, &ctx)) + .try_collect()?; + Ok(FilesetExpression::union_all(expressions)) + } + + pub(crate) fn fileset_parse_context(&self) -> FilesetParseContext<'_> { + FilesetParseContext { + cwd: &self.cwd, + workspace_root: self.workspace.workspace_root(), + } + } + #[instrument(skip_all)] pub fn base_ignores(&self) -> Result, GitIgnoreError> { fn get_excludes_file_path(config: &gix::config::File) -> Option { diff --git a/cli/src/command_error.rs b/cli/src/command_error.rs index 5be11648e6..c9121e85c5 100644 --- a/cli/src/command_error.rs +++ b/cli/src/command_error.rs @@ -19,6 +19,7 @@ use std::{error, io, iter, str}; use itertools::Itertools as _; use jj_lib::backend::BackendError; +use jj_lib::fileset::{FilesetParseError, FilesetParseErrorKind}; use jj_lib::git::{GitConfigParseError, GitExportError, GitImportError, GitRemoteManagementError}; use jj_lib::gitignore::GitIgnoreError; use jj_lib::op_heads_store::OpHeadResolutionError; @@ -400,6 +401,24 @@ impl From for CommandError { } } +impl From for CommandError { + fn from(err: FilesetParseError) -> Self { + let hint = match err.kind() { + FilesetParseErrorKind::NoSuchFunction { + name: _, + candidates, + } => format_similarity_hint(candidates), + FilesetParseErrorKind::InvalidArguments { .. } + | FilesetParseErrorKind::Expression(_) => find_source_parse_error_hint(&err), + _ => None, + }; + let mut cmd_err = + user_error_with_message(format!("Failed to parse fileset: {}", err.kind()), err); + cmd_err.extend_hints(hint); + cmd_err + } +} + impl From for CommandError { fn from(err: RevsetParseError) -> Self { // Only for the bottom error, which is usually the root cause diff --git a/cli/src/commands/debug.rs b/cli/src/commands/debug.rs index 7916503191..1b22cc96de 100644 --- a/cli/src/commands/debug.rs +++ b/cli/src/commands/debug.rs @@ -25,7 +25,7 @@ use jj_lib::object_id::ObjectId; use jj_lib::repo::Repo; use jj_lib::repo_path::RepoPathBuf; use jj_lib::working_copy::WorkingCopy; -use jj_lib::{op_walk, revset}; +use jj_lib::{fileset, op_walk, revset}; use crate::cli_util::{CommandHelper, RevisionArg}; use crate::command_error::{internal_error, user_error, CommandError}; @@ -36,6 +36,7 @@ use crate::{revset_util, template_parser}; #[derive(Subcommand, Clone, Debug)] #[command(hide = true)] pub enum DebugCommand { + Fileset(DebugFilesetArgs), Revset(DebugRevsetArgs), #[command(name = "workingcopy")] WorkingCopy(DebugWorkingCopyArgs), @@ -50,6 +51,13 @@ pub enum DebugCommand { Watchman(DebugWatchmanSubcommand), } +/// Parse fileset expression +#[derive(clap::Args, Clone, Debug)] +pub struct DebugFilesetArgs { + #[arg(value_hint = clap::ValueHint::AnyPath)] + path: String, +} + /// Evaluate revset to full commit IDs #[derive(clap::Args, Clone, Debug)] pub struct DebugRevsetArgs { @@ -121,6 +129,7 @@ pub fn cmd_debug( subcommand: &DebugCommand, ) -> Result<(), CommandError> { match subcommand { + DebugCommand::Fileset(args) => cmd_debug_fileset(ui, command, args), DebugCommand::Revset(args) => cmd_debug_revset(ui, command, args), DebugCommand::WorkingCopy(args) => cmd_debug_working_copy(ui, command, args), DebugCommand::Template(args) => cmd_debug_template(ui, command, args), @@ -132,6 +141,25 @@ pub fn cmd_debug( } } +fn cmd_debug_fileset( + ui: &mut Ui, + command: &CommandHelper, + args: &DebugFilesetArgs, +) -> Result<(), CommandError> { + let workspace_command = command.workspace_helper(ui)?; + let ctx = workspace_command.fileset_parse_context(); + + let expression = fileset::parse(&args.path, &ctx)?; + writeln!(ui.stdout(), "-- Parsed:")?; + writeln!(ui.stdout(), "{expression:#?}")?; + writeln!(ui.stdout())?; + + let matcher = expression.to_matcher(); + writeln!(ui.stdout(), "-- Matcher:")?; + writeln!(ui.stdout(), "{matcher:#?}")?; + Ok(()) +} + fn cmd_debug_revset( ui: &mut Ui, command: &CommandHelper, diff --git a/cli/tests/test_debug_command.rs b/cli/tests/test_debug_command.rs index ef8eabfd2b..4a126c3155 100644 --- a/cli/tests/test_debug_command.rs +++ b/cli/tests/test_debug_command.rs @@ -17,6 +17,36 @@ use regex::Regex; use crate::common::TestEnvironment; +#[test] +fn test_debug_fileset() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + let workspace_path = test_env.env_root().join("repo"); + + let stdout = test_env.jj_cmd_success(&workspace_path, &["debug", "fileset", "all()"]); + assert_snapshot!(stdout, @r###" + -- Parsed: + All + + -- Matcher: + EverythingMatcher + "###); + + let stderr = test_env.jj_cmd_failure(&workspace_path, &["debug", "fileset", "cwd:.."]); + assert_snapshot!(stderr.replace('\\', "/"), @r###" + Error: Failed to parse fileset: Invalid file pattern + Caused by: + 1: --> 1:1 + | + 1 | cwd:.. + | ^----^ + | + = Invalid file pattern + 2: Path ".." is not in the repo "." + 3: Invalid component ".." in repo-relative path "../" + "###); +} + #[test] fn test_debug_revset() { let test_env = TestEnvironment::default();