use std::ffi::OsStr;
use std::path::{Component, Path};
use anyhow::{Context, Result};
const CHANGEPACKS_DIR: &str = ".changepacks";
#[must_use]
pub fn contains_changepacks_component(path: &Path) -> bool {
if let Some(text) = path.to_str()
&& !text.contains(CHANGEPACKS_DIR)
{
return false;
}
path.components()
.any(|c| matches!(c, Component::Normal(name) if name == OsStr::new(CHANGEPACKS_DIR)))
}
pub fn manifest_parent_dir(manifest_path: &Path) -> Result<&Path> {
manifest_path
.parent()
.with_context(|| format!("Parent not found - {}", manifest_path.display()))
}
pub(crate) fn should_mark_changed(candidate: &Path, project_manifest: &Path) -> Result<bool> {
if contains_changepacks_component(candidate) {
return Ok(false);
}
let project_dir = manifest_parent_dir(project_manifest)?;
Ok(candidate.starts_with(project_dir))
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case(".changepacks/changepack_log_x.json", true)]
#[case("a/.changepacks/b", true)]
#[case("/project/.changepacks/change.json", true)]
#[case(".changepacks-backup/file.json", false)]
#[case("notes-about-.changepacks.md", false)]
#[case("pkg/.changepacks.bak/file.json", false)]
#[case("pkg/src/my.changepacks.rs", false)]
#[case("src/index.js", false)]
#[case("packages/core/package.json", false)]
fn test_contains_changepacks_component(#[case] path: &str, #[case] expected: bool) {
assert_eq!(contains_changepacks_component(Path::new(path)), expected);
}
#[cfg(any(unix, windows))]
fn non_utf8_component() -> std::path::PathBuf {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
std::path::PathBuf::from(OsStr::from_bytes(&[0xff]))
}
#[cfg(windows)]
{
use std::os::windows::ffi::OsStringExt;
std::path::PathBuf::from(std::ffi::OsString::from_wide(&[0xD800]))
}
}
#[cfg(any(unix, windows))]
#[test]
fn test_contains_changepacks_component_non_utf8_path_falls_through() {
let path = non_utf8_component().join(".changepacks").join("log.json");
assert!(path.to_str().is_none());
assert!(contains_changepacks_component(&path));
}
#[rstest]
#[case("/project/src/index.js", "/project/package.json", true)]
#[case("/project/.changepacks/change.json", "/project/package.json", false)]
#[case("/other-project/src/index.js", "/project/package.json", false)]
#[case(
"/project/.changepacks-backup/pinned.json",
"/project/package.json",
true
)]
#[case("/project/notes-about-.changepacks.md", "/project/package.json", true)]
fn test_should_mark_changed(
#[case] candidate: &str,
#[case] manifest: &str,
#[case] expected: bool,
) {
assert_eq!(
should_mark_changed(Path::new(candidate), Path::new(manifest)).unwrap(),
expected
);
}
#[test]
fn test_manifest_parent_dir_returns_containing_dir() {
assert_eq!(
manifest_parent_dir(Path::new("/project/package.json")).unwrap(),
Path::new("/project")
);
}
#[test]
fn test_manifest_parent_dir_errors_without_parent() {
let err = manifest_parent_dir(Path::new("")).unwrap_err();
assert_eq!(err.to_string(), "Parent not found - ");
}
#[test]
fn test_should_mark_changed_errors_when_manifest_has_no_parent() {
let candidate = Path::new("src/index.js");
let manifest = Path::new("/");
let err = should_mark_changed(candidate, manifest).unwrap_err();
assert!(err.to_string().contains("Parent not found - /"));
}
}