Skip to content

Commit f87477b

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 c32e22b commit f87477b

File tree

6 files changed

+198
-139
lines changed

6 files changed

+198
-139
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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` and `overrides`.
24+
/// `globals` contribute first and consist of all globally available, static files.
25+
pub fn new(
26+
globals: AttributeMatchGroup,
27+
overrides: AttributeMatchGroup,
28+
info_attributes: Option<PathBuf>,
29+
case: Case,
30+
source: Source,
31+
) -> Self {
32+
let stack = AttributeMatchGroup::default();
33+
Attributes {
34+
globals,
35+
stack,
36+
overrides,
37+
info_attributes,
38+
case,
39+
source,
40+
}
41+
}
42+
}
43+
44+
/// Builder
45+
impl Attributes {
46+
/// Set the case to use when matching attributes to paths.
47+
pub fn with_case(mut self, case: gix_glob::pattern::Case) -> Self {
48+
self.case = case;
49+
self
50+
}
51+
}

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

Lines changed: 4 additions & 133 deletions
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,
@@ -199,118 +185,3 @@ impl Ignore {
199185
Ok(())
200186
}
201187
}
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-
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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+
/// Attribute assignments passed as additions or adjustments to everything else,
22+
/// typically passed on the command-line.
23+
overrides: AttributeMatchGroup,
24+
/// The first time we push the root, we have to load additional information from this file if it exists along with the root attributes
25+
/// file if possible, and keep them there throughout.
26+
info_attributes: Option<PathBuf>,
27+
/// The case to use when matching directories as they are pushed onto the stack. We run them against the exclude engine
28+
/// to know if an entire path can be ignored as a parent directory is ignored.
29+
case: Case,
30+
/// Where to read `.gitattributes` data from.
31+
source: attributes::Source,
32+
}
33+
34+
///
35+
pub mod attributes;
36+
mod ignore;
37+
pub use ignore::Ignore;
38+
39+
/// Initialization
40+
impl State {
41+
/// Configure a state to be suitable for checking out files.
42+
pub fn for_checkout(unlink_on_collision: bool, attributes: Attributes) -> Self {
43+
State::CreateDirectoryAndAttributesStack {
44+
unlink_on_collision,
45+
#[cfg(debug_assertions)]
46+
test_mkdir_calls: 0,
47+
attributes,
48+
}
49+
}
50+
51+
/// Configure a state for adding files.
52+
pub fn for_add(attributes: Attributes, ignore: Ignore) -> Self {
53+
State::AttributesAndIgnoreStack { attributes, ignore }
54+
}
55+
56+
/// Configure a state for status retrieval.
57+
pub fn for_status(ignore: Ignore) -> Self {
58+
State::IgnoreStack(ignore)
59+
}
60+
}
61+
62+
/// Utilities
63+
impl State {
64+
/// Returns a vec of tuples of relative index paths along with the best usable OID for either ignore, attribute files or both.
65+
///
66+
/// - ignores entries which aren't blobs
67+
/// - ignores ignore entries which are not skip-worktree
68+
/// - within merges, picks 'our' stage both for ignore and attribute files.
69+
pub fn attribute_list_from_index(
70+
&self,
71+
index: &gix_index::State,
72+
paths: &gix_index::PathStorageRef,
73+
case: Case,
74+
) -> Vec<PathOidMapping> {
75+
let a1_backing;
76+
let a2_backing;
77+
let names = match self {
78+
State::IgnoreStack(v) => {
79+
a1_backing = [(v.exclude_file_name_for_directories.as_bytes().as_bstr(), true)];
80+
a1_backing.as_ref()
81+
}
82+
State::AttributesAndIgnoreStack { ignore, .. } => {
83+
a2_backing = [
84+
(ignore.exclude_file_name_for_directories.as_bytes().as_bstr(), true),
85+
(".gitattributes".into(), false),
86+
];
87+
a2_backing.as_ref()
88+
}
89+
State::CreateDirectoryAndAttributesStack { .. } => {
90+
a1_backing = [(".gitattributes".into(), true)];
91+
a1_backing.as_ref()
92+
}
93+
};
94+
95+
index
96+
.entries()
97+
.iter()
98+
.filter_map(move |entry| {
99+
let path = entry.path_in(paths);
100+
101+
// Stage 0 means there is no merge going on, stage 2 means it's 'our' side of the merge, but then
102+
// there won't be a stage 0.
103+
if entry.mode == gix_index::entry::Mode::FILE && (entry.stage() == 0 || entry.stage() == 2) {
104+
let basename = path
105+
.rfind_byte(b'/')
106+
.map(|pos| path[pos + 1..].as_bstr())
107+
.unwrap_or(path);
108+
let is_ignore = names.iter().find_map(|t| {
109+
match case {
110+
Case::Sensitive => basename == t.0,
111+
Case::Fold => basename.eq_ignore_ascii_case(t.0),
112+
}
113+
.then_some(t.1)
114+
})?;
115+
// See https://github.com/git/git/blob/master/dir.c#L912:L912
116+
if is_ignore && !entry.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE) {
117+
return None;
118+
}
119+
Some((path.to_owned(), entry.id))
120+
} else {
121+
None
122+
}
123+
})
124+
.collect()
125+
}
126+
127+
pub(crate) fn ignore_or_panic(&self) -> &Ignore {
128+
match self {
129+
State::IgnoreStack(v) => v,
130+
State::AttributesAndIgnoreStack { ignore, .. } => ignore,
131+
State::CreateDirectoryAndAttributesStack { .. } => {
132+
unreachable!("BUG: must not try to check excludes without it being setup")
133+
}
134+
}
135+
}
136+
}

gix-worktree/src/index/checkout.rs

Lines changed: 4 additions & 3 deletions
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

Lines changed: 2 additions & 2 deletions
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),

gix-worktree/tests/worktree/fs/cache/ignore.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ fn check_against_baseline() -> crate::Result {
106106
),
107107
);
108108
let paths_storage = index.take_path_backing();
109-
let attribute_files_in_index = state.build_attribute_list(&index, &paths_storage, case);
109+
let attribute_files_in_index = state.attribute_list_from_index(&index, &paths_storage, case);
110110
assert_eq!(
111111
attribute_files_in_index,
112112
vec![(

0 commit comments

Comments
 (0)