1use std::cell::RefCell;
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15
16use crate::attributes::{
17 parse_gitattributes_file_content, parse_gitattributes_file_content_with_base,
18 ParsedGitAttributes, MAX_ATTR_FILE_BYTES,
19};
20use crate::config::parse_path;
21use crate::config::ConfigSet;
22use crate::crlf::{self, AttrRule, ConversionConfig, FileAttrs};
23use crate::environment::Environment;
24use crate::error::{Error, Result};
25use crate::filter_process::FilterProcessState;
26use crate::ignore::IgnoreMatcher;
27use crate::index::Index;
28use crate::objects::ObjectId;
29use crate::odb::Odb;
30use crate::repo::Repository;
31use crate::repo_caches::RepoCaches;
32
33#[cfg(test)]
39pub mod file_load_counters {
40 use std::cell::RefCell;
41 use std::collections::HashMap;
42 use std::path::{Path, PathBuf};
43
44 thread_local! {
45 static MAP: RefCell<HashMap<PathBuf, u32>> = RefCell::new(HashMap::new());
46 }
47
48 pub fn reset() {
50 MAP.with(|m| m.borrow_mut().clear());
51 }
52
53 pub fn record(path: &Path) {
55 let key = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
56 MAP.with(|m| *m.borrow_mut().entry(key).or_insert(0) += 1);
57 }
58
59 #[must_use]
61 pub fn max_reads_per_file() -> u32 {
62 MAP.with(|m| m.borrow().values().copied().max().unwrap_or(0))
63 }
64
65 #[must_use]
67 pub fn reads_for(path: &Path) -> u32 {
68 let key = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
69 MAP.with(|m| m.borrow().get(&key).copied().unwrap_or(0))
70 }
71}
72
73fn record_attr_read(path: &Path) {
74 #[cfg(test)]
75 file_load_counters::record(path);
76 let _ = path;
77}
78
79fn record_ignore_read(path: &Path) {
80 #[cfg(test)]
81 file_load_counters::record(path);
82 let _ = path;
83}
84
85pub struct WorktreeRules {
87 config: Arc<ConfigSet>,
88 conversion: ConversionConfig,
89 ignore: RefCell<IgnoreMatcher>,
90 attrs: AttributeState,
91 caches: Arc<RepoCaches>,
92}
93
94struct AttributeState {
95 work_tree: PathBuf,
96 _git_dir: PathBuf,
97 odb: Odb,
98 gitattributes_index: HashMap<Vec<u8>, ObjectId>,
100 gitattributes_index_only: bool,
102 stack_prefix: ParsedGitAttributes,
104 info_rules: ParsedGitAttributes,
106 dir_fragment: RefCell<HashMap<String, ParsedGitAttributes>>,
107 stack_without_info_by_dir: RefCell<HashMap<String, ParsedGitAttributes>>,
108}
109
110impl WorktreeRules {
111 pub fn from_repository(repo: &Repository, index: &Index) -> Result<Self> {
117 let config = repo.config()?.clone();
118 Self::from_parts(repo, index, config)
119 }
120
121 pub fn from_parts(repo: &Repository, index: &Index, config: Arc<ConfigSet>) -> Result<Self> {
127 Self::from_parts_with_attribute_source(repo, index, config, false)
128 }
129
130 pub(crate) fn from_parts_index_attributes_only(
132 repo: &Repository,
133 index: &Index,
134 config: Arc<ConfigSet>,
135 ) -> Result<Self> {
136 Self::from_parts_with_attribute_source(repo, index, config, true)
137 }
138
139 fn from_parts_with_attribute_source(
140 repo: &Repository,
141 index: &Index,
142 config: Arc<ConfigSet>,
143 gitattributes_index_only: bool,
144 ) -> Result<Self> {
145 let work_tree = repo
146 .work_tree()
147 .ok_or(Error::RequiresWorkTree)?
148 .to_path_buf();
149 let conversion = ConversionConfig::from_config_with_runner(&config, repo.command_runner());
150 let ignore = RefCell::new(IgnoreMatcher::from_repository(repo)?);
151 let (stack_prefix, info_rules) = load_attribute_stack_prefix_and_info(
152 repo,
153 &work_tree,
154 &config,
155 index,
156 repo.odb(),
157 gitattributes_index_only,
158 )?;
159 let attrs = AttributeState {
160 work_tree,
161 _git_dir: repo.git_dir().to_path_buf(),
162 odb: repo.odb().clone(),
163 gitattributes_index: gitattributes_index_map(index),
164 gitattributes_index_only,
165 stack_prefix,
166 info_rules,
167 dir_fragment: RefCell::new(HashMap::new()),
168 stack_without_info_by_dir: RefCell::new(HashMap::new()),
169 };
170 Ok(Self {
171 config,
172 conversion,
173 ignore,
174 attrs,
175 caches: Arc::clone(repo.caches()),
176 })
177 }
178
179 pub fn from_checkout_prospective_index(
189 repo: &Repository,
190 prospective_index: &Index,
191 config: Arc<ConfigSet>,
192 ) -> Result<Self> {
193 Self::from_parts_index_attributes_only(repo, prospective_index, config)
194 }
195
196 #[must_use]
198 pub fn filter_process(&self) -> &FilterProcessState {
199 self.caches.filters()
200 }
201
202 #[must_use]
204 pub fn config(&self) -> &ConfigSet {
205 &self.config
206 }
207
208 #[must_use]
210 pub fn config_arc(&self) -> Arc<ConfigSet> {
211 Arc::clone(&self.config)
212 }
213
214 #[must_use]
216 pub fn conversion(&self) -> &ConversionConfig {
217 &self.conversion
218 }
219
220 pub fn ignore(&self) -> std::cell::Ref<'_, IgnoreMatcher> {
222 self.ignore.borrow()
223 }
224
225 pub fn ignore_mut(&self) -> std::cell::RefMut<'_, IgnoreMatcher> {
227 self.ignore.borrow_mut()
228 }
229
230 #[must_use]
232 pub fn attribute_rules_for_path(&self, rel_path: &str) -> Vec<AttrRule> {
233 let parent = parent_dir_key(rel_path);
234 self.attrs.merged_for_dir(&parent).rules
235 }
236
237 #[must_use]
239 pub fn file_attrs(&self, rel_path: &str, is_dir: bool) -> FileAttrs {
240 let parent = parent_dir_key(rel_path);
241 let parsed = self.attrs.merged_for_dir(&parent);
242 crlf::file_attrs_from_gitattributes(
243 &parsed.rules,
244 &parsed.macros,
245 rel_path,
246 is_dir,
247 &self.config,
248 )
249 }
250}
251
252impl AttributeState {
253 fn merged_for_dir(&self, dir: &str) -> ParsedGitAttributes {
254 let mut merged = self.stack_without_info_for_dir(dir);
255 append_parsed(&mut merged, self.info_rules.clone(), true);
256 merged
257 }
258
259 fn stack_without_info_for_dir(&self, dir: &str) -> ParsedGitAttributes {
260 if let Some(hit) = self.stack_without_info_by_dir.borrow().get(dir).cloned() {
261 return hit;
262 }
263 let stack = if dir.is_empty() {
264 self.stack_prefix.clone()
265 } else {
266 let parent = parent_of_dir(dir);
267 let mut parsed = self.stack_without_info_for_dir(&parent);
268 append_parsed(&mut parsed, self.load_dir_fragment(dir), false);
269 parsed
270 };
271 self.stack_without_info_by_dir
272 .borrow_mut()
273 .insert(dir.to_owned(), stack.clone());
274 stack
275 }
276
277 fn load_dir_fragment(&self, dir: &str) -> ParsedGitAttributes {
278 if let Some(hit) = self.dir_fragment.borrow().get(dir).cloned() {
279 return hit;
280 }
281 let mut parsed = ParsedGitAttributes::default();
282 let ga_stored = if dir.is_empty() {
283 ".gitattributes".to_owned()
284 } else {
285 format!("{dir}/.gitattributes")
286 };
287 let key = crate::repo_relpath::storage_bytes(&ga_stored).into_owned();
288 if !self.gitattributes_index_only {
289 let wt_ga = if dir.is_empty() {
290 self.work_tree.join(".gitattributes")
291 } else {
292 self.work_tree.join(dir).join(".gitattributes")
293 };
294 if let Some(content) = read_worktree_gitattributes(&wt_ga) {
295 if content.len() <= MAX_ATTR_FILE_BYTES {
296 let mut p =
297 parse_gitattributes_file_content_with_base(&content, &ga_stored, dir);
298 parsed.rules.append(&mut p.rules);
299 parsed.warnings.append(&mut p.warnings);
300 }
301 self.dir_fragment
302 .borrow_mut()
303 .insert(dir.to_owned(), parsed.clone());
304 return parsed;
305 }
306 }
307 if let Some(oid) = self.gitattributes_index.get(&key) {
308 if let Ok(obj) = self.odb.read(oid) {
309 if let Ok(content) = String::from_utf8(obj.data) {
310 if content.len() <= MAX_ATTR_FILE_BYTES {
311 let mut p =
312 parse_gitattributes_file_content_with_base(&content, &ga_stored, dir);
313 parsed.rules.append(&mut p.rules);
314 parsed.warnings.append(&mut p.warnings);
315 }
316 }
317 }
318 }
319 self.dir_fragment
320 .borrow_mut()
321 .insert(dir.to_owned(), parsed.clone());
322 parsed
323 }
324}
325
326fn append_parsed(
327 merged: &mut ParsedGitAttributes,
328 mut parsed: ParsedGitAttributes,
329 take_macros: bool,
330) {
331 merged.rules.append(&mut parsed.rules);
332 if take_macros {
333 merged.macros.defs.extend(parsed.macros.defs.drain());
334 }
335 merged.warnings.append(&mut parsed.warnings);
336}
337
338fn parent_dir_key(rel_path: &str) -> String {
339 Path::new(rel_path)
340 .parent()
341 .map(|p| p.to_string_lossy().replace('\\', "/"))
342 .filter(|s| !s.is_empty())
343 .unwrap_or_default()
344}
345
346fn parent_of_dir(dir: &str) -> String {
347 Path::new(dir)
348 .parent()
349 .map(|p| p.to_string_lossy().replace('\\', "/"))
350 .filter(|s| !s.is_empty())
351 .unwrap_or_default()
352}
353
354fn load_attribute_stack_prefix_and_info(
355 repo: &Repository,
356 work_tree: &Path,
357 config: &ConfigSet,
358 index: &Index,
359 odb: &Odb,
360 gitattributes_index_only: bool,
361) -> Result<(ParsedGitAttributes, ParsedGitAttributes)> {
362 let mut stack_prefix = ParsedGitAttributes::default();
363 if let Some(g) = global_attributes_path(repo.environment(), config)? {
364 if let Some(content) = read_worktree_gitattributes(&g) {
365 if content.len() <= MAX_ATTR_FILE_BYTES {
366 let p = parse_gitattributes_file_content(&content, g.to_string_lossy().as_ref());
367 append_parsed(&mut stack_prefix, p, true);
368 }
369 }
370 }
371 if gitattributes_index_only {
372 if let Some(entry) = index.get(b".gitattributes", 0) {
373 if let Ok(obj) = odb.read(&entry.oid) {
374 if let Ok(content) = String::from_utf8(obj.data) {
375 if content.len() <= MAX_ATTR_FILE_BYTES {
376 let p = parse_gitattributes_file_content(&content, ".gitattributes");
377 append_parsed(&mut stack_prefix, p, true);
378 }
379 }
380 }
381 }
382 } else {
383 let root_ga = work_tree.join(".gitattributes");
384 if let Some(content) = read_worktree_gitattributes(&root_ga) {
385 if content.len() <= MAX_ATTR_FILE_BYTES {
386 let p = parse_gitattributes_file_content(&content, ".gitattributes");
387 append_parsed(&mut stack_prefix, p, true);
388 }
389 } else if let Some(entry) = index.get(b".gitattributes", 0) {
390 if let Ok(obj) = odb.read(&entry.oid) {
391 if let Ok(content) = String::from_utf8(obj.data) {
392 if content.len() <= MAX_ATTR_FILE_BYTES {
393 let p = parse_gitattributes_file_content(&content, ".gitattributes");
394 append_parsed(&mut stack_prefix, p, true);
395 }
396 }
397 }
398 }
399 }
400 let mut info_rules = ParsedGitAttributes::default();
401 let info = repo.git_dir().join("info/attributes");
402 if let Some(content) = read_worktree_gitattributes(&info) {
403 if content.len() <= MAX_ATTR_FILE_BYTES {
404 let p = parse_gitattributes_file_content(&content, "info/attributes");
405 append_parsed(&mut info_rules, p, true);
406 }
407 }
408 Ok((stack_prefix, info_rules))
409}
410
411fn global_attributes_path(env: &Environment, config: &ConfigSet) -> Result<Option<PathBuf>> {
412 if let Some(path) = config.get("core.attributesfile") {
413 return Ok(Some(PathBuf::from(parse_path(env, &path))));
414 }
415 Ok(env.default_xdg_config_git_file("attributes"))
416}
417
418fn read_worktree_gitattributes(path: &Path) -> Option<String> {
419 let meta = std::fs::symlink_metadata(path).ok()?;
420 if meta.file_type().is_symlink() {
421 return None;
422 }
423 record_attr_read(path);
424 std::fs::read_to_string(path).ok()
425}
426
427fn gitattributes_index_map(index: &Index) -> HashMap<Vec<u8>, ObjectId> {
428 index
429 .entries()
430 .iter()
431 .filter(|e| e.stage() == 0 && is_gitattributes_index_path(&e.path))
432 .map(|e| (e.path.clone(), e.oid))
433 .collect()
434}
435
436fn is_gitattributes_index_path(path: &[u8]) -> bool {
437 path == b".gitattributes" || path.ends_with(b"/.gitattributes")
438}
439
440pub(crate) fn record_ignore_pattern_file_read(path: &Path) {
442 record_ignore_read(path);
443}