repo_path: show more detailed error if filesystem path failed to parse

This should address both use cases:
 1. If from_relative_path() is directly called, the error says ".." shouldn't
    be included in the (normalized) relative path.
 2. If parse_fs_path() is used, the error message contains paths relative to
    cwd. #3216
This commit is contained in:
Yuya Nishihara 2023-11-20 16:59:54 +09:00
parent 2df977b221
commit a224d0f172
5 changed files with 73 additions and 21 deletions

View file

@ -411,6 +411,8 @@ impl From<TemplateParseError> for CommandError {
impl From<FsPathParseError> for CommandError { impl From<FsPathParseError> for CommandError {
fn from(err: FsPathParseError) -> Self { 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) user_error(err)
} }
} }

View file

@ -225,6 +225,25 @@ fn test_no_workspace_directory() {
"###); "###);
} }
#[test]
fn test_bad_path() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]);
let repo_path = test_env.env_root().join("repo");
let stderr = test_env.jj_cmd_failure(&repo_path, &["cat", "../out"]);
insta::assert_snapshot!(stderr.replace('\\', "/"), @r###"
Error: Path "../out" is not in the repo "."
Caused by: Invalid component ".." in repo-relative path "../out"
"###);
let stderr = test_env.jj_cmd_failure(test_env.env_root(), &["cat", "-Rrepo", "out"]);
insta::assert_snapshot!(stderr.replace('\\', "/"), @r###"
Error: Path "out" is not in the repo "repo"
Caused by: Invalid component ".." in repo-relative path "../out"
"###);
}
#[test] #[test]
fn test_broken_repo_structure() { fn test_broken_repo_structure() {
let test_env = TestEnvironment::default(); let test_env = TestEnvironment::default();

View file

@ -141,13 +141,13 @@ fn test_bad_function_call() {
"###); "###);
let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", r#"file(a, "../out")"#]); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", r#"file(a, "../out")"#]);
insta::assert_snapshot!(stderr, @r###" insta::assert_snapshot!(stderr.replace('\\', "/"), @r###"
Error: Failed to parse revset: --> 1:9 Error: Failed to parse revset: --> 1:9
| |
1 | file(a, "../out") 1 | file(a, "../out")
| ^------^ | ^------^
| |
= Invalid file pattern: Path "../out" is not in the repo = Invalid file pattern: Path "../out" is not in the repo ".": Invalid component ".." in repo-relative path "../out"
"###); "###);
let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "branches(bad:pattern)"]); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "branches(bad:pattern)"]);

View file

@ -1043,7 +1043,7 @@ impl TreeState {
let repo_paths = trace_span!("processing fsmonitor paths").in_scope(|| { let repo_paths = trace_span!("processing fsmonitor paths").in_scope(|| {
changed_files changed_files
.into_iter() .into_iter()
.filter_map(RepoPathBuf::from_relative_path) .filter_map(|path| RepoPathBuf::from_relative_path(path).ok())
.collect_vec() .collect_vec()
}); });

View file

@ -204,14 +204,18 @@ impl RepoPathBuf {
/// Converts repo-relative `Path` to `RepoPathBuf`. /// Converts repo-relative `Path` to `RepoPathBuf`.
/// ///
/// The input path should not contain `.` or `..`. /// The input path should not contain `.` or `..`.
pub fn from_relative_path(relative_path: impl AsRef<Path>) -> Option<Self> { pub fn from_relative_path(
relative_path: impl AsRef<Path>,
) -> Result<Self, RelativePathParseError> {
let relative_path = relative_path.as_ref(); let relative_path = relative_path.as_ref();
let mut components = relative_path let mut components = relative_path
.components() .components()
.map(|c| match c { .map(|c| match c {
Component::Normal(name) => Some(name.to_str().unwrap()), Component::Normal(name) => Ok(name.to_str().unwrap()),
// TODO: better to return Err instead of None? _ => Err(RelativePathParseError::InvalidComponent {
_ => None, component: c.as_os_str().to_string_lossy().into(),
path: relative_path.into(),
}),
}) })
.fuse(); .fuse();
let mut value = String::with_capacity(relative_path.as_os_str().len()); let mut value = String::with_capacity(relative_path.as_os_str().len());
@ -222,7 +226,7 @@ impl RepoPathBuf {
value.push('/'); value.push('/');
value.push_str(name?); value.push_str(name?);
} }
Some(RepoPathBuf { value }) Ok(RepoPathBuf { value })
} }
/// Parses an `input` path into a `RepoPathBuf` relative to `base`. /// Parses an `input` path into a `RepoPathBuf` relative to `base`.
@ -241,8 +245,11 @@ impl RepoPathBuf {
if repo_relative_path == Path::new(".") { if repo_relative_path == Path::new(".") {
return Ok(Self::root()); return Ok(Self::root());
} }
Self::from_relative_path(repo_relative_path) Self::from_relative_path(repo_relative_path).map_err(|source| FsPathParseError {
.ok_or_else(|| FsPathParseError::InputNotInRepo(input.to_owned())) base: file_util::relative_path(cwd, base).into(),
input: input.into(),
source,
})
} }
/// Consumes this and returns the underlying string representation. /// Consumes this and returns the underlying string representation.
@ -411,9 +418,20 @@ impl PartialOrd for RepoPathBuf {
} }
#[derive(Clone, Debug, Eq, Error, PartialEq)] #[derive(Clone, Debug, Eq, Error, PartialEq)]
pub enum FsPathParseError { pub enum RelativePathParseError {
#[error(r#"Path "{}" is not in the repo"#, .0.display())] #[error(r#"Invalid component "{component}" in repo-relative path "{path}""#)]
InputNotInRepo(PathBuf), InvalidComponent {
component: Box<str>,
path: Box<Path>,
},
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[error(r#"Path "{input}" is not in the repo "{base}""#)]
pub struct FsPathParseError {
base: Box<Path>,
input: Box<Path>,
source: RelativePathParseError,
} }
fn is_valid_repo_path_component_str(value: &str) -> bool { fn is_valid_repo_path_component_str(value: &str) -> bool {
@ -428,6 +446,7 @@ fn is_valid_repo_path_str(value: &str) -> bool {
mod tests { mod tests {
use std::panic; use std::panic;
use assert_matches::assert_matches;
use itertools::Itertools as _; use itertools::Itertools as _;
use super::*; use super::*;
@ -667,9 +686,12 @@ mod tests {
RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "dir/file").as_deref(), RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "dir/file").as_deref(),
Ok(repo_path("dir/file")) Ok(repo_path("dir/file"))
); );
assert_eq!( assert_matches!(
RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".."), RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".."),
Err(FsPathParseError::InputNotInRepo("..".into())) Err(FsPathParseError {
source: RelativePathParseError::InvalidComponent { .. },
..
})
); );
assert_eq!( assert_eq!(
RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo").as_deref(), RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo").as_deref(),
@ -717,9 +739,12 @@ mod tests {
RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "..").as_deref(), RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "..").as_deref(),
Ok(RepoPath::root()) Ok(RepoPath::root())
); );
assert_eq!( assert_matches!(
RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../.."), RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../.."),
Err(FsPathParseError::InputNotInRepo("../..".into())) Err(FsPathParseError {
source: RelativePathParseError::InvalidComponent { .. },
..
})
); );
assert_eq!( assert_eq!(
RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../other-dir/file").as_deref(), RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../other-dir/file").as_deref(),
@ -733,13 +758,19 @@ mod tests {
let cwd_path = temp_dir.path().join("cwd"); let cwd_path = temp_dir.path().join("cwd");
let wc_path = cwd_path.join("repo"); let wc_path = cwd_path.join("repo");
assert_eq!( assert_matches!(
RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ""), RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ""),
Err(FsPathParseError::InputNotInRepo("".into())) Err(FsPathParseError {
source: RelativePathParseError::InvalidComponent { .. },
..
})
); );
assert_eq!( assert_matches!(
RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "not-repo"), RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "not-repo"),
Err(FsPathParseError::InputNotInRepo("not-repo".into())) Err(FsPathParseError {
source: RelativePathParseError::InvalidComponent { .. },
..
})
); );
assert_eq!( assert_eq!(
RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo").as_deref(), RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo").as_deref(),