gix_worktree/stack/state/mod.rs
1use bstr::{BString, ByteSlice};
2use gix_glob::pattern::Case;
3
4use crate::{stack::State, PathIdMapping};
5
6#[cfg(feature = "attributes")]
7type AttributeMatchGroup = gix_attributes::Search;
8type IgnoreMatchGroup = gix_ignore::Search;
9
10/// State related to attributes associated with files in the repository.
11#[derive(Default, Clone)]
12#[cfg(feature = "attributes")]
13pub struct Attributes {
14 /// Attribute patterns which aren't tied to the repository root, hence are global, they contribute first.
15 globals: AttributeMatchGroup,
16 /// Attribute patterns that match the currently set directory (in the stack).
17 ///
18 /// Note that the root-level file is always loaded, if present, followed by, the `$GIT_DIR/info/attributes`, if present, based
19 /// on the location of the `info_attributes` file.
20 stack: AttributeMatchGroup,
21 /// The first time we push the root, we have to load additional information from this file if it exists along with the root attributes
22 /// file if possible, and keep them there throughout.
23 info_attributes: Option<std::path::PathBuf>,
24 /// A lookup table to accelerate searches.
25 collection: gix_attributes::search::MetadataCollection,
26 /// Where to read `.gitattributes` data from.
27 source: attributes::Source,
28}
29
30/// State related to the exclusion of files, supporting static overrides and globals, along with a stack of dynamically read
31/// ignore files from disk or from the index each time the directory changes.
32#[derive(Default, Clone)]
33#[allow(unused)]
34pub struct Ignore {
35 /// Ignore patterns passed as overrides to everything else, typically passed on the command-line and the first patterns to
36 /// be consulted.
37 overrides: IgnoreMatchGroup,
38 /// Ignore patterns that match the currently set director (in the stack), which is pushed and popped as needed.
39 stack: IgnoreMatchGroup,
40 /// Ignore patterns which aren't tied to the repository root, hence are global. They are consulted last.
41 globals: IgnoreMatchGroup,
42 /// A matching stack of pattern indices which is empty if we have just been initialized to indicate that the
43 /// currently set directory had a pattern matched. Note that this one could be negated.
44 /// (index into match groups, index into list of pattern lists, index into pattern list)
45 matched_directory_patterns_stack: Vec<Option<(usize, usize, usize)>>,
46 /// The name of the file to look for in directories.
47 pub(crate) exclude_file_name_for_directories: BString,
48 /// Where to read ignore files from
49 source: ignore::Source,
50 /// Control how to parse ignore files.
51 parse: gix_ignore::search::Ignore,
52}
53
54///
55#[cfg(feature = "attributes")]
56pub mod attributes;
57///
58pub mod ignore;
59
60/// Initialization
61impl State {
62 /// Configure a state to be suitable for checking out files, which only needs access to attribute files read from the index.
63 #[cfg(feature = "attributes")]
64 pub fn for_checkout(
65 unlink_on_collision: bool,
66 validate: gix_validate::path::component::Options,
67 attributes: Attributes,
68 ) -> Self {
69 State::CreateDirectoryAndAttributesStack {
70 unlink_on_collision,
71 validate,
72 attributes,
73 }
74 }
75
76 /// Configure a state for adding files, with support for ignore files and attribute files.
77 #[cfg(feature = "attributes")]
78 pub fn for_add(attributes: Attributes, ignore: Ignore) -> Self {
79 State::AttributesAndIgnoreStack { attributes, ignore }
80 }
81}
82
83/// Utilities
84impl State {
85 /// Returns a vec of tuples of relative index paths along with the best usable blob OID for
86 /// either *ignore* or *attribute* files or both. This allows files to be accessed directly from
87 /// the object database without the need for a worktree checkout.
88 ///
89 /// Note that this method…
90 /// - ignores entries which aren't blobs.
91 /// - ignores ignore entries which are not skip-worktree.
92 /// - within merges, picks 'our' stage both for *ignore* and *attribute* files.
93 ///
94 /// * `index` is where we look for suitable files by path in order to obtain their blob hash.
95 /// * `paths` is the indices storage backend for paths.
96 /// * `case` determines if the search for files should be case-sensitive or not.
97 pub fn id_mappings_from_index(
98 &self,
99 index: &gix_index::State,
100 paths: &gix_index::PathStorageRef,
101 case: Case,
102 ) -> Vec<PathIdMapping> {
103 let a1_backing;
104 #[cfg(feature = "attributes")]
105 let a2_backing;
106 let names = match self {
107 State::IgnoreStack(ignore) => {
108 a1_backing = [(
109 ignore.exclude_file_name_for_directories.as_bytes().as_bstr(),
110 Some(ignore.source),
111 )];
112 a1_backing.as_ref()
113 }
114 #[cfg(feature = "attributes")]
115 State::AttributesAndIgnoreStack { ignore, .. } => {
116 a2_backing = [
117 (
118 ignore.exclude_file_name_for_directories.as_bytes().as_bstr(),
119 Some(ignore.source),
120 ),
121 (".gitattributes".into(), None),
122 ];
123 a2_backing.as_ref()
124 }
125 #[cfg(feature = "attributes")]
126 State::CreateDirectoryAndAttributesStack { .. } | State::AttributesStack(_) => {
127 a1_backing = [(".gitattributes".into(), None)];
128 a1_backing.as_ref()
129 }
130 };
131
132 index
133 .entries()
134 .iter()
135 .filter_map(move |entry| {
136 let path = entry.path_in(paths);
137
138 // Stage 0 means there is no merge going on, stage 2 means it's 'our' side of the merge, but then
139 // there won't be a stage 0.
140 if entry.mode == gix_index::entry::Mode::FILE && (entry.stage_raw() == 0 || entry.stage_raw() == 2) {
141 let basename = path.rfind_byte(b'/').map_or(path, |pos| path[pos + 1..].as_bstr());
142 let ignore_source = names.iter().find_map(|t| {
143 match case {
144 Case::Sensitive => basename == t.0,
145 Case::Fold => basename.eq_ignore_ascii_case(t.0),
146 }
147 .then_some(t.1)
148 })?;
149 if let Some(source) = ignore_source {
150 match source {
151 ignore::Source::IdMapping => {}
152 ignore::Source::WorktreeThenIdMappingIfNotSkipped => {
153 // See https://github.com/git/git/blob/master/dir.c#L912:L912
154 if !entry.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE) {
155 return None;
156 }
157 }
158 }
159 }
160 Some((path.to_owned(), entry.id))
161 } else {
162 None
163 }
164 })
165 .collect()
166 }
167
168 pub(crate) fn ignore_or_panic(&self) -> &Ignore {
169 match self {
170 State::IgnoreStack(v) => v,
171 #[cfg(feature = "attributes")]
172 State::AttributesAndIgnoreStack { ignore, .. } => ignore,
173 #[cfg(feature = "attributes")]
174 State::AttributesStack(_) | State::CreateDirectoryAndAttributesStack { .. } => {
175 unreachable!("BUG: must not try to check excludes without it being setup")
176 }
177 }
178 }
179
180 #[cfg(feature = "attributes")]
181 pub(crate) fn attributes_or_panic(&self) -> &Attributes {
182 match self {
183 State::AttributesStack(attributes)
184 | State::AttributesAndIgnoreStack { attributes, .. }
185 | State::CreateDirectoryAndAttributesStack { attributes, .. } => attributes,
186 State::IgnoreStack(_) => {
187 unreachable!("BUG: must not try to check excludes without it being setup")
188 }
189 }
190 }
191}