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