Skip to main content

gix_pathspec/
lib.rs

1//! Parse [path specifications](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefpathspecapathspec) and
2//! see if a path matches.
3//!
4//! ## Examples
5//!
6//! ```
7//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
8//! use std::path::Path;
9//!
10//! fn no_attrs(
11//!     _path: &bstr::BStr,
12//!     _case: gix_pathspec::attributes::glob::pattern::Case,
13//!     _is_dir: bool,
14//!     _out: &mut gix_pathspec::attributes::search::Outcome,
15//! ) -> bool {
16//!     false
17//! }
18//!
19//! let specs = ["src/**", ":!src/generated/**"]
20//!     .into_iter()
21//!     .map(|spec| gix_pathspec::parse(spec.as_bytes(), Default::default()).unwrap());
22//! let mut search = gix_pathspec::Search::from_specs(specs, None, Path::new(""))?;
23//!
24//! assert!(search.can_match_relative_path("src".into(), Some(true)));
25//!
26//! let matched = search
27//!     .pattern_matching_relative_path("src/lib.rs".into(), Some(false), &mut no_attrs)
28//!     .unwrap();
29//! assert_eq!(matched.pattern.path(), "src/**");
30//! assert!(!matched.pattern.is_excluded());
31//!
32//! let excluded = search
33//!     .pattern_matching_relative_path("src/generated/lib.rs".into(), Some(false), &mut no_attrs)
34//!     .unwrap();
35//! assert_eq!(excluded.pattern.to_bstring(), ":(exclude)src/generated/**");
36//! assert!(excluded.pattern.is_excluded());
37//! # Ok(()) }
38//! ```
39#![deny(missing_docs)]
40#![forbid(unsafe_code)]
41
42use std::path::PathBuf;
43
44use bitflags::bitflags;
45use bstr::BString;
46/// `gix-glob` types are available through [`attributes::glob`].
47pub use gix_attributes as attributes;
48
49///
50pub mod normalize {
51    use std::path::PathBuf;
52
53    /// The error returned by [Pattern::normalize()](super::Pattern::normalize()).
54    #[derive(Debug, thiserror::Error)]
55    #[expect(missing_docs)]
56    pub enum Error {
57        #[error("The path '{}' is not inside of the worktree '{}'", path.display(), worktree_path.display())]
58        AbsolutePathOutsideOfWorktree { path: PathBuf, worktree_path: PathBuf },
59        #[error("The path '{}' leaves the repository", path.display())]
60        OutsideOfWorktree { path: PathBuf },
61    }
62}
63
64mod pattern;
65
66///
67pub mod search;
68
69///
70pub mod parse;
71
72/// Default settings for some fields of a [`Pattern`].
73///
74/// These can be used to represent `GIT_*_PATHSPECS` environment variables, for example.
75#[derive(Debug, Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
76pub struct Defaults {
77    /// The default signature.
78    pub signature: MagicSignature,
79    /// The default search-mode.
80    ///
81    /// Note that even if it's [`SearchMode::Literal`], the pathspecs will be parsed as usual, but matched verbatim afterwards.
82    ///
83    /// Note that pathspecs can override this the [`SearchMode::Literal`] variant with an explicit `:(glob)` prefix.
84    pub search_mode: SearchMode,
85    /// If set, the pathspec will not be parsed but used verbatim. Implies [`SearchMode::Literal`] for `search_mode`.
86    pub literal: bool,
87}
88
89///
90pub mod defaults;
91
92/// A lists of pathspec patterns, possibly from a file.
93///
94/// Pathspecs are generally relative to the root of the repository.
95#[derive(Debug, Clone)]
96pub struct Search {
97    /// Patterns and their associated data in the order they were loaded in or specified,
98    /// the line number in its source file or its sequence number (_`(pattern, value, line_number)`_).
99    ///
100    /// During matching, this order is reversed.
101    patterns: Vec<gix_glob::search::pattern::Mapping<search::Spec>>,
102
103    /// The synthetic pattern returned when an empty search matches everything.
104    /// Can't be static as it's not (always) `Send` due to `gix-attributes/?parallel`.
105    match_all: Pattern,
106
107    /// The path from which the patterns were read, or `None` if the patterns
108    /// don't originate in a file on disk.
109    pub source: Option<PathBuf>,
110
111    /// If `true`, this means all `patterns` are exclude patterns. This means that if there is no match
112    /// (which would exclude an item), we would actually match it for lack of exclusion.
113    all_patterns_are_excluded: bool,
114    /// The amount of bytes that are in common among all `patterns` and that aren't matched case-insensitively
115    common_prefix_len: usize,
116}
117
118/// The output of a pathspec [parsing][parse()] operation. It can be used to match against a one or more paths.
119#[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
120pub struct Pattern {
121    /// The path part of a pathspec, which is typically a path possibly mixed with glob patterns.
122    /// Note that it might be an empty string as well.
123    ///
124    /// For example, `:(top,literal,icase,attr,exclude)some/path` would yield `some/path`.
125    path: BString,
126    /// All magic signatures that were included in the pathspec.
127    pub signature: MagicSignature,
128    /// The search mode of the pathspec.
129    pub search_mode: SearchMode,
130    /// All attributes that were included in the `ATTR` part of the pathspec, if present.
131    ///
132    /// `:(attr:a=one b=):path` would yield attribute `a` and `b`.
133    pub attributes: Vec<gix_attributes::Assignment>,
134    /// If `true`, we are a special Nil pattern and always match.
135    nil: bool,
136    /// The length of bytes in `path` that belong to the prefix, which will always be matched case-sensitively
137    /// on case-sensitive filesystems.
138    ///
139    /// That way, even though pathspecs are applied from the top, we can emulate having changed directory into
140    /// a specific sub-directory in a case-sensitive file-system, even if the rest of the pathspec can be set to
141    /// match case-insensitively.
142    /// Is set by [Pattern::normalize()].
143    prefix_len: usize,
144}
145
146bitflags! {
147    /// Flags to represent 'magic signatures' which are parsed behind colons, like `:top:`.
148    #[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
149    pub struct MagicSignature: u32 {
150        /// Matches patterns from the root of the repository
151        const TOP = 1 << 0;
152        /// Matches patterns in case insensitive mode
153        const ICASE = 1 << 1;
154        /// Excludes the matching patterns from the previous results
155        const EXCLUDE = 1 << 2;
156        /// The pattern must match a directory, and not a file.
157        /// This is equivalent to how it's handled in `gix-glob`
158        const MUST_BE_DIR = 1 << 3;
159    }
160}
161
162/// Parts of [magic signatures][MagicSignature] which don't stack as they all configure
163/// the way path specs are matched.
164#[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
165pub enum SearchMode {
166    /// Expand special characters like `*` similar to how the shell would do it.
167    ///
168    /// See [`PathAwareGlob`](SearchMode::PathAwareGlob) for the alternative.
169    #[default]
170    ShellGlob,
171    /// Special characters in the pattern, like `*` or `?`, are treated literally, effectively turning off globbing.
172    Literal,
173    /// A single `*` will not match a `/` in the pattern, but a `**` will
174    PathAwareGlob,
175}
176
177/// Parse a git-style pathspec into a [`Pattern`],
178/// setting the given `default` values in case these aren't specified in `input`.
179///
180/// Note that empty [paths](Pattern::path) are allowed here, and generally some processing has to be performed.
181pub fn parse(input: &[u8], default: Defaults) -> Result<Pattern, parse::Error> {
182    Pattern::from_bytes(input, default)
183}