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