gix_pathspec/search/mod.rs
1use std::{borrow::Cow, path::Path};
2
3use bstr::{BStr, ByteSlice};
4use gix_error::Result;
5
6use crate::{MagicSignature, Pattern, Search};
7
8/// Describes a matching pattern within a search for ignored paths.
9#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
10pub struct Match<'a> {
11 /// The matching search specification, which contains the pathspec as well.
12 pub pattern: &'a Pattern,
13 /// The number of the sequence the matching pathspec was in, or the line of pathspec file it was read from if [Search::source] is not `None`.
14 pub sequence_number: usize,
15 /// How the pattern matched.
16 pub kind: MatchKind,
17}
18
19/// Describe how a pathspec pattern matched.
20#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
21pub enum MatchKind {
22 /// The match happened because there wasn't any pattern, which matches all, or because there was a nil pattern or one with an empty path.
23 /// Thus this is not a match by merit.
24 Always,
25 /// The first part of a pathspec matches, like `dir/` that matches `dir/a`.
26 Prefix,
27 /// The whole pathspec matched and used a wildcard match, like `a/*` matching `a/file`.
28 WildcardMatch,
29 /// The entire pathspec matched, letter by letter, e.g. `a/file` matching `a/file`.
30 Verbatim,
31}
32
33mod init;
34
35impl Match<'_> {
36 /// Return `true` if the pathspec that matched was negative, which excludes this item from the set.
37 pub fn is_excluded(&self) -> bool {
38 self.pattern.is_excluded()
39 }
40}
41
42/// Access
43impl Search {
44 /// Return an iterator over the patterns that participate in the search.
45 pub fn patterns(&self) -> impl ExactSizeIterator<Item = &Pattern> + '_ {
46 self.patterns.iter().map(|m| &m.value.pattern)
47 }
48
49 /// Return the portion of the prefix among all of the pathspecs involved in this search, or an empty string if
50 /// there is none. It doesn't have to end at a directory boundary though, nor does it denote a directory.
51 ///
52 /// Note that the common_prefix is always matched case-sensitively, and it is useful to skip large portions of input.
53 /// Further, excluded pathspecs don't participate which makes this common prefix inclusive. To work correctly though,
54 /// one will have to additionally match paths that have the common prefix with that pathspec itself to assure it is
55 /// not excluded.
56 pub fn common_prefix(&self) -> &BStr {
57 self.patterns
58 .iter()
59 .find(|p| !p.value.pattern.is_excluded())
60 .map_or("".into(), |m| m.value.pattern.path[..self.common_prefix_len].as_bstr())
61 }
62
63 /// Returns a guaranteed-to-be-directory that is shared across all pathspecs, in its repository-relative form.
64 /// Thus to be valid, it must be joined with the worktree root.
65 /// The prefix is the CWD within a worktree passed when [normalizing](crate::Pattern::normalize) the pathspecs.
66 ///
67 /// Note that it may well be that the directory isn't available even though there is a [`common_prefix()`](Self::common_prefix),
68 /// as they are not quire the same.
69 ///
70 /// See also: [`maybe_prefix_directory()`](Self::longest_common_directory).
71 /// Returns an error if the directory cannot be represented as a platform path.
72 pub fn prefix_directory(&self) -> Result<Cow<'_, Path>> {
73 gix_path::from_bstr(
74 self.patterns
75 .iter()
76 .find(|p| !p.value.pattern.is_excluded())
77 .map_or("".into(), |m| m.value.pattern.prefix_directory()),
78 )
79 }
80
81 /// Return the longest possible common directory that is shared across all non-exclusive pathspecs.
82 /// It must be tested for existence by joining it with a suitable root before being able to use it.
83 /// Note that if it is returned, it's guaranteed to be longer than the [prefix-directory](Self::prefix_directory).
84 ///
85 /// Returns `None` if the returned directory would be empty, or if all pathspecs are exclusive.
86 /// Returns an error if the directory cannot be represented as a platform path.
87 pub fn longest_common_directory(&self) -> Result<Option<Cow<'_, Path>>> {
88 let Some(first_non_excluded) = self.patterns.iter().find(|p| !p.value.pattern.is_excluded()) else {
89 return Ok(None);
90 };
91 let common_prefix = first_non_excluded.value.pattern.path[..self.common_prefix_len].as_bstr();
92 let stripped_prefix = if first_non_excluded
93 .value
94 .pattern
95 .signature
96 .contains(MagicSignature::MUST_BE_DIR)
97 {
98 common_prefix
99 } else {
100 let Some(last_slash) = common_prefix.rfind_byte(b'/') else {
101 return Ok(None);
102 };
103 common_prefix[..last_slash].as_bstr()
104 };
105 gix_path::from_bstr(stripped_prefix).map(Some)
106 }
107}
108
109#[derive(Default, Clone, Debug)]
110pub(crate) struct Spec {
111 pub pattern: Pattern,
112 pub attrs_match: Option<gix_attributes::search::Outcome>,
113}
114
115mod matching;