paths
Source: crates/lvcs-doc/src/paths.rs.
/// A set of gitignore-syntax patterns, matched against document addresses
/// (design §4). The lines keep their gitignore meaning: the last line that
/// matches the address, or a directory above it, decides, so a `!` line
/// re-includes what an earlier line matched, and blank and `#` lines match
/// nothing. With `case_insensitive`, ASCII letters match either case and
/// every other character matches only itself (ASCII folding, decision 122),
/// as a case-insensitive filesystem resolves `BUILD.RS` to `build.rs`.
#[derive(Debug, Clone)]
pub struct PathSet {
matcher: Gitignore,
}
/// Why a [`PathSet`] didn't build.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathSetError {
/// A line gitignore syntax refuses, with the reason.
Pattern { pattern: String, reason: String },
/// Every line parsed, but the set didn't compile.
Build { reason: String },
}
impl PathSet {
/// The set of `patterns`, one gitignore line each, in order.
pub fn new<I>(patterns: I, case_insensitive: bool) -> Result<Self, PathSetError>
where
I: IntoIterator,
I::Item: AsRef<str>,
{
let build = |err: ignore::Error| PathSetError::Build {
reason: err.to_string(),
};
let mut builder = GitignoreBuilder::new("");
// Before any line: the setting applies to lines added after it.
builder.case_insensitive(case_insensitive).map_err(build)?;
for pattern in patterns {
let pattern = pattern.as_ref();
builder
.add_line(None, pattern)
.map_err(|err| PathSetError::Pattern {
pattern: pattern.to_owned(),
reason: err.to_string(),
})?;
}
let matcher = builder.build().map_err(build)?;
Ok(Self { matcher })
}
/// Whether the set matches the file at `address`: a line matches it or
/// a directory above it, and no later `!` line re-includes it. An
/// address that isn't a plain relative path, one whose every
/// `/`-separated segment is a name (not empty, `.`, or `..`), matches
/// nothing; no valid address (design §4) is one.
pub fn matches(&self, address: &str) -> bool {
let plain = address
.split('/')
.all(|segment| !matches!(segment, "" | "." | ".."));
plain
&& self
.matcher
.matched_path_or_any_parents(Path::new(address), false)
.is_ignore()
}
}
/// `DEFAULT_PROTECTED` as one case-insensitive [`PathSet`], built once:
/// the protected-path rule (decision 122).
pub fn protected() -> &'static PathSet {
static PROTECTED: LazyLock<PathSet> = LazyLock::new(|| {
PathSet::new(DEFAULT_PROTECTED.iter().map(|rule| rule.pattern), true)
.expect("DEFAULT_PROTECTED is valid gitignore syntax")
});
&PROTECTED
}