Docs

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
}