1use std::cell::RefCell;
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16
17use crate::attributes::{
18 parse_gitattributes_file_content, parse_gitattributes_file_content_with_base,
19 ParsedGitAttributes, MAX_ATTR_FILE_BYTES,
20};
21use crate::config::parse_path;
22use crate::config::ConfigSet;
23use crate::crlf::{self, AttrRule, ConversionConfig, FileAttrs};
24use crate::environment::Environment;
25use crate::error::{Error, Result};
26use crate::filter_process::FilterProcessState;
27use crate::ignore::IgnoreMatcher;
28use crate::index::Index;
29use crate::objects::ObjectId;
30use crate::odb::Odb;
31use crate::repo::Repository;
32use crate::repo_caches::RepoCaches;
33
34#[cfg(test)]
40pub mod file_load_counters {
41 use crate::git_path::CanonicalizeExt;
42 use std::cell::RefCell;
43 use std::collections::HashMap;
44 use std::path::{Path, PathBuf};
45
46 thread_local! {
47 static MAP: RefCell<HashMap<PathBuf, u32>> = RefCell::new(HashMap::new());
48 }
49
50 pub fn reset() {
52 MAP.with(|m| m.borrow_mut().clear());
53 }
54
55 pub fn record(path: &Path) {
57 let key = path
58 .canonicalize_simplified()
59 .unwrap_or_else(|_| path.to_path_buf());
60 MAP.with(|m| *m.borrow_mut().entry(key).or_insert(0) += 1);
61 }
62
63 #[must_use]
65 pub fn max_reads_per_file() -> u32 {
66 MAP.with(|m| m.borrow().values().copied().max().unwrap_or(0))
67 }
68
69 #[must_use]
71 pub fn reads_for(path: &Path) -> u32 {
72 let key = path
73 .canonicalize_simplified()
74 .unwrap_or_else(|_| path.to_path_buf());
75 MAP.with(|m| m.borrow().get(&key).copied().unwrap_or(0))
76 }
77}
78
79fn record_attr_read(path: &Path) {
80 #[cfg(test)]
81 file_load_counters::record(path);
82 let _ = path;
83}
84
85fn record_ignore_read(path: &Path) {
86 #[cfg(test)]
87 file_load_counters::record(path);
88 let _ = path;
89}
90
91pub struct WorktreeRules {
93 config: Arc<ConfigSet>,
94 conversion: ConversionConfig,
95 ignore: RefCell<IgnoreMatcher>,
96 attrs: AttributeState,
97 caches: Arc<RepoCaches>,
98}
99
100struct AttributeState {
101 work_tree: PathBuf,
102 _git_dir: PathBuf,
103 odb: Odb,
104 gitattributes_index: HashMap<Vec<u8>, ObjectId>,
106 gitattributes_index_only: bool,
108 stack_prefix: Arc<ParsedGitAttributes>,
110 info_rules: ParsedGitAttributes,
112 dir_fragment: RefCell<HashMap<String, Arc<ParsedGitAttributes>>>,
115 dir_stack: RefCell<Vec<DirFrame>>,
118 root_merged: RefCell<Option<Arc<ParsedGitAttributes>>>,
120}
121
122struct DirFrame {
124 dir: String,
125 stack: Arc<ParsedGitAttributes>,
128 merged: Option<Arc<ParsedGitAttributes>>,
130}
131
132impl WorktreeRules {
133 pub fn from_repository(repo: &Repository, index: &Index) -> Result<Self> {
139 let config = repo.config()?.clone();
140 Self::from_parts(repo, index, config)
141 }
142
143 pub fn from_parts(repo: &Repository, index: &Index, config: Arc<ConfigSet>) -> Result<Self> {
149 Self::from_parts_with_attribute_source(repo, index, config, false)
150 }
151
152 pub(crate) fn from_parts_index_attributes_only(
154 repo: &Repository,
155 index: &Index,
156 config: Arc<ConfigSet>,
157 ) -> Result<Self> {
158 Self::from_parts_with_attribute_source(repo, index, config, true)
159 }
160
161 fn from_parts_with_attribute_source(
162 repo: &Repository,
163 index: &Index,
164 config: Arc<ConfigSet>,
165 gitattributes_index_only: bool,
166 ) -> Result<Self> {
167 let work_tree = repo
168 .work_tree()
169 .ok_or(Error::RequiresWorkTree)?
170 .to_path_buf();
171 let conversion = ConversionConfig::from_config_with_runner(&config, repo.command_runner());
172 let ignore = RefCell::new(IgnoreMatcher::from_repository(repo)?);
173 let (stack_prefix, info_rules) = load_attribute_stack_prefix_and_info(
174 repo,
175 &work_tree,
176 &config,
177 index,
178 repo.odb(),
179 gitattributes_index_only,
180 )?;
181 let attrs = AttributeState {
182 work_tree,
183 _git_dir: repo.git_dir().to_path_buf(),
184 odb: repo.odb().clone(),
185 gitattributes_index: gitattributes_index_map(index),
186 gitattributes_index_only,
187 stack_prefix: Arc::new(stack_prefix),
188 info_rules,
189 dir_fragment: RefCell::new(HashMap::new()),
190 dir_stack: RefCell::new(Vec::new()),
191 root_merged: RefCell::new(None),
192 };
193 Ok(Self {
194 config,
195 conversion,
196 ignore,
197 attrs,
198 caches: Arc::clone(repo.caches()),
199 })
200 }
201
202 pub fn from_checkout_prospective_index(
212 repo: &Repository,
213 prospective_index: &Index,
214 config: Arc<ConfigSet>,
215 ) -> Result<Self> {
216 Self::from_parts_index_attributes_only(repo, prospective_index, config)
217 }
218
219 #[must_use]
221 pub fn filter_process(&self) -> &FilterProcessState {
222 self.caches.filters()
223 }
224
225 #[must_use]
227 pub fn config(&self) -> &ConfigSet {
228 &self.config
229 }
230
231 #[must_use]
233 pub fn config_arc(&self) -> Arc<ConfigSet> {
234 Arc::clone(&self.config)
235 }
236
237 #[must_use]
239 pub fn conversion(&self) -> &ConversionConfig {
240 &self.conversion
241 }
242
243 pub fn ignore(&self) -> std::cell::Ref<'_, IgnoreMatcher> {
245 self.ignore.borrow()
246 }
247
248 pub fn ignore_mut(&self) -> std::cell::RefMut<'_, IgnoreMatcher> {
250 self.ignore.borrow_mut()
251 }
252
253 #[must_use]
255 pub fn attribute_rules_for_path(&self, rel_path: &str) -> Vec<AttrRule> {
256 let parent = parent_dir_key(rel_path);
257 self.attrs.merged_for_dir(&parent).rules.clone()
258 }
259
260 #[must_use]
262 pub fn file_attrs(&self, rel_path: &str, is_dir: bool) -> FileAttrs {
263 let parent = parent_dir_key(rel_path);
264 let parsed = self.attrs.merged_for_dir(&parent);
265 crlf::file_attrs_from_gitattributes(
266 &parsed.rules,
267 &parsed.macros,
268 rel_path,
269 is_dir,
270 &self.config,
271 )
272 }
273}
274
275impl AttributeState {
276 fn merged_for_dir(&self, dir: &str) -> Arc<ParsedGitAttributes> {
279 if dir.is_empty() {
280 let mut root = self.root_merged.borrow_mut();
281 return Arc::clone(root.get_or_insert_with(|| self.with_info(&self.stack_prefix)));
282 }
283 self.enter_dir(dir);
284 let mut frames = self.dir_stack.borrow_mut();
285 let Some(frame) = frames.last_mut() else {
286 return self.with_info(&self.stack_prefix);
287 };
288 Arc::clone(
289 frame
290 .merged
291 .get_or_insert_with(|| self.with_info(&frame.stack)),
292 )
293 }
294
295 fn with_info(&self, stack: &Arc<ParsedGitAttributes>) -> Arc<ParsedGitAttributes> {
296 let info = &self.info_rules;
297 if info.rules.is_empty() && info.macros.defs.is_empty() && info.warnings.is_empty() {
298 return Arc::clone(stack);
299 }
300 let mut merged = ParsedGitAttributes::clone(stack);
301 append_parsed(&mut merged, info.clone(), true);
302 Arc::new(merged)
303 }
304
305 fn enter_dir(&self, dir: &str) {
308 let ends: Vec<usize> = dir
309 .match_indices('/')
310 .map(|(at, _)| at)
311 .chain(std::iter::once(dir.len()))
312 .collect();
313 let mut frames = self.dir_stack.borrow_mut();
314 let shared = (1..=frames.len().min(ends.len()))
317 .rev()
318 .find(|&depth| frames[depth - 1].dir == dir[..ends[depth - 1]])
319 .unwrap_or(0);
320 frames.truncate(shared);
321 for &end in &ends[shared..] {
322 let prefix = &dir[..end];
323 let parent = frames
324 .last()
325 .map_or(&self.stack_prefix, |frame| &frame.stack);
326 let stack = match self.load_dir_fragment(prefix) {
327 Some(fragment) if !fragment.rules.is_empty() || !fragment.warnings.is_empty() => {
328 let mut stack = ParsedGitAttributes::clone(parent);
329 append_parsed(&mut stack, ParsedGitAttributes::clone(&fragment), false);
330 Arc::new(stack)
331 }
332 _ => Arc::clone(parent),
333 };
334 frames.push(DirFrame {
335 dir: prefix.to_owned(),
336 stack,
337 merged: None,
338 });
339 }
340 }
341
342 fn load_dir_fragment(&self, dir: &str) -> Option<Arc<ParsedGitAttributes>> {
345 if let Some(hit) = self.dir_fragment.borrow().get(dir) {
346 return Some(Arc::clone(hit));
347 }
348 let mut parsed = ParsedGitAttributes::default();
349 let ga_stored = if dir.is_empty() {
350 ".gitattributes".to_owned()
351 } else {
352 format!("{dir}/.gitattributes")
353 };
354 let key = crate::repo_relpath::storage_bytes(&ga_stored).into_owned();
355 if !self.gitattributes_index_only {
356 let wt_ga = if dir.is_empty() {
357 self.work_tree.join(".gitattributes")
358 } else {
359 self.work_tree.join(dir).join(".gitattributes")
360 };
361 if let Some(content) = read_worktree_gitattributes(&wt_ga) {
362 if content.len() <= MAX_ATTR_FILE_BYTES {
363 let mut p =
364 parse_gitattributes_file_content_with_base(&content, &ga_stored, dir);
365 parsed.rules.append(&mut p.rules);
366 parsed.warnings.append(&mut p.warnings);
367 }
368 return Some(self.remember_fragment(dir, parsed));
369 }
370 }
371 let oid = self.gitattributes_index.get(&key)?;
372 if let Ok(obj) = self.odb.read(oid) {
373 if let Ok(content) = String::from_utf8(obj.data) {
374 if content.len() <= MAX_ATTR_FILE_BYTES {
375 let mut p =
376 parse_gitattributes_file_content_with_base(&content, &ga_stored, dir);
377 parsed.rules.append(&mut p.rules);
378 parsed.warnings.append(&mut p.warnings);
379 }
380 }
381 }
382 Some(self.remember_fragment(dir, parsed))
383 }
384
385 fn remember_fragment(
386 &self,
387 dir: &str,
388 parsed: ParsedGitAttributes,
389 ) -> Arc<ParsedGitAttributes> {
390 let parsed = Arc::new(parsed);
391 self.dir_fragment
392 .borrow_mut()
393 .insert(dir.to_owned(), Arc::clone(&parsed));
394 parsed
395 }
396}
397
398fn append_parsed(
399 merged: &mut ParsedGitAttributes,
400 mut parsed: ParsedGitAttributes,
401 take_macros: bool,
402) {
403 merged.rules.append(&mut parsed.rules);
404 if take_macros {
405 merged.macros.defs.extend(parsed.macros.defs.drain());
406 }
407 merged.warnings.append(&mut parsed.warnings);
408}
409
410fn parent_dir_key(rel_path: &str) -> String {
411 Path::new(rel_path)
412 .parent()
413 .map(|p| p.to_string_lossy().replace('\\', "/"))
414 .filter(|s| !s.is_empty())
415 .unwrap_or_default()
416}
417
418fn load_attribute_stack_prefix_and_info(
419 repo: &Repository,
420 work_tree: &Path,
421 config: &ConfigSet,
422 index: &Index,
423 odb: &Odb,
424 gitattributes_index_only: bool,
425) -> Result<(ParsedGitAttributes, ParsedGitAttributes)> {
426 let mut stack_prefix = ParsedGitAttributes::default();
427 if let Some(g) = global_attributes_path(repo.environment(), config)? {
428 if let Some(content) = read_worktree_gitattributes(&g) {
429 if content.len() <= MAX_ATTR_FILE_BYTES {
430 let p = parse_gitattributes_file_content(&content, g.to_string_lossy().as_ref());
431 append_parsed(&mut stack_prefix, p, true);
432 }
433 }
434 }
435 if gitattributes_index_only {
436 if let Some(entry) = index.get(b".gitattributes", 0) {
437 if let Ok(obj) = odb.read(&entry.oid) {
438 if let Ok(content) = String::from_utf8(obj.data) {
439 if content.len() <= MAX_ATTR_FILE_BYTES {
440 let p = parse_gitattributes_file_content(&content, ".gitattributes");
441 append_parsed(&mut stack_prefix, p, true);
442 }
443 }
444 }
445 }
446 } else {
447 let root_ga = work_tree.join(".gitattributes");
448 if let Some(content) = read_worktree_gitattributes(&root_ga) {
449 if content.len() <= MAX_ATTR_FILE_BYTES {
450 let p = parse_gitattributes_file_content(&content, ".gitattributes");
451 append_parsed(&mut stack_prefix, p, true);
452 }
453 } else if let Some(entry) = index.get(b".gitattributes", 0) {
454 if let Ok(obj) = odb.read(&entry.oid) {
455 if let Ok(content) = String::from_utf8(obj.data) {
456 if content.len() <= MAX_ATTR_FILE_BYTES {
457 let p = parse_gitattributes_file_content(&content, ".gitattributes");
458 append_parsed(&mut stack_prefix, p, true);
459 }
460 }
461 }
462 }
463 }
464 let mut info_rules = ParsedGitAttributes::default();
465 let info = repo.git_dir().join("info/attributes");
466 if let Some(content) = read_worktree_gitattributes(&info) {
467 if content.len() <= MAX_ATTR_FILE_BYTES {
468 let p = parse_gitattributes_file_content(&content, "info/attributes");
469 append_parsed(&mut info_rules, p, true);
470 }
471 }
472 Ok((stack_prefix, info_rules))
473}
474
475fn global_attributes_path(env: &Environment, config: &ConfigSet) -> Result<Option<PathBuf>> {
476 if let Some(path) = config.get("core.attributesfile") {
477 return Ok(Some(PathBuf::from(parse_path(env, &path))));
478 }
479 Ok(env.default_xdg_config_git_file("attributes"))
480}
481
482fn read_worktree_gitattributes(path: &Path) -> Option<String> {
483 let meta = std::fs::symlink_metadata(path).ok()?;
484 if meta.file_type().is_symlink() {
485 return None;
486 }
487 record_attr_read(path);
488 std::fs::read_to_string(path).ok()
489}
490
491fn gitattributes_index_map(index: &Index) -> HashMap<Vec<u8>, ObjectId> {
492 index
493 .entries()
494 .iter()
495 .filter(|e| e.stage() == 0 && is_gitattributes_index_path(&e.path))
496 .map(|e| (e.path.clone(), e.oid))
497 .collect()
498}
499
500fn is_gitattributes_index_path(path: &[u8]) -> bool {
501 path == b".gitattributes" || path.ends_with(b"/.gitattributes")
502}
503
504pub(crate) fn record_ignore_pattern_file_read(path: &Path) {
506 record_ignore_read(path);
507}