Coverage Report

Created: 2026-03-31 07:35

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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;