Skip to content

Commit ef1a499

Browse files
committed
First steps of attribute cache initialization.
This is done to fully understand how attribute initialization works with the current architecture, while taking care of its particular difference compared to the handling of exclude patterns.
1 parent 38b87c4 commit ef1a499

File tree

6 files changed

+198
-145
lines changed

6 files changed

+198
-145
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use crate::fs::cache::state::{AttributeMatchGroup, Attributes};
2+
use gix_glob::pattern::Case;
3+
use std::path::PathBuf;
4+
5+
/// Decide where to read `.gitattributes` files from.
6+
#[derive(Default, Debug, Clone, Copy)]
7+
pub enum Source {
8+
/// Retrieve attribute files from an attribute list, see
9+
/// [State::attribute_list_from_index()][crate::fs::cache::State::attribute_list_from_index()].
10+
///
11+
/// The attribute list is typically produced from an index. If a tree should be the source, build an attribute list
12+
/// from a tree instead.
13+
#[default]
14+
AttributeList,
15+
/// Read from an attribute list and if not present, read from the worktree.
16+
AttributeListThenWorktree,
17+
/// Read from the worktree and if not present, read from the attribute list.
18+
WorktreeThenAttributeList,
19+
}
20+
21+
/// Initialization
22+
impl Attributes {
23+
/// Create a new instance from an attribute match group that represents `globals`.
24+
/// `globals` contribute first and consist of all globally available, static files.
25+
pub fn new(
26+
globals: AttributeMatchGroup,
27+
info_attributes: Option<PathBuf>,
28+
case: Case,
29+
source: Source,
30+
collection: gix_attributes::search::MetadataCollection,
31+
) -> Self {
32+
Attributes {
33+
globals,
34+
stack: Default::default(),
35+
info_attributes,
36+
case,
37+
source,
38+
collection,
39+
}
40+
}
41+
}
42+
43+
/// Builder
44+
impl Attributes {
45+
/// Set the case to use when matching attributes to paths.
46+
pub fn with_case(mut self, case: gix_glob::pattern::Case) -> Self {
47+
self.case = case;
48+
self
49+
}
50+
}

gix-worktree/src/fs/cache/state.rs renamed to gix-worktree/src/fs/cache/state/ignore.rs

+6-139
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,9 @@
1-
use std::path::Path;
2-
1+
use crate::fs::cache::state::IgnoreMatchGroup;
2+
use crate::fs::PathOidMapping;
33
use bstr::{BStr, BString, ByteSlice};
44
use gix_glob::pattern::Case;
55
use gix_hash::oid;
6-
7-
use crate::fs::{cache::State, PathOidMapping};
8-
9-
type AttributeMatchGroup = gix_attributes::Search;
10-
type IgnoreMatchGroup = gix_ignore::Search;
11-
12-
/// State related to attributes associated with files in the repository.
13-
#[derive(Default, Clone)]
14-
#[allow(unused)]
15-
pub struct Attributes {
16-
/// Attribute patterns that match the currently set directory (in the stack).
17-
pub stack: AttributeMatchGroup,
18-
/// Attribute patterns which aren't tied to the repository root, hence are global. They are consulted last.
19-
pub globals: AttributeMatchGroup,
20-
}
6+
use std::path::Path;
217

228
/// State related to the exclusion of files.
239
#[derive(Default, Clone)]
@@ -35,7 +21,7 @@ pub struct Ignore {
3521
/// (index into match groups, index into list of pattern lists, index into pattern list)
3622
matched_directory_patterns_stack: Vec<Option<(usize, usize, usize)>>,
3723
/// The name of the file to look for in directories.
38-
exclude_file_name_for_directories: BString,
24+
pub(crate) exclude_file_name_for_directories: BString,
3925
/// The case to use when matching directories as they are pushed onto the stack. We run them against the exclude engine
4026
/// to know if an entire path can be ignored as a parent directory is ignored.
4127
case: Case,
@@ -183,12 +169,8 @@ impl Ignore {
183169
let ignore_blob = find(&attribute_files_in_index[idx].1, buf)
184170
.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
185171
let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned());
186-
gix_glob::search::add_patterns_buffer(
187-
&mut self.stack.patterns,
188-
ignore_blob.data,
189-
ignore_path,
190-
Some(root),
191-
);
172+
self.stack
173+
.add_patterns_buffer(ignore_blob.data, ignore_path, Some(root));
192174
}
193175
Err(_) => {
194176
// Need one stack level per component so push and pop matches.
@@ -199,118 +181,3 @@ impl Ignore {
199181
Ok(())
200182
}
201183
}
202-
203-
impl Attributes {
204-
/// Create a new instance from an attribute match group that represents `globals`.
205-
///
206-
/// A stack of attributes will be applied on top of it later.
207-
pub fn new(globals: AttributeMatchGroup) -> Self {
208-
Attributes {
209-
globals,
210-
stack: Default::default(),
211-
}
212-
}
213-
}
214-
215-
impl From<AttributeMatchGroup> for Attributes {
216-
fn from(group: AttributeMatchGroup) -> Self {
217-
Attributes::new(group)
218-
}
219-
}
220-
221-
impl State {
222-
/// Configure a state to be suitable for checking out files.
223-
pub fn for_checkout(unlink_on_collision: bool, attributes: Attributes) -> Self {
224-
State::CreateDirectoryAndAttributesStack {
225-
unlink_on_collision,
226-
#[cfg(debug_assertions)]
227-
test_mkdir_calls: 0,
228-
attributes,
229-
}
230-
}
231-
232-
/// Configure a state for adding files.
233-
pub fn for_add(attributes: Attributes, ignore: Ignore) -> Self {
234-
State::AttributesAndIgnoreStack { attributes, ignore }
235-
}
236-
237-
/// Configure a state for status retrieval.
238-
pub fn for_status(ignore: Ignore) -> Self {
239-
State::IgnoreStack(ignore)
240-
}
241-
}
242-
243-
impl State {
244-
/// Returns a vec of tuples of relative index paths along with the best usable OID for either ignore, attribute files or both.
245-
///
246-
/// - ignores entries which aren't blobs
247-
/// - ignores ignore entries which are not skip-worktree
248-
/// - within merges, picks 'our' stage both for ignore and attribute files.
249-
pub fn build_attribute_list(
250-
&self,
251-
index: &gix_index::State,
252-
paths: &gix_index::PathStorageRef,
253-
case: Case,
254-
) -> Vec<PathOidMapping> {
255-
let a1_backing;
256-
let a2_backing;
257-
let names = match self {
258-
State::IgnoreStack(v) => {
259-
a1_backing = [(v.exclude_file_name_for_directories.as_bytes().as_bstr(), true)];
260-
a1_backing.as_ref()
261-
}
262-
State::AttributesAndIgnoreStack { ignore, .. } => {
263-
a2_backing = [
264-
(ignore.exclude_file_name_for_directories.as_bytes().as_bstr(), true),
265-
(".gitattributes".into(), false),
266-
];
267-
a2_backing.as_ref()
268-
}
269-
State::CreateDirectoryAndAttributesStack { .. } => {
270-
a1_backing = [(".gitattributes".into(), true)];
271-
a1_backing.as_ref()
272-
}
273-
};
274-
275-
index
276-
.entries()
277-
.iter()
278-
.filter_map(move |entry| {
279-
let path = entry.path_in(paths);
280-
281-
// Stage 0 means there is no merge going on, stage 2 means it's 'our' side of the merge, but then
282-
// there won't be a stage 0.
283-
if entry.mode == gix_index::entry::Mode::FILE && (entry.stage() == 0 || entry.stage() == 2) {
284-
let basename = path
285-
.rfind_byte(b'/')
286-
.map(|pos| path[pos + 1..].as_bstr())
287-
.unwrap_or(path);
288-
let is_ignore = names.iter().find_map(|t| {
289-
match case {
290-
Case::Sensitive => basename == t.0,
291-
Case::Fold => basename.eq_ignore_ascii_case(t.0),
292-
}
293-
.then_some(t.1)
294-
})?;
295-
// See https://github.com/git/git/blob/master/dir.c#L912:L912
296-
if is_ignore && !entry.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE) {
297-
return None;
298-
}
299-
Some((path.to_owned(), entry.id))
300-
} else {
301-
None
302-
}
303-
})
304-
.collect()
305-
}
306-
307-
pub(crate) fn ignore_or_panic(&self) -> &Ignore {
308-
match self {
309-
State::IgnoreStack(v) => v,
310-
State::AttributesAndIgnoreStack { ignore, .. } => ignore,
311-
State::CreateDirectoryAndAttributesStack { .. } => {
312-
unreachable!("BUG: must not try to check excludes without it being setup")
313-
}
314-
}
315-
}
316-
}
+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
use bstr::ByteSlice;
2+
use gix_glob::pattern::Case;
3+
use std::path::PathBuf;
4+
5+
use crate::fs::{cache::State, PathOidMapping};
6+
7+
type AttributeMatchGroup = gix_attributes::Search;
8+
type IgnoreMatchGroup = gix_ignore::Search;
9+
10+
/// State related to attributes associated with files in the repository.
11+
#[derive(Default, Clone)]
12+
#[allow(unused)]
13+
pub 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<PathBuf>,
24+
/// A lookup table to accelerate searches.
25+
collection: gix_attributes::search::MetadataCollection,
26+
/// The case to use when matching directories as they are pushed onto the stack. We run them against the exclude engine
27+
/// to know if an entire path can be ignored as a parent directory is ignored.
28+
case: Case,
29+
/// Where to read `.gitattributes` data from.
30+
source: attributes::Source,
31+
}
32+
33+
///
34+
pub mod attributes;
35+
mod ignore;
36+
pub use ignore::Ignore;
37+
38+
/// Initialization
39+
impl State {
40+
/// Configure a state to be suitable for checking out files.
41+
pub fn for_checkout(unlink_on_collision: bool, attributes: Attributes) -> Self {
42+
State::CreateDirectoryAndAttributesStack {
43+
unlink_on_collision,
44+
#[cfg(debug_assertions)]
45+
test_mkdir_calls: 0,
46+
attributes,
47+
}
48+
}
49+
50+
/// Configure a state for adding files.
51+
pub fn for_add(attributes: Attributes, ignore: Ignore) -> Self {
52+
State::AttributesAndIgnoreStack { attributes, ignore }
53+
}
54+
55+
/// Configure a state for status retrieval.
56+
pub fn for_status(ignore: Ignore) -> Self {
57+
State::IgnoreStack(ignore)
58+
}
59+
}
60+
61+
/// Utilities
62+
impl State {
63+
/// Returns a vec of tuples of relative index paths along with the best usable OID for either ignore, attribute files or both.
64+
///
65+
/// - ignores entries which aren't blobs
66+
/// - ignores ignore entries which are not skip-worktree
67+
/// - within merges, picks 'our' stage both for ignore and attribute files.
68+
pub fn attribute_list_from_index(
69+
&self,
70+
index: &gix_index::State,
71+
paths: &gix_index::PathStorageRef,
72+
case: Case,
73+
) -> Vec<PathOidMapping> {
74+
let a1_backing;
75+
let a2_backing;
76+
let names = match self {
77+
State::IgnoreStack(v) => {
78+
a1_backing = [(v.exclude_file_name_for_directories.as_bytes().as_bstr(), true)];
79+
a1_backing.as_ref()
80+
}
81+
State::AttributesAndIgnoreStack { ignore, .. } => {
82+
a2_backing = [
83+
(ignore.exclude_file_name_for_directories.as_bytes().as_bstr(), true),
84+
(".gitattributes".into(), false),
85+
];
86+
a2_backing.as_ref()
87+
}
88+
State::CreateDirectoryAndAttributesStack { .. } => {
89+
a1_backing = [(".gitattributes".into(), true)];
90+
a1_backing.as_ref()
91+
}
92+
};
93+
94+
index
95+
.entries()
96+
.iter()
97+
.filter_map(move |entry| {
98+
let path = entry.path_in(paths);
99+
100+
// Stage 0 means there is no merge going on, stage 2 means it's 'our' side of the merge, but then
101+
// there won't be a stage 0.
102+
if entry.mode == gix_index::entry::Mode::FILE && (entry.stage() == 0 || entry.stage() == 2) {
103+
let basename = path
104+
.rfind_byte(b'/')
105+
.map(|pos| path[pos + 1..].as_bstr())
106+
.unwrap_or(path);
107+
let is_ignore = names.iter().find_map(|t| {
108+
match case {
109+
Case::Sensitive => basename == t.0,
110+
Case::Fold => basename.eq_ignore_ascii_case(t.0),
111+
}
112+
.then_some(t.1)
113+
})?;
114+
// See https://github.com/git/git/blob/master/dir.c#L912:L912
115+
if is_ignore && !entry.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE) {
116+
return None;
117+
}
118+
Some((path.to_owned(), entry.id))
119+
} else {
120+
None
121+
}
122+
})
123+
.collect()
124+
}
125+
126+
pub(crate) fn ignore_or_panic(&self) -> &Ignore {
127+
match self {
128+
State::IgnoreStack(v) => v,
129+
State::AttributesAndIgnoreStack { ignore, .. } => ignore,
130+
State::CreateDirectoryAndAttributesStack { .. } => {
131+
unreachable!("BUG: must not try to check excludes without it being setup")
132+
}
133+
}
134+
}
135+
}

gix-worktree/src/index/checkout.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#![allow(missing_docs)]
2+
23
use bstr::BString;
34

45
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
@@ -58,8 +59,8 @@ pub struct Options {
5859
///
5960
/// Default true.
6061
pub check_stat: bool,
61-
/// A group of attribute patterns that are applied globally, i.e. aren't rooted within the repository itself.
62-
pub attribute_globals: gix_attributes::Search,
62+
/// A stack of attributes to use with the filesystem cache to use as driver for filters.
63+
pub attributes: crate::fs::cache::state::Attributes,
6364
}
6465

6566
impl Default for Options {
@@ -72,7 +73,7 @@ impl Default for Options {
7273
trust_ctime: true,
7374
check_stat: true,
7475
overwrite_existing: false,
75-
attribute_globals: Default::default(),
76+
attributes: Default::default(),
7677
}
7778
}
7879
}

gix-worktree/src/index/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ where
5959
None,
6060
);
6161

62-
let state = fs::cache::State::for_checkout(options.overwrite_existing, options.attribute_globals.clone().into());
63-
let attribute_files = state.build_attribute_list(index, paths, case);
62+
let state = fs::cache::State::for_checkout(options.overwrite_existing, options.attributes.clone().with_case(case));
63+
let attribute_files = state.attribute_list_from_index(index, paths, case);
6464
let mut ctx = chunk::Context {
6565
buf: Vec::new(),
6666
path_cache: fs::Cache::new(dir, state, case, Vec::with_capacity(512), attribute_files),

0 commit comments

Comments
 (0)