1use super::*;
6use crate::attributes::*;
7use crate::index_io::*;
8use crate::status::*;
9use crate::types_admin::*;
10use std::sync::atomic::{AtomicUsize, Ordering};
11
12pub fn untracked_paths(
13 worktree_root: impl AsRef<Path>,
14 git_dir: impl AsRef<Path>,
15 format: ObjectFormat,
16) -> Result<Vec<Vec<u8>>> {
17 untracked_paths_with_options(
18 worktree_root,
19 git_dir,
20 format,
21 UntrackedPathOptions::default(),
22 )
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct UntrackedPathspecFilter {
29 pub path: Vec<u8>,
30 pub recursive: bool,
31 pub is_glob: bool,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Default)]
35pub struct UntrackedPathOptions {
36 pub directory: bool,
37 pub no_empty_directory: bool,
38 pub preserve_ignored_directories: bool,
39 pub exclude_standard: bool,
40 pub ignored_only: bool,
41 pub exclude_patterns: Vec<Vec<u8>>,
42 pub exclude_per_directory: Vec<String>,
43 pub pathspecs: Vec<UntrackedPathspecFilter>,
44}
45
46pub use sley_pathspec::{
50 PathspecMatchMagic, WM_CASEFOLD, WM_PATHNAME, pathspec_is_glob, pathspec_item_matches,
51 wildmatch,
52};
53
54pub fn untracked_pathspec_matches(spec: &UntrackedPathspecFilter, path: &[u8]) -> bool {
56 if spec.path.is_empty() {
57 return true;
58 }
59 let path_no_slash = path.strip_suffix(b"/").unwrap_or(path);
60 if path == spec.path.as_slice() || path_no_slash == spec.path.as_slice() {
61 return true;
62 }
63 if spec.recursive
64 && let Some(rest) = path
65 .strip_prefix(spec.path.as_slice())
66 .and_then(|rest| rest.strip_prefix(b"/"))
67 && !rest.is_empty()
68 {
69 return true;
70 }
71 if spec.is_glob {
72 return untracked_wildmatch(&spec.path, path)
73 || untracked_wildmatch(&spec.path, path_no_slash);
74 }
75 false
76}
77
78pub fn untracked_pathspec_needs_descent(parent: &[u8], specs: &[UntrackedPathspecFilter]) -> bool {
80 if specs.is_empty() {
81 return false;
82 }
83 let parent_prefix = if parent.is_empty() {
84 Vec::new()
85 } else {
86 let mut prefix = parent.to_vec();
87 prefix.push(b'/');
88 prefix
89 };
90 for spec in specs {
91 if !parent.is_empty()
92 && spec.path.starts_with(&parent_prefix)
93 && spec.path.as_slice() != parent
94 {
95 return true;
96 }
97 if spec.is_glob && glob_pathspec_may_match_under(&spec.path, parent) {
98 return true;
99 }
100 if spec.recursive
101 && !parent.is_empty()
102 && parent.starts_with(spec.path.as_slice())
103 && parent != spec.path.as_slice()
104 {
105 return true;
106 }
107 }
108 false
109}
110
111pub(crate) fn untracked_pathspec_selects_directory(
118 specs: &[UntrackedPathspecFilter],
119 git_path: &[u8],
120) -> bool {
121 specs
122 .iter()
123 .any(|spec| untracked_pathspec_matches(spec, git_path))
124}
125
126pub(crate) fn glob_pathspec_may_match_under(pattern: &[u8], dir: &[u8]) -> bool {
127 let literal_prefix = literal_prefix_before_glob(pattern);
128 if literal_prefix.is_empty() {
129 return true;
130 }
131 if dir.is_empty() {
132 return true;
133 }
134 let mut dir_prefix = dir.to_vec();
135 dir_prefix.push(b'/');
136 if literal_prefix.starts_with(&dir_prefix) {
137 return true;
138 }
139 if dir_prefix.starts_with(&literal_prefix) {
140 return true;
141 }
142 literal_prefix
143 .strip_suffix(b"/")
144 .is_some_and(|prefix| prefix == dir)
145}
146
147pub(crate) fn literal_prefix_before_glob(pattern: &[u8]) -> Vec<u8> {
148 let mut prefix = Vec::new();
149 for &byte in pattern {
150 if pathspec_is_glob(&[byte]) {
151 break;
152 }
153 prefix.push(byte);
154 }
155 prefix
156}
157
158pub(crate) fn insert_untracked_directory(paths: &mut BTreeSet<Vec<u8>>, git_path: &[u8]) {
159 let mut directory = git_path.to_vec();
160 if directory.last() != Some(&b'/') {
161 directory.push(b'/');
162 }
163 paths.insert(directory);
164}
165
166pub(crate) fn untracked_wildmatch(pattern: &[u8], text: &[u8]) -> bool {
168 wildmatch(pattern, text, 0)
171}
172
173#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct IgnoreMatch {
175 pub source: Vec<u8>,
176 pub line_number: usize,
177 pub pattern: Vec<u8>,
178 pub ignored: bool,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
182pub enum AttributeState {
183 Set,
184 Unset,
185 Value(Vec<u8>),
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct AttributeCheck {
190 pub attribute: Vec<u8>,
191 pub state: Option<AttributeState>,
192}
193
194pub fn untracked_paths_with_options(
195 worktree_root: impl AsRef<Path>,
196 git_dir: impl AsRef<Path>,
197 format: ObjectFormat,
198 options: UntrackedPathOptions,
199) -> Result<Vec<Vec<u8>>> {
200 let worktree_root = worktree_root.as_ref();
201 let git_dir = git_dir.as_ref();
202 let db = FileObjectDatabase::from_git_dir(git_dir, format);
203 let (index, stat_cache, _) = read_index_entries_with_stat_cache(git_dir, format, &db)?;
204 let all_index_paths = read_all_index_paths(git_dir, format)?;
205 let ignores = IgnoreMatcher::from_sources(
206 worktree_root,
207 options.exclude_standard,
208 &options.exclude_patterns,
209 &options.exclude_per_directory,
210 )?;
211 if options.ignored_only {
212 return ignored_untracked_paths(
213 worktree_root,
214 git_dir,
215 &index,
216 &ignores,
217 options.directory,
218 );
219 }
220 if options.directory {
221 let mut paths = BTreeSet::new();
222 collect_untracked_directory_paths(
223 worktree_root,
224 git_dir,
225 worktree_root,
226 &index,
227 &ignores,
228 &options,
229 &mut paths,
230 )?;
231 return Ok(paths.into_iter().collect());
232 }
233 let worktree = worktree_entries_with_stat_cache(
234 worktree_root,
235 git_dir,
236 format,
237 Some(&stat_cache),
238 None,
239 None,
240 )?;
241 Ok(ls_files_untracked_paths_from_worktree(
242 &worktree,
243 &index,
244 &all_index_paths,
245 &ignores,
246 ))
247}
248
249pub fn killed_paths(
250 worktree_root: impl AsRef<Path>,
251 git_dir: impl AsRef<Path>,
252 format: ObjectFormat,
253) -> Result<Vec<Vec<u8>>> {
254 let worktree_root = worktree_root.as_ref();
255 let git_dir = git_dir.as_ref();
256 let index_path = repository_index_path(git_dir);
257 let bytes = match fs::read(index_path) {
258 Ok(bytes) => bytes,
259 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
260 Err(err) => return Err(err.into()),
261 };
262 let index = Index::parse(&bytes, format)?;
263 let mut exact = BTreeMap::new();
264 let mut directories = BTreeSet::new();
265 for entry in index
266 .entries
267 .iter()
268 .filter(|entry| entry.stage() == Stage::Normal)
269 {
270 let path = entry.path.as_bytes();
271 exact.insert(path.to_vec(), entry.mode);
272 for (idx, byte) in path.iter().enumerate() {
273 if *byte == b'/' && idx > 0 {
274 directories.insert(path[..idx].to_vec());
275 }
276 }
277 }
278 let mut paths = BTreeSet::new();
279 collect_killed_paths(
280 worktree_root,
281 git_dir,
282 worktree_root,
283 &[],
284 &exact,
285 &directories,
286 &mut paths,
287 )?;
288 Ok(paths.into_iter().collect())
289}
290
291fn collect_killed_paths(
292 root: &Path,
293 git_dir: &Path,
294 dir: &Path,
295 dir_git_path: &[u8],
296 exact: &BTreeMap<Vec<u8>, u32>,
297 directories: &BTreeSet<Vec<u8>>,
298 paths: &mut BTreeSet<Vec<u8>>,
299) -> Result<()> {
300 if is_same_path(dir, git_dir) {
301 return Ok(());
302 }
303 let mut entries = fs::read_dir(dir)?.collect::<std::result::Result<Vec<_>, _>>()?;
304 entries.sort_by_key(|entry| entry.file_name());
305 for entry in entries {
306 let path = entry.path();
307 if is_dot_git_entry(&path) || is_same_path(&path, git_dir) {
308 continue;
309 }
310 let git_path = git_path_append_component(dir_git_path, &entry.file_name());
311 let file_type = entry.file_type()?;
312 if let Some(mode) = exact.get(&git_path) {
313 if file_type.is_dir() && !sley_index::is_gitlink(*mode) && *mode != SPARSE_DIR_MODE {
314 collect_killed_leaves(git_dir, &path, &git_path, paths)?;
315 }
316 continue;
317 }
318 if directories.contains(&git_path) {
319 if file_type.is_dir() {
320 collect_killed_paths(root, git_dir, &path, &git_path, exact, directories, paths)?;
321 } else if file_type.is_file() || file_type.is_symlink() {
322 paths.insert(git_path);
323 }
324 }
325 }
326 Ok(())
327}
328
329fn collect_killed_leaves(
330 git_dir: &Path,
331 dir: &Path,
332 dir_git_path: &[u8],
333 paths: &mut BTreeSet<Vec<u8>>,
334) -> Result<()> {
335 if is_same_path(dir, git_dir) {
336 return Ok(());
337 }
338 let mut entries = fs::read_dir(dir)?.collect::<std::result::Result<Vec<_>, _>>()?;
339 entries.sort_by_key(|entry| entry.file_name());
340 for entry in entries {
341 let path = entry.path();
342 if is_dot_git_entry(&path) || is_same_path(&path, git_dir) {
343 continue;
344 }
345 let git_path = git_path_append_component(dir_git_path, &entry.file_name());
346 let file_type = entry.file_type()?;
347 if file_type.is_dir() {
348 collect_killed_leaves(git_dir, &path, &git_path, paths)?;
349 } else if file_type.is_file() || file_type.is_symlink() {
350 paths.insert(git_path);
351 }
352 }
353 Ok(())
354}
355
356pub(crate) fn ls_files_untracked_paths_from_worktree(
360 worktree: &BTreeMap<Vec<u8>, TrackedEntry>,
361 index: &BTreeMap<Vec<u8>, TrackedEntry>,
362 all_index_paths: &BTreeSet<Vec<u8>>,
363 ignores: &IgnoreMatcher,
364) -> Vec<Vec<u8>> {
365 let mut paths = BTreeSet::new();
366 for (path, entry) in worktree {
367 if index.contains_key(path)
368 || all_index_paths.contains(path)
369 || ignores.is_ignored(path, false)
370 {
371 continue;
372 }
373 if entry.mode == 0o040000 && entry.oid.is_null() {
374 insert_untracked_directory(&mut paths, path);
375 continue;
376 }
377 paths.insert(path.clone());
378 }
379 paths.into_iter().collect()
380}
381
382pub fn path_matches_standard_ignore(
383 worktree_root: impl AsRef<Path>,
384 path: &[u8],
385 is_dir: bool,
386) -> Result<bool> {
387 path_matches_ignore(worktree_root, path, is_dir, true, &[])
388}
389
390pub fn standard_ignore_match(
391 worktree_root: impl AsRef<Path>,
392 path: &[u8],
393 is_dir: bool,
394) -> Result<Option<IgnoreMatch>> {
395 let ignores = IgnoreMatcher::from_worktree_root(worktree_root.as_ref())?;
396 Ok(ignores.match_for(path, is_dir).map(IgnorePattern::to_match))
397}
398
399pub fn standard_attributes_for_path(
400 worktree_root: impl AsRef<Path>,
401 path: &[u8],
402 requested: &[Vec<u8>],
403 all: bool,
404) -> Result<Vec<AttributeCheck>> {
405 let matcher = AttributeMatcher::from_worktree_root(worktree_root.as_ref())?;
406 Ok(matcher.attributes_for_path(path, requested, all))
407}
408
409pub struct StandardAttributeMatcher {
416 matcher: AttributeMatcher,
417}
418
419impl StandardAttributeMatcher {
420 pub fn from_worktree_root(worktree_root: impl AsRef<Path>) -> Result<Self> {
421 Ok(Self {
422 matcher: AttributeMatcher::from_worktree_root(worktree_root.as_ref())?,
423 })
424 }
425
426 pub fn attributes_for_path(
427 &self,
428 path: &[u8],
429 requested: &[Vec<u8>],
430 all: bool,
431 ) -> Vec<AttributeCheck> {
432 self.matcher.attributes_for_path(path, requested, all)
433 }
434}
435
436pub fn standard_attributes_for_path_in_repo(
437 attr_root: impl AsRef<Path>,
438 git_dir: impl AsRef<Path>,
439 path: &[u8],
440 requested: &[Vec<u8>],
441 all: bool,
442 include_worktree_attributes: bool,
443 ignore_case: bool,
444) -> Result<Vec<AttributeCheck>> {
445 let attr_root = attr_root.as_ref();
446 let git_dir = git_dir.as_ref();
447 let mut matcher = AttributeMatcher::default();
448 matcher.configure_case_sensitivity(git_dir);
449 matcher.ignore_case = ignore_case;
450 if !matcher.read_configured_attributes(attr_root, git_dir) {
451 matcher.read_default_global_attributes();
452 }
453 if include_worktree_attributes {
454 collect_attribute_patterns(attr_root, attr_root, &mut matcher)?;
455 }
456 read_attribute_patterns(
457 git_dir.join("info").join("attributes"),
458 &mut matcher,
459 &[],
460 b"info/attributes",
461 false,
462 );
463 Ok(matcher.attributes_for_path(path, requested, all))
464}
465
466pub fn standard_attributes_for_path_from_tree(
467 worktree_root: impl AsRef<Path>,
468 git_dir: impl AsRef<Path>,
469 db: &FileObjectDatabase,
470 format: ObjectFormat,
471 tree_oid: &ObjectId,
472 path: &[u8],
473 requested: &[Vec<u8>],
474 all: bool,
475) -> Result<Vec<AttributeCheck>> {
476 let mut matcher = AttributeMatcher::default();
477 let worktree_root = worktree_root.as_ref();
478 let git_dir = git_dir.as_ref();
479 matcher.configure_case_sensitivity(git_dir);
480 if !matcher.read_configured_attributes(worktree_root, git_dir) {
481 matcher.read_default_global_attributes();
482 }
483 collect_attribute_patterns_from_tree(db, format, tree_oid, Vec::new(), &mut matcher)?;
484 read_attribute_patterns(
485 git_dir.join("info").join("attributes"),
486 &mut matcher,
487 &[],
488 b"info/attributes",
489 false,
490 );
491 Ok(matcher.attributes_for_path(path, requested, all))
492}
493
494pub fn standard_attributes_for_path_from_index(
495 worktree_root: impl AsRef<Path>,
496 git_dir: impl AsRef<Path>,
497 format: ObjectFormat,
498 path: &[u8],
499 requested: &[Vec<u8>],
500 all: bool,
501) -> Result<Vec<AttributeCheck>> {
502 let worktree_root = worktree_root.as_ref();
503 let git_dir = git_dir.as_ref();
504 let mut matcher = AttributeMatcher::default();
505 matcher.configure_case_sensitivity(git_dir);
506 if !matcher.read_configured_attributes(worktree_root, git_dir) {
507 matcher.read_default_global_attributes();
508 }
509 let db = FileObjectDatabase::from_git_dir(git_dir, format);
510 collect_attribute_patterns_from_index(git_dir, format, &db, &mut matcher)?;
511 read_attribute_patterns(
512 git_dir.join("info").join("attributes"),
513 &mut matcher,
514 &[],
515 b"info/attributes",
516 false,
517 );
518 Ok(matcher.attributes_for_path(path, requested, all))
519}
520
521pub fn path_matches_ignore(
522 worktree_root: impl AsRef<Path>,
523 path: &[u8],
524 is_dir: bool,
525 exclude_standard: bool,
526 exclude_patterns: &[Vec<u8>],
527) -> Result<bool> {
528 path_matches_ignore_with_per_directory(
529 worktree_root,
530 path,
531 is_dir,
532 exclude_standard,
533 exclude_patterns,
534 &[],
535 )
536}
537
538pub fn path_matches_ignore_with_per_directory(
539 worktree_root: impl AsRef<Path>,
540 path: &[u8],
541 is_dir: bool,
542 exclude_standard: bool,
543 exclude_patterns: &[Vec<u8>],
544 exclude_per_directory: &[String],
545) -> Result<bool> {
546 let ignores = IgnoreMatcher::from_sources(
547 worktree_root.as_ref(),
548 exclude_standard,
549 exclude_patterns,
550 exclude_per_directory,
551 )?;
552 Ok(ignores.is_ignored(path, is_dir))
553}
554
555pub fn ignored_index_entries<'a>(
556 worktree_root: impl AsRef<Path>,
557 entries: &'a [IndexEntry],
558 exclude_standard: bool,
559 exclude_patterns: &[Vec<u8>],
560 exclude_per_directory: &[String],
561) -> Result<Vec<&'a IndexEntry>> {
562 let ignores = IgnoreMatcher::from_sources(
563 worktree_root.as_ref(),
564 exclude_standard,
565 exclude_patterns,
566 exclude_per_directory,
567 )?;
568 Ok(entries
569 .iter()
570 .filter(|entry| ignores.is_ignored(entry.path.as_bytes(), false))
571 .collect())
572}
573
574pub(crate) fn collect_untracked_directory_paths(
575 root: &Path,
576 git_dir: &Path,
577 dir: &Path,
578 index: &BTreeMap<Vec<u8>, TrackedEntry>,
579 ignores: &IgnoreMatcher,
580 options: &UntrackedPathOptions,
581 paths: &mut BTreeSet<Vec<u8>>,
582) -> Result<()> {
583 if is_same_path(dir, git_dir) {
584 return Ok(());
585 }
586 let mut entries = fs::read_dir(dir)?.collect::<std::result::Result<Vec<_>, _>>()?;
587 entries.sort_by_key(|entry| entry.file_name());
588 for entry in entries {
589 let path = entry.path();
590 if is_dot_git_entry(&path) {
591 continue;
592 }
593 if is_embedded_git_internals(root, &path) {
594 continue;
595 }
596 if is_same_path(&path, git_dir) {
597 continue;
598 }
599 let metadata = entry.metadata()?;
600 let relative = path.strip_prefix(root).map_err(|_| {
601 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
602 })?;
603 let git_path = git_path_bytes(relative)?;
604 if index
605 .get(&git_path)
606 .is_some_and(|entry| sley_index::is_gitlink(entry.mode))
607 {
608 continue;
609 }
610 if ignores.is_ignored(&git_path, metadata.is_dir()) {
611 continue;
612 }
613 if metadata.is_dir() {
614 if is_nested_repository_boundary(&path, git_dir) {
615 insert_untracked_directory(paths, &git_path);
616 continue;
617 }
618 let has_tracked_below = index_has_path_under(index, &git_path);
619 let needs_descent = untracked_pathspec_needs_descent(&git_path, &options.pathspecs);
620 if has_tracked_below {
621 collect_untracked_directory_paths(
622 root, git_dir, &path, index, ignores, options, paths,
623 )?;
624 } else if active_repository_worktree_dir(&path, git_dir) {
625 insert_untracked_directory(paths, &git_path);
626 } else if needs_descent {
627 if untracked_pathspec_selects_directory(&options.pathspecs, &git_path) {
635 insert_untracked_directory(paths, &git_path);
636 continue;
637 }
638 collect_untracked_directory_paths(
639 root, git_dir, &path, index, ignores, options, paths,
640 )?;
641 } else if options.preserve_ignored_directories
642 && directory_has_ignored(&path, root, git_dir, ignores)?
643 {
644 collect_untracked_directory_paths(
645 root, git_dir, &path, index, ignores, options, paths,
646 )?;
647 } else if !options.no_empty_directory
648 || directory_has_file(&path, root, git_dir, ignores)?
649 {
650 insert_untracked_directory(paths, &git_path);
651 }
652 } else if !index.contains_key(&git_path)
653 && (metadata.is_file() || metadata.file_type().is_symlink())
654 && (options.pathspecs.is_empty()
655 || options
656 .pathspecs
657 .iter()
658 .any(|spec| untracked_pathspec_matches(spec, &git_path)))
659 {
660 paths.insert(git_path);
668 }
669 }
670 Ok(())
671}
672
673pub(crate) fn index_has_path_under(
674 index: &BTreeMap<Vec<u8>, TrackedEntry>,
675 directory: &[u8],
676) -> bool {
677 let mut prefix = directory.to_vec();
681 prefix.push(b'/');
682 index
683 .range::<[u8], _>((
684 std::ops::Bound::Included(prefix.as_slice()),
685 std::ops::Bound::Unbounded,
686 ))
687 .next()
688 .is_some_and(|(path, _)| path.starts_with(&prefix))
689}
690
691pub(crate) fn normal_untracked_paths_from_worktree(
694 worktree: &BTreeMap<Vec<u8>, TrackedEntry>,
695 index: &BTreeMap<Vec<u8>, TrackedEntry>,
696 ignores: &IgnoreMatcher,
697) -> Vec<Vec<u8>> {
698 let mut paths = BTreeSet::new();
699 for (path, entry) in worktree {
700 if index.contains_key(path) || path_or_parent_is_ignored(ignores, path, false) {
701 continue;
702 }
703 if entry.mode == 0o040000 && entry.oid.is_null() {
704 insert_untracked_directory(&mut paths, path);
705 continue;
706 }
707 paths.insert(untracked_normal_rollup_path(path, index, ignores));
708 }
709 paths.into_iter().collect()
710}
711
712pub(crate) fn path_or_parent_is_ignored(
713 ignores: &IgnoreMatcher,
714 path: &[u8],
715 is_dir: bool,
716) -> bool {
717 if ignores.is_ignored(path, is_dir) {
718 return true;
719 }
720 for (index, byte) in path.iter().enumerate() {
721 if *byte == b'/' && index > 0 && ignores.is_ignored(&path[..index], true) {
722 return true;
723 }
724 }
725 false
726}
727
728pub(crate) fn status_untracked_paths_from_index(
729 root: &Path,
730 git_dir: &Path,
731 index: &Index,
732 stat_cache: &IndexStatCache,
733 ignores: &mut IgnoreMatcher,
734 untracked_mode: StatusUntrackedMode,
735 profile: Option<&mut StatusProfileCounters>,
736) -> Result<Vec<Vec<u8>>> {
737 if matches!(untracked_mode, StatusUntrackedMode::None) {
738 return Ok(Vec::new());
739 }
740 let mut paths = Vec::new();
741 let tracked_dirs = stage0_tracked_directories(index);
742 let tracked = IndexStatusLookup {
743 stat_cache,
744 tracked_dirs: &tracked_dirs,
745 };
746 let mut context = StatusUntrackedWalk {
747 git_dir,
748 tracked: &tracked,
749 ignores,
750 untracked_mode,
751 profile,
752 };
753 collect_status_untracked_paths(&mut context, root, &[], &mut paths)?;
754 paths.sort();
755 paths.dedup();
756 Ok(paths)
757}
758
759pub(crate) fn status_untracked_paths_from_borrowed_index(
760 root: &Path,
761 git_dir: &Path,
762 index: &BorrowedIndex<'_>,
763 ignores: &mut IgnoreMatcher,
764 untracked_mode: StatusUntrackedMode,
765 profile: Option<&mut StatusProfileCounters>,
766) -> Result<Vec<Vec<u8>>> {
767 if matches!(untracked_mode, StatusUntrackedMode::None) {
768 return Ok(Vec::new());
769 }
770 let (mut paths, local_profile) = collect_status_untracked_paths_from_borrowed_index_parallel(
771 root,
772 git_dir,
773 index,
774 ignores.clone(),
775 untracked_mode,
776 )?;
777 if let Some(profile) = profile {
778 profile.merge_untracked(local_profile);
779 }
780 paths.sort();
781 paths.dedup();
782 Ok(paths)
783}
784
785pub(crate) fn stream_status_untracked_paths_from_borrowed_index<F>(
786 root: &Path,
787 git_dir: &Path,
788 index: &BorrowedIndex<'_>,
789 ignores: &mut IgnoreMatcher,
790 untracked_mode: StatusUntrackedMode,
791 profile: Option<&mut StatusProfileCounters>,
792 mut emit: F,
793) -> Result<()>
794where
795 F: for<'a> FnMut(&'a [u8]) -> Result<StreamControl>,
796{
797 if matches!(untracked_mode, StatusUntrackedMode::None) {
798 return Ok(());
799 }
800 let tracked = BorrowedIndexLookup::new(&index.entries);
801 let mut context = StatusUntrackedWalk {
802 git_dir,
803 tracked: &tracked,
804 ignores,
805 untracked_mode,
806 profile,
807 };
808 stream_status_untracked_paths(&mut context, root, &[], &mut emit).map(|_| ())
809}
810
811pub(crate) fn status_untracked_count_from_borrowed_index(
812 root: &Path,
813 git_dir: &Path,
814 index: &BorrowedIndex<'_>,
815 ignores: &mut IgnoreMatcher,
816 untracked_mode: StatusUntrackedMode,
817 profile: Option<&mut StatusProfileCounters>,
818) -> Result<usize> {
819 if matches!(untracked_mode, StatusUntrackedMode::None) {
820 return Ok(0);
821 }
822 let (paths, local_profile) = collect_status_untracked_paths_from_borrowed_index_parallel(
823 root,
824 git_dir,
825 index,
826 ignores.clone(),
827 untracked_mode,
828 )?;
829 if let Some(profile) = profile {
830 profile.merge_untracked(local_profile);
831 }
832 Ok(paths.len())
833}
834
835pub(crate) fn collect_status_untracked_paths_from_borrowed_index_parallel(
836 root: &Path,
837 git_dir: &Path,
838 index: &BorrowedIndex<'_>,
839 ignores: IgnoreMatcher,
840 untracked_mode: StatusUntrackedMode,
841) -> Result<(Vec<Vec<u8>>, StatusProfileCounters)> {
842 let executor = StatusExecutor::new();
843 let mut frontier = vec![StatusUntrackedFrontierTask {
844 dir: root.to_path_buf(),
845 git_path: Vec::new(),
846 ignores,
847 }];
848 let mut paths = Vec::new();
849 let mut profile = StatusProfileCounters::default();
850
851 while !frontier.is_empty() {
852 let worker_count = executor.worker_count_for(frontier.len(), 1, 8);
853 let output = if worker_count <= 1 {
854 let tracked = BorrowedIndexLookup::new(&index.entries);
855 let mut output = StatusUntrackedFrontierOutput::default();
856 for mut task in frontier {
857 let mut context = StatusUntrackedWalk {
858 git_dir,
859 tracked: &tracked,
860 ignores: &mut task.ignores,
861 untracked_mode,
862 profile: Some(&mut output.profile),
863 };
864 collect_status_untracked_frontier_dir(
865 &mut context,
866 &task.dir,
867 &task.git_path,
868 &mut output.paths,
869 &mut output.next,
870 )?;
871 }
872 output
873 } else {
874 let next_task = AtomicUsize::new(0);
875 std::thread::scope(|scope| -> Result<StatusUntrackedFrontierOutput> {
876 let mut handles = Vec::new();
877 for _ in 0..worker_count {
878 let frontier = &frontier;
879 let next_task = &next_task;
880 handles.push(executor.spawn(
881 scope,
882 "status-untracked-frontier",
883 move || -> Result<StatusUntrackedFrontierOutput> {
884 let tracked = BorrowedIndexLookup::new(&index.entries);
885 let mut output = StatusUntrackedFrontierOutput::default();
886 loop {
887 let task_idx = next_task.fetch_add(1, Ordering::Relaxed);
888 let Some(mut task) = frontier.get(task_idx).cloned() else {
889 break;
890 };
891 let mut context = StatusUntrackedWalk {
892 git_dir,
893 tracked: &tracked,
894 ignores: &mut task.ignores,
895 untracked_mode,
896 profile: Some(&mut output.profile),
897 };
898 collect_status_untracked_frontier_dir(
899 &mut context,
900 &task.dir,
901 &task.git_path,
902 &mut output.paths,
903 &mut output.next,
904 )?;
905 }
906 Ok(output)
907 },
908 )?);
909 }
910
911 let mut combined = StatusUntrackedFrontierOutput::default();
912 for handle in handles {
913 let mut output = handle.join()?;
914 combined.paths.append(&mut output.paths);
915 combined.next.append(&mut output.next);
916 combined.profile.merge_untracked(output.profile);
917 }
918 Ok(combined)
919 })?
920 };
921
922 let mut output = output;
923 paths.append(&mut output.paths);
924 profile.merge_untracked(output.profile);
925 frontier = output.next;
926 }
927
928 Ok((paths, profile))
929}
930
931pub(crate) trait StatusTrackedLookup {
932 fn tracked_kind(&self, git_path: &[u8]) -> Option<StatusTrackedKind>;
933 fn tracked_directory_kind(&self, git_path: &[u8]) -> Option<StatusTrackedDirectoryKind>;
934}
935
936#[derive(Debug, Clone, Copy, PartialEq, Eq)]
937pub(crate) enum StatusTrackedKind {
938 File,
939 Gitlink,
940 SkipWorktree,
941}
942
943impl StatusTrackedKind {
944 fn from_mode_and_skip(mode: u32, skip_worktree: bool) -> Self {
945 if sley_index::is_gitlink(mode) {
946 Self::Gitlink
947 } else if skip_worktree {
948 Self::SkipWorktree
949 } else {
950 Self::File
951 }
952 }
953}
954
955#[derive(Debug, Clone, Copy, PartialEq, Eq)]
956pub(crate) enum StatusTrackedDirectoryKind {
957 ContainsTracked,
958 TrackedExcluded,
959}
960
961pub(crate) struct IndexStatusLookup<'a> {
962 stat_cache: &'a IndexStatCache,
963 tracked_dirs: &'a HashSet<&'a [u8]>,
964}
965
966impl StatusTrackedLookup for IndexStatusLookup<'_> {
967 fn tracked_kind(&self, git_path: &[u8]) -> Option<StatusTrackedKind> {
968 self.stat_cache.entries.get(git_path).map(|entry| {
969 StatusTrackedKind::from_mode_and_skip(entry.mode, entry.is_skip_worktree())
970 })
971 }
972
973 fn tracked_directory_kind(&self, git_path: &[u8]) -> Option<StatusTrackedDirectoryKind> {
974 self.tracked_dirs
975 .contains(git_path)
976 .then_some(StatusTrackedDirectoryKind::ContainsTracked)
977 }
978}
979
980pub(crate) struct BorrowedIndexLookup<'a> {
981 entries: &'a [IndexEntryRef<'a>],
982 exact_cursor: Cell<usize>,
983 directory_prefix: RefCell<Vec<u8>>,
984}
985
986impl<'a> BorrowedIndexLookup<'a> {
987 pub(crate) fn new(entries: &'a [IndexEntryRef<'a>]) -> Self {
988 Self {
989 entries,
990 exact_cursor: Cell::new(0),
991 directory_prefix: RefCell::new(Vec::new()),
992 }
993 }
994}
995
996impl StatusTrackedLookup for BorrowedIndexLookup<'_> {
997 fn tracked_kind(&self, git_path: &[u8]) -> Option<StatusTrackedKind> {
998 let mut start = self.exact_cursor.get().min(self.entries.len());
999 if start == self.entries.len() || self.entries[start].path > git_path {
1000 start = self.entries.partition_point(|entry| entry.path < git_path);
1001 } else {
1002 while start < self.entries.len() && self.entries[start].path < git_path {
1003 start += 1;
1004 }
1005 }
1006 self.exact_cursor.set(start);
1007 self.entries[start..]
1008 .iter()
1009 .take_while(|entry| entry.path == git_path)
1010 .find(|entry| entry.stage() == Stage::Normal)
1011 .map(|entry| {
1012 StatusTrackedKind::from_mode_and_skip(entry.mode, entry.is_skip_worktree())
1013 })
1014 }
1015
1016 fn tracked_directory_kind(&self, git_path: &[u8]) -> Option<StatusTrackedDirectoryKind> {
1017 let mut prefix_buf = self.directory_prefix.borrow_mut();
1018 prefix_buf.clear();
1019 prefix_buf.extend_from_slice(git_path);
1020 prefix_buf.push(b'/');
1021 let prefix = prefix_buf.as_slice();
1022 let start = self.entries.partition_point(|entry| entry.path < prefix);
1023 let mut saw_normal = false;
1024 for entry in self.entries[start..]
1025 .iter()
1026 .take_while(|entry| entry.path.starts_with(prefix))
1027 {
1028 if entry.stage() != Stage::Normal {
1029 continue;
1030 }
1031 saw_normal = true;
1032 if !entry.is_skip_worktree() {
1033 return Some(StatusTrackedDirectoryKind::ContainsTracked);
1034 }
1035 }
1036 saw_normal.then_some(StatusTrackedDirectoryKind::TrackedExcluded)
1037 }
1038}
1039
1040pub(crate) struct StatusUntrackedWalk<'a, T: StatusTrackedLookup + ?Sized> {
1041 git_dir: &'a Path,
1042 tracked: &'a T,
1043 ignores: &'a mut IgnoreMatcher,
1044 untracked_mode: StatusUntrackedMode,
1045 profile: Option<&'a mut StatusProfileCounters>,
1046}
1047
1048#[derive(Clone)]
1049pub(crate) struct StatusUntrackedFrontierTask {
1050 dir: PathBuf,
1051 git_path: Vec<u8>,
1052 ignores: IgnoreMatcher,
1053}
1054
1055#[derive(Default)]
1056pub(crate) struct StatusUntrackedFrontierOutput {
1057 paths: Vec<Vec<u8>>,
1058 next: Vec<StatusUntrackedFrontierTask>,
1059 profile: StatusProfileCounters,
1060}
1061
1062pub(crate) struct StatusDirEntry {
1063 name: std::ffi::OsString,
1064}
1065
1066impl StatusDirEntry {
1067 fn file_name(&self) -> std::ffi::OsString {
1068 self.name.clone()
1069 }
1070
1071 fn file_type_in(&self, dir: &Path) -> Result<fs::FileType> {
1072 Ok(fs::symlink_metadata(self.path_in(dir))?.file_type())
1073 }
1074
1075 fn path_in(&self, dir: &Path) -> PathBuf {
1076 dir.join(&self.name)
1077 }
1078}
1079
1080fn sort_status_dir_entries(entries: &mut [StatusDirEntry]) {
1081 entries.sort_by(|left, right| left.name.cmp(&right.name));
1082}
1083
1084pub(crate) fn collect_status_untracked_paths<T: StatusTrackedLookup + ?Sized>(
1085 context: &mut StatusUntrackedWalk<'_, T>,
1086 dir: &Path,
1087 dir_git_path: &[u8],
1088 paths: &mut Vec<Vec<u8>>,
1089) -> Result<()> {
1090 if is_same_path(dir, context.git_dir) {
1091 return Ok(());
1092 }
1093 let ignore_len = context.ignores.patterns.len();
1094 let mut entries = read_dir_entries_with_ignore_patterns(
1095 dir,
1096 dir_git_path,
1097 context.ignores,
1098 context.profile.as_deref_mut(),
1099 )?;
1100 sort_status_dir_entries(&mut entries);
1101 let result = (|| -> Result<()> {
1102 let mut git_path = dir_git_path.to_vec();
1103 for entry in entries {
1104 let file_name = entry.file_name();
1105 if file_name == std::ffi::OsStr::new(".git") {
1106 continue;
1107 }
1108 let path_len = git_path_push_component(&mut git_path, &file_name);
1109 let entry_result = (|| -> Result<()> {
1110 if let Some(tracked_kind) = context.tracked.tracked_kind(&git_path) {
1111 if let Some(profile) = context.profile.as_deref_mut() {
1112 profile.tracked_exact_hits += 1;
1113 }
1114 if !matches!(context.untracked_mode, StatusUntrackedMode::All)
1115 || tracked_kind == StatusTrackedKind::Gitlink
1116 {
1117 return Ok(());
1118 }
1119 if let Some(profile) = context.profile.as_deref_mut() {
1120 profile.file_type_calls += 1;
1121 }
1122 let file_type = entry.file_type_in(dir)?;
1123 if file_type.is_dir() {
1124 let path = entry.path_in(dir);
1125 if !is_same_path(&path, context.git_dir) {
1126 collect_status_untracked_paths(context, &path, &git_path, paths)?;
1127 }
1128 }
1129 return Ok(());
1130 }
1131 if let Some(profile) = context.profile.as_deref_mut() {
1132 profile.file_type_calls += 1;
1133 }
1134 let file_type = entry.file_type_in(dir)?;
1135 let is_dir = file_type.is_dir();
1136 if file_type.is_file() || file_type.is_symlink() {
1137 if !context.ignores.is_ignored_profiled(
1138 &git_path,
1139 false,
1140 context.profile.as_deref_mut(),
1141 ) {
1142 paths.push(git_path.clone());
1143 }
1144 return Ok(());
1145 } else if is_dir {
1146 let path = entry.path_in(dir);
1147 if context.ignores.is_ignored_profiled(
1148 &git_path,
1149 true,
1150 context.profile.as_deref_mut(),
1151 ) {
1152 return Ok(());
1153 }
1154 if is_same_path(&path, context.git_dir) {
1155 return Ok(());
1156 }
1157 let tracked_directory = context.tracked.tracked_directory_kind(&git_path);
1158 if let Some(directory_kind) = tracked_directory {
1159 if let Some(profile) = context.profile.as_deref_mut() {
1160 profile.tracked_dir_prefix_hits += 1;
1161 if directory_kind == StatusTrackedDirectoryKind::TrackedExcluded {
1162 profile.tracked_skip_worktree_prefix_hits += 1;
1163 }
1164 }
1165 }
1166 match context.untracked_mode {
1167 StatusUntrackedMode::All => {
1168 if tracked_directory.is_none()
1169 && is_nested_repository_boundary(&path, context.git_dir)
1170 {
1171 push_untracked_directory(paths, &git_path);
1172 } else {
1173 collect_status_untracked_paths(context, &path, &git_path, paths)?;
1174 }
1175 }
1176 StatusUntrackedMode::Normal => {
1177 if tracked_directory.is_some() {
1178 collect_status_untracked_paths(context, &path, &git_path, paths)?;
1179 } else if is_nested_repository_boundary(&path, context.git_dir) {
1180 push_untracked_directory(paths, &git_path);
1181 } else if status_untracked_directory_has_file(
1182 context, &path, &git_path,
1183 )? {
1184 push_untracked_directory(paths, &git_path);
1185 }
1186 }
1187 StatusUntrackedMode::None => {}
1188 }
1189 }
1190 Ok(())
1191 })();
1192 git_path.truncate(path_len);
1193 entry_result?;
1194 }
1195 Ok(())
1196 })();
1197 context.ignores.truncate(ignore_len);
1198 result
1199}
1200
1201pub(crate) fn collect_status_untracked_frontier_dir<T: StatusTrackedLookup + ?Sized>(
1202 context: &mut StatusUntrackedWalk<'_, T>,
1203 dir: &Path,
1204 dir_git_path: &[u8],
1205 paths: &mut Vec<Vec<u8>>,
1206 next: &mut Vec<StatusUntrackedFrontierTask>,
1207) -> Result<()> {
1208 if is_same_path(dir, context.git_dir) {
1209 return Ok(());
1210 }
1211 let mut entries = read_dir_entries_with_ignore_patterns(
1212 dir,
1213 dir_git_path,
1214 context.ignores,
1215 context.profile.as_deref_mut(),
1216 )?;
1217 sort_status_dir_entries(&mut entries);
1218 let mut git_path = dir_git_path.to_vec();
1219 for entry in entries {
1220 let file_name = entry.file_name();
1221 if file_name == std::ffi::OsStr::new(".git") {
1222 continue;
1223 }
1224 let path_len = git_path_push_component(&mut git_path, &file_name);
1225 let entry_result = (|| -> Result<()> {
1226 if let Some(tracked_kind) = context.tracked.tracked_kind(&git_path) {
1227 if let Some(profile) = context.profile.as_deref_mut() {
1228 profile.tracked_exact_hits += 1;
1229 }
1230 if !matches!(context.untracked_mode, StatusUntrackedMode::All)
1231 || tracked_kind == StatusTrackedKind::Gitlink
1232 {
1233 return Ok(());
1234 }
1235 if let Some(profile) = context.profile.as_deref_mut() {
1236 profile.file_type_calls += 1;
1237 }
1238 let file_type = entry.file_type_in(dir)?;
1239 if file_type.is_dir() {
1240 let path = entry.path_in(dir);
1241 if !is_same_path(&path, context.git_dir) {
1242 next.push(StatusUntrackedFrontierTask {
1243 dir: path,
1244 git_path: git_path.clone(),
1245 ignores: context.ignores.clone(),
1246 });
1247 }
1248 }
1249 return Ok(());
1250 }
1251 if let Some(profile) = context.profile.as_deref_mut() {
1252 profile.file_type_calls += 1;
1253 }
1254 let file_type = entry.file_type_in(dir)?;
1255 let is_dir = file_type.is_dir();
1256 if file_type.is_file() || file_type.is_symlink() {
1257 if !context.ignores.is_ignored_profiled(
1258 &git_path,
1259 false,
1260 context.profile.as_deref_mut(),
1261 ) {
1262 paths.push(git_path.clone());
1263 }
1264 return Ok(());
1265 } else if is_dir {
1266 let path = entry.path_in(dir);
1267 if context.ignores.is_ignored_profiled(
1268 &git_path,
1269 true,
1270 context.profile.as_deref_mut(),
1271 ) {
1272 return Ok(());
1273 }
1274 if is_same_path(&path, context.git_dir) {
1275 return Ok(());
1276 }
1277 let tracked_directory = context.tracked.tracked_directory_kind(&git_path);
1278 if let Some(directory_kind) = tracked_directory {
1279 if let Some(profile) = context.profile.as_deref_mut() {
1280 profile.tracked_dir_prefix_hits += 1;
1281 if directory_kind == StatusTrackedDirectoryKind::TrackedExcluded {
1282 profile.tracked_skip_worktree_prefix_hits += 1;
1283 }
1284 }
1285 }
1286 match context.untracked_mode {
1287 StatusUntrackedMode::All => {
1288 if tracked_directory.is_none()
1289 && is_nested_repository_boundary(&path, context.git_dir)
1290 {
1291 push_untracked_directory(paths, &git_path);
1292 } else {
1293 next.push(StatusUntrackedFrontierTask {
1294 dir: path,
1295 git_path: git_path.clone(),
1296 ignores: context.ignores.clone(),
1297 });
1298 }
1299 }
1300 StatusUntrackedMode::Normal => {
1301 if tracked_directory.is_some() {
1302 next.push(StatusUntrackedFrontierTask {
1303 dir: path,
1304 git_path: git_path.clone(),
1305 ignores: context.ignores.clone(),
1306 });
1307 } else if is_nested_repository_boundary(&path, context.git_dir)
1308 || status_untracked_directory_has_file(context, &path, &git_path)?
1309 {
1310 push_untracked_directory(paths, &git_path);
1311 }
1312 }
1313 StatusUntrackedMode::None => {}
1314 }
1315 }
1316 Ok(())
1317 })();
1318 git_path.truncate(path_len);
1319 entry_result?;
1320 }
1321 Ok(())
1322}
1323
1324pub(crate) fn stream_status_untracked_paths<T, F>(
1325 context: &mut StatusUntrackedWalk<'_, T>,
1326 dir: &Path,
1327 dir_git_path: &[u8],
1328 emit: &mut F,
1329) -> Result<StreamControl>
1330where
1331 T: StatusTrackedLookup + ?Sized,
1332 F: for<'a> FnMut(&'a [u8]) -> Result<StreamControl>,
1333{
1334 if is_same_path(dir, context.git_dir) {
1335 return Ok(StreamControl::Continue);
1336 }
1337 let ignore_len = context.ignores.patterns.len();
1338 let mut entries = read_dir_entries_with_ignore_patterns(
1339 dir,
1340 dir_git_path,
1341 context.ignores,
1342 context.profile.as_deref_mut(),
1343 )?;
1344 sort_status_dir_entries(&mut entries);
1345 let result = (|| -> Result<StreamControl> {
1346 let mut git_path = dir_git_path.to_vec();
1347 for entry in entries {
1348 let file_name = entry.file_name();
1349 if file_name == std::ffi::OsStr::new(".git") {
1350 continue;
1351 }
1352 let path_len = git_path_push_component(&mut git_path, &file_name);
1353 let entry_result = (|| -> Result<StreamControl> {
1354 if let Some(tracked_kind) = context.tracked.tracked_kind(&git_path) {
1355 if let Some(profile) = context.profile.as_deref_mut() {
1356 profile.tracked_exact_hits += 1;
1357 }
1358 if !matches!(context.untracked_mode, StatusUntrackedMode::All)
1359 || tracked_kind == StatusTrackedKind::Gitlink
1360 {
1361 return Ok(StreamControl::Continue);
1362 }
1363 if let Some(profile) = context.profile.as_deref_mut() {
1364 profile.file_type_calls += 1;
1365 }
1366 let file_type = entry.file_type_in(dir)?;
1367 if file_type.is_dir() {
1368 let path = entry.path_in(dir);
1369 if !is_same_path(&path, context.git_dir) {
1370 if stream_status_untracked_paths(context, &path, &git_path, emit)?
1371 .is_stop()
1372 {
1373 return Ok(StreamControl::Stop);
1374 }
1375 }
1376 }
1377 return Ok(StreamControl::Continue);
1378 }
1379 if let Some(profile) = context.profile.as_deref_mut() {
1380 profile.file_type_calls += 1;
1381 }
1382 let file_type = entry.file_type_in(dir)?;
1383 let is_dir = file_type.is_dir();
1384 if file_type.is_file() || file_type.is_symlink() {
1385 if !context.ignores.is_ignored_profiled(
1386 &git_path,
1387 false,
1388 context.profile.as_deref_mut(),
1389 ) {
1390 if emit_status_untracked_path(context, &git_path, emit)?.is_stop() {
1391 return Ok(StreamControl::Stop);
1392 }
1393 }
1394 return Ok(StreamControl::Continue);
1395 } else if is_dir {
1396 if context.ignores.is_ignored_profiled(
1397 &git_path,
1398 true,
1399 context.profile.as_deref_mut(),
1400 ) {
1401 return Ok(StreamControl::Continue);
1402 }
1403 let path = entry.path_in(dir);
1404 if is_same_path(&path, context.git_dir) {
1405 return Ok(StreamControl::Continue);
1406 }
1407 let tracked_directory = context.tracked.tracked_directory_kind(&git_path);
1408 if let Some(directory_kind) = tracked_directory {
1409 if let Some(profile) = context.profile.as_deref_mut() {
1410 profile.tracked_dir_prefix_hits += 1;
1411 if directory_kind == StatusTrackedDirectoryKind::TrackedExcluded {
1412 profile.tracked_skip_worktree_prefix_hits += 1;
1413 }
1414 }
1415 }
1416 match context.untracked_mode {
1417 StatusUntrackedMode::All => {
1418 if tracked_directory.is_none()
1419 && is_nested_repository_boundary(&path, context.git_dir)
1420 {
1421 let directory_len = git_path.len();
1422 if git_path.last() != Some(&b'/') {
1423 git_path.push(b'/');
1424 }
1425 let control = emit_status_untracked_path(context, &git_path, emit)?;
1426 git_path.truncate(directory_len);
1427 if control.is_stop() {
1428 return Ok(StreamControl::Stop);
1429 }
1430 } else {
1431 if stream_status_untracked_paths(context, &path, &git_path, emit)?
1432 .is_stop()
1433 {
1434 return Ok(StreamControl::Stop);
1435 }
1436 }
1437 }
1438 StatusUntrackedMode::Normal => {
1439 if tracked_directory.is_some() {
1440 if stream_status_untracked_paths(context, &path, &git_path, emit)?
1441 .is_stop()
1442 {
1443 return Ok(StreamControl::Stop);
1444 }
1445 } else if is_nested_repository_boundary(&path, context.git_dir)
1446 || status_untracked_directory_has_file(context, &path, &git_path)?
1447 {
1448 let directory_len = git_path.len();
1449 if git_path.last() != Some(&b'/') {
1450 git_path.push(b'/');
1451 }
1452 let control = emit_status_untracked_path(context, &git_path, emit)?;
1453 git_path.truncate(directory_len);
1454 if control.is_stop() {
1455 return Ok(StreamControl::Stop);
1456 }
1457 }
1458 }
1459 StatusUntrackedMode::None => {}
1460 }
1461 }
1462 Ok(StreamControl::Continue)
1463 })();
1464 git_path.truncate(path_len);
1465 if entry_result?.is_stop() {
1466 return Ok(StreamControl::Stop);
1467 }
1468 }
1469 Ok(StreamControl::Continue)
1470 })();
1471 context.ignores.truncate(ignore_len);
1472 result
1473}
1474
1475pub(crate) fn emit_status_untracked_path<T, F>(
1476 context: &mut StatusUntrackedWalk<'_, T>,
1477 path: &[u8],
1478 emit: &mut F,
1479) -> Result<StreamControl>
1480where
1481 T: StatusTrackedLookup + ?Sized,
1482 F: for<'a> FnMut(&'a [u8]) -> Result<StreamControl>,
1483{
1484 if let Some(profile) = context.profile.as_deref_mut() {
1485 profile.untracked_rows += 1;
1486 }
1487 emit(path)
1488}
1489
1490pub(crate) fn stage0_tracked_directories(index: &Index) -> HashSet<&[u8]> {
1491 let mut directories = HashSet::new();
1492 for entry in index
1493 .entries
1494 .iter()
1495 .filter(|entry| entry.stage() == Stage::Normal)
1496 {
1497 let path = entry.path.as_bytes();
1498 for (idx, byte) in path.iter().enumerate() {
1499 if *byte == b'/' && idx > 0 {
1500 directories.insert(&path[..idx]);
1501 }
1502 }
1503 }
1504 directories
1505}
1506
1507pub(crate) fn status_untracked_directory_has_file<T: StatusTrackedLookup + ?Sized>(
1508 context: &mut StatusUntrackedWalk<'_, T>,
1509 dir: &Path,
1510 dir_git_path: &[u8],
1511) -> Result<bool> {
1512 if is_same_path(dir, context.git_dir) {
1513 return Ok(false);
1514 }
1515 let ignore_len = context.ignores.patterns.len();
1516 let mut entries = read_dir_entries_with_ignore_patterns(
1517 dir,
1518 dir_git_path,
1519 context.ignores,
1520 context.profile.as_deref_mut(),
1521 )?;
1522 sort_status_dir_entries(&mut entries);
1523 let result = (|| -> Result<bool> {
1524 let mut git_path = dir_git_path.to_vec();
1525 for entry in entries {
1526 let file_name = entry.file_name();
1527 if file_name == std::ffi::OsStr::new(".git") {
1528 continue;
1529 }
1530 let path_len = git_path_push_component(&mut git_path, &file_name);
1531 let entry_result = (|| -> Result<Option<bool>> {
1532 if let Some(profile) = context.profile.as_deref_mut() {
1533 profile.file_type_calls += 1;
1534 }
1535 let file_type = entry.file_type_in(dir)?;
1536 let is_dir = file_type.is_dir();
1537 if context.ignores.is_ignored_profiled(
1538 &git_path,
1539 is_dir,
1540 context.profile.as_deref_mut(),
1541 ) {
1542 return Ok(None);
1543 }
1544 if file_type.is_file() || file_type.is_symlink() {
1545 return Ok(Some(true));
1546 }
1547 if is_dir {
1548 let path = entry.path_in(dir);
1549 if is_same_path(&path, context.git_dir) {
1550 return Ok(None);
1551 }
1552 if is_nested_repository_boundary(&path, context.git_dir) {
1553 return Ok(Some(true));
1554 }
1555 if status_untracked_directory_has_file(context, &path, &git_path)? {
1556 return Ok(Some(true));
1557 }
1558 }
1559 Ok(None)
1560 })();
1561 git_path.truncate(path_len);
1562 if let Some(has_file) = entry_result? {
1563 return Ok(has_file);
1564 }
1565 }
1566 Ok(false)
1567 })();
1568 context.ignores.truncate(ignore_len);
1569 result
1570}
1571
1572pub(crate) fn read_dir_entries_with_ignore_patterns(
1573 dir: &Path,
1574 base: &[u8],
1575 matcher: &mut IgnoreMatcher,
1576 mut profile: Option<&mut StatusProfileCounters>,
1577) -> Result<Vec<StatusDirEntry>> {
1578 let mut entries = Vec::new();
1579 let mut ignore_path = None;
1580 if let Some(profile) = profile.as_deref_mut() {
1581 profile.read_dir_calls += 1;
1582 }
1583 for entry in fs::read_dir(dir)? {
1584 let entry = entry?;
1585 let name = entry.file_name();
1586 if let Some(profile) = profile.as_deref_mut() {
1587 profile.dir_entries_seen += 1;
1588 }
1589 if name == std::ffi::OsStr::new(".gitignore") {
1590 ignore_path = Some(dir.join(&name));
1591 }
1592 entries.push(StatusDirEntry { name });
1593 }
1594 if let Some(profile) = profile {
1595 profile.read_dir_entry_vec_cap_bytes +=
1596 (entries.capacity() * std::mem::size_of::<StatusDirEntry>()) as u64;
1597 profile.read_dir_entry_vec_max_len =
1598 profile.read_dir_entry_vec_max_len.max(entries.len() as u64);
1599 profile.read_dir_entry_vec_max_cap = profile
1600 .read_dir_entry_vec_max_cap
1601 .max(entries.capacity() as u64);
1602 profile.read_dir_name_vec_cap_bytes += entries
1603 .iter()
1604 .map(|entry| entry.name.capacity() as u64)
1605 .sum::<u64>();
1606 profile.read_dir_name_vec_max_len = profile.read_dir_name_vec_max_len.max(
1607 entries
1608 .iter()
1609 .map(|entry| entry.name.len() as u64)
1610 .max()
1611 .unwrap_or(0),
1612 );
1613 profile.read_dir_name_vec_max_cap = profile.read_dir_name_vec_max_cap.max(
1614 entries
1615 .iter()
1616 .map(|entry| entry.name.capacity() as u64)
1617 .max()
1618 .unwrap_or(0),
1619 );
1620 }
1621 if let Some(path) = ignore_path {
1622 let mut source = base.to_vec();
1623 if !source.is_empty() {
1624 source.push(b'/');
1625 }
1626 source.extend_from_slice(b".gitignore");
1627 read_per_directory_ignore_patterns_into_matcher(path, matcher, base, &source)?;
1628 }
1629 Ok(entries)
1630}
1631
1632pub(crate) fn build_untracked_cache(
1633 worktree_root: &Path,
1634 git_dir: &Path,
1635 format: ObjectFormat,
1636 index: &Index,
1637 untracked_mode: StatusUntrackedMode,
1638) -> Result<UntrackedCache> {
1639 let stat_cache = IndexStatCache::from_index(index, &repository_index_path(git_dir));
1640 let tracked_dirs = stage0_tracked_directories(index);
1641 let tracked = IndexStatusLookup {
1642 stat_cache: &stat_cache,
1643 tracked_dirs: &tracked_dirs,
1644 };
1645 let mut ignores = IgnoreMatcher::from_worktree_base(worktree_root)?;
1646 let mut cache = UntrackedCache::new(
1647 format,
1648 untracked_cache_ident(worktree_root),
1649 untracked_cache_dir_flags(untracked_mode),
1650 );
1651 cache.info_exclude = untracked_cache_oid_stat(&git_dir.join("info").join("exclude"), format)?;
1652 cache.excludes_file = UntrackedCacheOidStat::new(format);
1653 cache.root = Some(build_untracked_cache_dir(
1654 worktree_root,
1655 git_dir,
1656 worktree_root,
1657 &[],
1658 b"",
1659 &tracked,
1660 &mut ignores,
1661 untracked_mode,
1662 format,
1663 false,
1664 )?);
1665 Ok(cache)
1666}
1667
1668pub(crate) fn emit_untracked_cache_trace(old: Option<&UntrackedCache>, new: &UntrackedCache) {
1669 sley_core::trace2::perf_read_directory_data("path", "");
1670 let dir_count = new
1671 .root
1672 .as_ref()
1673 .map(count_untracked_cache_dirs)
1674 .unwrap_or(0);
1675 let Some(old) = old else {
1676 sley_core::trace2::perf_read_directory_data("node-creation", dir_count.saturating_sub(1));
1677 sley_core::trace2::perf_read_directory_data("gitignore-invalidation", 1);
1678 sley_core::trace2::perf_read_directory_data("directory-invalidation", 0);
1679 sley_core::trace2::perf_read_directory_data("opendir", dir_count);
1680 return;
1681 };
1682 let Some(old_root) = old.root.as_ref() else {
1683 sley_core::trace2::perf_read_directory_data("node-creation", dir_count.saturating_sub(1));
1684 sley_core::trace2::perf_read_directory_data("gitignore-invalidation", 1);
1685 sley_core::trace2::perf_read_directory_data("directory-invalidation", 0);
1686 sley_core::trace2::perf_read_directory_data("opendir", dir_count);
1687 return;
1688 };
1689 let Some(new_root) = new.root.as_ref() else {
1690 return;
1691 };
1692 if old.ident != new.ident || old.dir_flags != new.dir_flags {
1693 sley_core::trace2::perf_read_directory_data("node-creation", dir_count.saturating_sub(1));
1694 sley_core::trace2::perf_read_directory_data("gitignore-invalidation", 1);
1695 sley_core::trace2::perf_read_directory_data("directory-invalidation", 0);
1696 sley_core::trace2::perf_read_directory_data("opendir", dir_count);
1697 return;
1698 }
1699 if old.info_exclude.oid != new.info_exclude.oid
1700 || old.excludes_file.oid != new.excludes_file.oid
1701 {
1702 sley_core::trace2::perf_read_directory_data("node-creation", 0);
1703 sley_core::trace2::perf_read_directory_data("gitignore-invalidation", 1);
1704 sley_core::trace2::perf_read_directory_data("directory-invalidation", 0);
1705 sley_core::trace2::perf_read_directory_data("opendir", dir_count);
1706 return;
1707 }
1708 if old_root.exclude_oid != new_root.exclude_oid {
1709 sley_core::trace2::perf_read_directory_data("node-creation", 0);
1710 sley_core::trace2::perf_read_directory_data("gitignore-invalidation", 1);
1711 sley_core::trace2::perf_read_directory_data("directory-invalidation", 1);
1712 sley_core::trace2::perf_read_directory_data("opendir", dir_count);
1713 return;
1714 }
1715 let invalid_dir_count = count_invalid_untracked_cache_dirs(old_root);
1716 if invalid_dir_count > 0 {
1717 sley_core::trace2::perf_read_directory_data("node-creation", 0);
1718 sley_core::trace2::perf_read_directory_data("gitignore-invalidation", 0);
1719 sley_core::trace2::perf_read_directory_data("directory-invalidation", 0);
1720 sley_core::trace2::perf_read_directory_data("opendir", invalid_dir_count);
1721 return;
1722 }
1723 if old_root.stat != new_root.stat {
1724 sley_core::trace2::perf_read_directory_data("node-creation", 0);
1725 sley_core::trace2::perf_read_directory_data("gitignore-invalidation", 0);
1726 sley_core::trace2::perf_read_directory_data("directory-invalidation", 1);
1727 sley_core::trace2::perf_read_directory_data("opendir", 1);
1728 return;
1729 }
1730 if old.root == new.root {
1731 sley_core::trace2::perf_read_directory_data("node-creation", 0);
1732 sley_core::trace2::perf_read_directory_data("gitignore-invalidation", 0);
1733 sley_core::trace2::perf_read_directory_data("directory-invalidation", 0);
1734 sley_core::trace2::perf_read_directory_data("opendir", 0);
1735 return;
1736 }
1737 sley_core::trace2::perf_read_directory_data("node-creation", 0);
1738 sley_core::trace2::perf_read_directory_data("gitignore-invalidation", 0);
1739 sley_core::trace2::perf_read_directory_data("directory-invalidation", 1);
1740 sley_core::trace2::perf_read_directory_data("opendir", dir_count);
1741}
1742
1743pub(crate) fn count_untracked_cache_dirs(dir: &UntrackedCacheDir) -> usize {
1744 1 + dir
1745 .dirs
1746 .iter()
1747 .map(count_untracked_cache_dirs)
1748 .sum::<usize>()
1749}
1750
1751pub(crate) fn count_invalid_untracked_cache_dirs(dir: &UntrackedCacheDir) -> usize {
1752 usize::from(!dir.valid)
1753 + dir
1754 .dirs
1755 .iter()
1756 .map(count_invalid_untracked_cache_dirs)
1757 .sum::<usize>()
1758}
1759
1760#[allow(clippy::too_many_arguments)]
1761pub(crate) fn build_untracked_cache_dir<T: StatusTrackedLookup + ?Sized>(
1762 worktree_root: &Path,
1763 git_dir: &Path,
1764 dir: &Path,
1765 dir_git_path: &[u8],
1766 name: &[u8],
1767 tracked: &T,
1768 ignores: &mut IgnoreMatcher,
1769 untracked_mode: StatusUntrackedMode,
1770 format: ObjectFormat,
1771 check_only: bool,
1772) -> Result<UntrackedCacheDir> {
1773 let ignore_len = ignores.patterns.len();
1774 let mut entries = read_dir_entries_with_ignore_patterns(dir, dir_git_path, ignores, None)?;
1775 sort_status_dir_entries(&mut entries);
1776 let exclude_path = if dir_git_path.is_empty() {
1777 b".gitignore".to_vec()
1778 } else {
1779 let mut path = dir_git_path.to_vec();
1780 path.push(b'/');
1781 path.extend_from_slice(b".gitignore");
1782 path
1783 };
1784 let exclude_oid = if tracked.tracked_kind(&exclude_path).is_some() {
1785 None
1786 } else {
1787 per_directory_ignore_oid(dir, format)?
1788 };
1789 let mut node = UntrackedCacheDir {
1790 name: name.to_vec(),
1791 stat: fs::symlink_metadata(dir)
1792 .map(|metadata| untracked_cache_stat_data(&metadata))
1793 .unwrap_or_default(),
1794 exclude_oid,
1795 valid: true,
1796 check_only,
1797 recurse: true,
1798 ..UntrackedCacheDir::default()
1799 };
1800 let result = (|| -> Result<()> {
1801 let mut git_path = dir_git_path.to_vec();
1802 for entry in entries {
1803 let file_name = entry.file_name();
1804 if file_name == std::ffi::OsStr::new(".git") {
1805 continue;
1806 }
1807 let path_len = git_path_push_component(&mut git_path, &file_name);
1808 let entry_result = (|| -> Result<()> {
1809 if tracked.tracked_kind(&git_path).is_some() {
1810 return Ok(());
1811 }
1812 let file_type = entry.file_type_in(dir)?;
1813 let is_dir = file_type.is_dir();
1814 if ignores.is_ignored(&git_path, is_dir) {
1815 return Ok(());
1816 }
1817 if file_type.is_file() || file_type.is_symlink() {
1818 node.untracked.push(component_name_bytes(&file_name));
1819 return Ok(());
1820 }
1821 if !is_dir {
1822 return Ok(());
1823 }
1824 let path = entry.path_in(dir);
1825 if is_same_path(&path, git_dir) {
1826 return Ok(());
1827 }
1828 let component = component_name_bytes(&file_name);
1829 let tracked_directory = tracked.tracked_directory_kind(&git_path);
1830 let child_check_only = matches!(untracked_mode, StatusUntrackedMode::Normal)
1831 && tracked_directory.is_none();
1832 let child = build_untracked_cache_dir(
1833 worktree_root,
1834 git_dir,
1835 &path,
1836 &git_path,
1837 &component,
1838 tracked,
1839 ignores,
1840 untracked_mode,
1841 format,
1842 child_check_only,
1843 )?;
1844 let child_has_untracked = !child.untracked.is_empty()
1845 || child
1846 .dirs
1847 .iter()
1848 .any(|dir| !dir.untracked.is_empty() || !dir.dirs.is_empty());
1849 match untracked_mode {
1850 StatusUntrackedMode::All => {
1851 node.dirs.push(child);
1852 }
1853 StatusUntrackedMode::Normal => {
1854 if tracked_directory.is_some() {
1855 node.dirs.push(child);
1856 } else {
1857 if child_has_untracked {
1858 let mut directory = component.clone();
1859 directory.push(b'/');
1860 node.untracked.push(directory);
1861 }
1862 node.dirs.push(child);
1863 }
1864 }
1865 StatusUntrackedMode::None => {}
1866 }
1867 Ok(())
1868 })();
1869 git_path.truncate(path_len);
1870 entry_result?;
1871 }
1872 Ok(())
1873 })();
1874 ignores.truncate(ignore_len);
1875 result?;
1876 if worktree_root == dir {
1877 node.name.clear();
1878 }
1879 Ok(node)
1880}
1881
1882pub(crate) fn component_name_bytes(name: &std::ffi::OsStr) -> Vec<u8> {
1883 #[cfg(unix)]
1884 {
1885 use std::os::unix::ffi::OsStrExt;
1886 name.as_bytes().to_vec()
1887 }
1888 #[cfg(not(unix))]
1889 {
1890 name.to_string_lossy().as_bytes().to_vec()
1891 }
1892}
1893
1894pub(crate) fn per_directory_ignore_oid(
1895 dir: &Path,
1896 format: ObjectFormat,
1897) -> Result<Option<ObjectId>> {
1898 let path = dir.join(".gitignore");
1899 match fs::read(&path) {
1900 Ok(bytes) => Ok(Some(untracked_cache_exclude_oid(bytes, format)?)),
1901 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
1902 Err(err) => Err(err.into()),
1903 }
1904}
1905
1906pub(crate) fn untracked_cache_oid_stat(
1907 path: &Path,
1908 format: ObjectFormat,
1909) -> Result<UntrackedCacheOidStat> {
1910 let stat = fs::symlink_metadata(path)
1911 .map(|metadata| untracked_cache_stat_data(&metadata))
1912 .unwrap_or_default();
1913 let oid = match fs::read(path) {
1914 Ok(bytes) => untracked_cache_exclude_oid(bytes, format)?,
1915 Err(err) if err.kind() == std::io::ErrorKind::NotFound => ObjectId::null(format),
1916 Err(err) => return Err(err.into()),
1917 };
1918 Ok(UntrackedCacheOidStat { stat, oid })
1919}
1920
1921pub(crate) fn untracked_cache_exclude_oid(
1922 mut bytes: Vec<u8>,
1923 format: ObjectFormat,
1924) -> Result<ObjectId> {
1925 if !bytes.is_empty() {
1926 bytes.push(b'\n');
1927 }
1928 EncodedObject::new(ObjectType::Blob, bytes).object_id(format)
1929}
1930
1931#[cfg(unix)]
1932pub(crate) fn untracked_cache_stat_data(metadata: &fs::Metadata) -> UntrackedCacheStatData {
1933 use std::os::unix::fs::MetadataExt;
1934 UntrackedCacheStatData {
1935 ctime_seconds: metadata.ctime().min(u32::MAX as i64).max(0) as u32,
1936 ctime_nanoseconds: metadata.ctime_nsec().min(u32::MAX as i64).max(0) as u32,
1937 mtime_seconds: metadata.mtime().min(u32::MAX as i64).max(0) as u32,
1938 mtime_nanoseconds: metadata.mtime_nsec().min(u32::MAX as i64).max(0) as u32,
1939 dev: metadata.dev() as u32,
1940 ino: metadata.ino() as u32,
1941 uid: metadata.uid(),
1942 gid: metadata.gid(),
1943 size: metadata.size().min(u32::MAX as u64) as u32,
1944 }
1945}
1946
1947#[cfg(not(unix))]
1948pub(crate) fn untracked_cache_stat_data(metadata: &fs::Metadata) -> UntrackedCacheStatData {
1949 let (mtime_seconds, mtime_nanoseconds) = file_mtime_parts(metadata).unwrap_or((0, 0));
1950 UntrackedCacheStatData {
1951 mtime_seconds: mtime_seconds.min(u64::from(u32::MAX)) as u32,
1952 mtime_nanoseconds: mtime_nanoseconds.min(u64::from(u32::MAX)) as u32,
1953 size: metadata.len().min(u64::from(u32::MAX)) as u32,
1954 ..UntrackedCacheStatData::default()
1955 }
1956}
1957
1958pub(crate) fn untracked_cache_dir_flags(untracked_mode: StatusUntrackedMode) -> u32 {
1959 match untracked_mode {
1960 StatusUntrackedMode::All => 0,
1961 StatusUntrackedMode::Normal | StatusUntrackedMode::None => {
1962 sley_index::untracked_cache_normal_flags()
1963 }
1964 }
1965}
1966
1967pub(crate) fn untracked_cache_ident(worktree_root: &Path) -> Vec<u8> {
1968 let mut ident = format!(
1969 "Location {}, system {}",
1970 worktree_root.display(),
1971 untracked_cache_system_name()
1972 )
1973 .into_bytes();
1974 ident.push(0);
1975 ident
1976}
1977
1978pub(crate) fn untracked_cache_system_name() -> String {
1979 fs::read_to_string("/proc/sys/kernel/ostype")
1980 .ok()
1981 .map(|name| name.trim().to_string())
1982 .filter(|name| !name.is_empty())
1983 .unwrap_or_else(|| {
1984 let os = std::env::consts::OS;
1985 let mut chars = os.chars();
1986 match chars.next() {
1987 Some(first) => first.to_uppercase().chain(chars).collect(),
1988 None => "Unknown".to_string(),
1989 }
1990 })
1991}
1992
1993pub(crate) fn push_untracked_directory(paths: &mut Vec<Vec<u8>>, git_path: &[u8]) {
1994 paths.push(untracked_directory_path(git_path));
1995}
1996
1997pub(crate) fn untracked_directory_path(git_path: &[u8]) -> Vec<u8> {
1998 let mut directory = git_path.to_vec();
1999 if directory.last() != Some(&b'/') {
2000 directory.push(b'/');
2001 }
2002 directory
2003}
2004
2005pub(crate) fn untracked_normal_rollup_path(
2006 file_path: &[u8],
2007 index: &BTreeMap<Vec<u8>, TrackedEntry>,
2008 ignores: &IgnoreMatcher,
2009) -> Vec<u8> {
2010 let segments = file_path
2011 .split(|byte| *byte == b'/')
2012 .filter(|segment| !segment.is_empty())
2013 .collect::<Vec<_>>();
2014 if segments.len() <= 1 {
2015 return file_path.to_vec();
2016 }
2017 let mut prefix = Vec::new();
2018 for segment in &segments[..segments.len() - 1] {
2019 if !prefix.is_empty() {
2020 prefix.push(b'/');
2021 }
2022 prefix.extend_from_slice(segment);
2023 if index_has_path_under(index, &prefix) {
2024 break;
2025 }
2026 if !ignores.is_ignored(&prefix, true) {
2027 let mut directory = prefix;
2028 directory.push(b'/');
2029 return directory;
2030 }
2031 }
2032 file_path.to_vec()
2033}
2034
2035pub(crate) fn ignored_traditional_rollup_path(
2036 root: &Path,
2037 git_dir: &Path,
2038 path: &[u8],
2039 index: &BTreeMap<Vec<u8>, TrackedEntry>,
2040 ignores: &IgnoreMatcher,
2041) -> Result<Vec<u8>> {
2042 let rolled = untracked_normal_rollup_path(path, index, ignores);
2043 if rolled == path {
2044 return Ok(rolled);
2045 }
2046 let Some(directory_path) = rolled.strip_suffix(b"/") else {
2047 return Ok(rolled);
2048 };
2049 if ignores.is_ignored(directory_path, true) {
2050 return Ok(rolled);
2051 }
2052 let mut absolute = PathBuf::new();
2053 set_worktree_path_from_repo_path(root, directory_path, &mut absolute)?;
2054 if directory_has_file(&absolute, root, git_dir, ignores)? {
2055 return Ok(path.to_vec());
2056 }
2057 Ok(rolled)
2058}
2059
2060pub(crate) fn directory_has_file(
2061 dir: &Path,
2062 root: &Path,
2063 git_dir: &Path,
2064 ignores: &IgnoreMatcher,
2065) -> Result<bool> {
2066 if is_same_path(dir, git_dir) {
2067 return Ok(false);
2068 }
2069 for entry in fs::read_dir(dir)? {
2070 let entry = entry?;
2071 let path = entry.path();
2072 if is_dot_git_entry(&path) {
2073 continue;
2074 }
2075 if is_embedded_git_internals(root, &path) {
2076 continue;
2077 }
2078 if is_same_path(&path, git_dir) {
2079 continue;
2080 }
2081 let metadata = entry.metadata()?;
2082 let relative = path.strip_prefix(root).map_err(|_| {
2083 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
2084 })?;
2085 let git_path = git_path_bytes(relative)?;
2086 if ignores.is_ignored(&git_path, metadata.is_dir()) {
2087 continue;
2088 }
2089 if metadata.is_file() || metadata.file_type().is_symlink() {
2090 return Ok(true);
2091 }
2092 if metadata.is_dir() {
2093 if is_nested_repository_boundary(&path, git_dir) {
2094 continue;
2095 }
2096 if directory_has_file(&path, root, git_dir, ignores)? {
2097 return Ok(true);
2098 }
2099 }
2100 }
2101 Ok(false)
2102}
2103
2104pub(crate) fn directory_has_ignored(
2105 dir: &Path,
2106 root: &Path,
2107 git_dir: &Path,
2108 ignores: &IgnoreMatcher,
2109) -> Result<bool> {
2110 if is_same_path(dir, git_dir) {
2111 return Ok(false);
2112 }
2113 for entry in fs::read_dir(dir)? {
2114 let entry = entry?;
2115 let path = entry.path();
2116 if is_dot_git_entry(&path) {
2117 continue;
2118 }
2119 if is_same_path(&path, git_dir) {
2120 continue;
2121 }
2122 let metadata = entry.metadata()?;
2123 let relative = path.strip_prefix(root).map_err(|_| {
2124 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
2125 })?;
2126 let git_path = git_path_bytes(relative)?;
2127 if ignores.is_ignored(&git_path, metadata.is_dir()) {
2128 return Ok(true);
2129 }
2130 if metadata.is_dir() && directory_has_ignored(&path, root, git_dir, ignores)? {
2131 return Ok(true);
2132 }
2133 }
2134 Ok(false)
2135}
2136
2137pub(crate) fn ignored_untracked_paths(
2138 root: &Path,
2139 git_dir: &Path,
2140 index: &BTreeMap<Vec<u8>, TrackedEntry>,
2141 ignores: &IgnoreMatcher,
2142 directory: bool,
2143) -> Result<Vec<Vec<u8>>> {
2144 let mut paths = BTreeSet::new();
2145 let context = IgnoredUntrackedContext {
2146 root,
2147 git_dir,
2148 index,
2149 ignores,
2150 directory,
2151 };
2152 collect_ignored_untracked_paths(&context, root, false, &mut paths)?;
2153 Ok(paths.into_iter().collect())
2154}
2155
2156pub(crate) fn ignored_traditional_path_is_empty_directory(
2157 root: &Path,
2158 path: &[u8],
2159) -> Result<bool> {
2160 let Some(path) = path.strip_suffix(b"/") else {
2161 return Ok(false);
2162 };
2163 let mut absolute = PathBuf::new();
2164 set_worktree_path_from_repo_path(root, path, &mut absolute)?;
2165 match fs::read_dir(&absolute) {
2166 Ok(mut entries) => Ok(entries.next().is_none()),
2167 Err(err) if err.kind() == std::io::ErrorKind::NotADirectory => Ok(false),
2168 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
2169 Err(err) => Err(err.into()),
2170 }
2171}
2172
2173pub(crate) struct IgnoredUntrackedContext<'a> {
2174 root: &'a Path,
2175 git_dir: &'a Path,
2176 index: &'a BTreeMap<Vec<u8>, TrackedEntry>,
2177 ignores: &'a IgnoreMatcher,
2178 directory: bool,
2179}
2180
2181pub(crate) fn collect_ignored_untracked_paths(
2182 context: &IgnoredUntrackedContext<'_>,
2183 dir: &Path,
2184 parent_ignored: bool,
2185 paths: &mut BTreeSet<Vec<u8>>,
2186) -> Result<()> {
2187 if is_same_path(dir, context.git_dir) {
2188 return Ok(());
2189 }
2190 let mut entries = fs::read_dir(dir)?.collect::<std::result::Result<Vec<_>, _>>()?;
2191 entries.sort_by_key(|entry| entry.file_name());
2192 for entry in entries {
2193 let path = entry.path();
2194 if is_dot_git_entry(&path) {
2195 continue;
2196 }
2197 if is_same_path(&path, context.git_dir) {
2198 continue;
2199 }
2200 let metadata = entry.metadata()?;
2201 let relative = path.strip_prefix(context.root).map_err(|_| {
2202 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
2203 })?;
2204 let git_path = git_path_bytes(relative)?;
2205 if metadata.is_dir() {
2206 let ignored = parent_ignored || context.ignores.is_ignored(&git_path, true);
2207 if ignored && !index_has_path_under(context.index, &git_path) {
2208 if context.directory || is_nested_repository_boundary(&path, context.git_dir) {
2209 let mut directory_path = git_path;
2210 directory_path.push(b'/');
2211 paths.insert(directory_path);
2212 } else {
2213 collect_ignored_untracked_paths(context, &path, true, paths)?;
2214 }
2215 } else {
2216 if is_nested_repository_boundary(&path, context.git_dir) {
2217 continue;
2218 }
2219 collect_ignored_untracked_paths(context, &path, ignored, paths)?;
2220 }
2221 } else if !context.index.contains_key(&git_path)
2222 && (metadata.is_file() || metadata.file_type().is_symlink())
2223 && (parent_ignored || context.ignores.is_ignored(&git_path, false))
2224 {
2225 paths.insert(git_path);
2226 }
2227 }
2228 Ok(())
2229}
2230
2231#[derive(Debug, Clone, Default)]
2232pub(crate) struct IgnoreMatcher {
2233 pub(crate) patterns: Vec<IgnorePattern>,
2234 pub(crate) buckets: IgnorePatternBuckets,
2235}
2236
2237#[derive(Debug, Clone, Default)]
2238pub(crate) struct IgnorePatternBuckets {
2239 pub(crate) literal_basename: HashMap<Vec<u8>, Vec<usize>>,
2240 pub(crate) directory_literal_basename: HashMap<Vec<u8>, Vec<usize>>,
2241 pub(crate) literal_path_basename: HashMap<Vec<u8>, Vec<usize>>,
2242 pub(crate) directory_literal_path_basename: HashMap<Vec<u8>, Vec<usize>>,
2243 pub(crate) path_suffix_basename: HashMap<Vec<u8>, Vec<usize>>,
2244 pub(crate) directory_path_suffix_basename: HashMap<Vec<u8>, Vec<usize>>,
2245 pub(crate) glob_path_literal_basename: HashMap<Vec<u8>, Vec<usize>>,
2246 pub(crate) glob_directory_literal_basename: HashMap<Vec<u8>, Vec<usize>>,
2247 pub(crate) glob_path_suffix_basename: Vec<usize>,
2248 pub(crate) glob_path_prefix_basename: Vec<usize>,
2249 pub(crate) glob_directory_suffix_basename: Vec<usize>,
2250 pub(crate) glob_directory_prefix_basename: Vec<usize>,
2251 pub(crate) suffix_basename: HashMap<u8, Vec<usize>>,
2252 pub(crate) prefix_basename: HashMap<u8, Vec<usize>>,
2253 pub(crate) other: Vec<usize>,
2254}
2255
2256impl IgnorePatternBuckets {
2257 fn push(&mut self, index: usize, pattern: &IgnorePattern) {
2258 match pattern.bucket_kind() {
2259 IgnoreBucketKind::LiteralBasename => self
2260 .literal_basename
2261 .entry(pattern.pattern.clone())
2262 .or_default()
2263 .push(index),
2264 IgnoreBucketKind::DirectoryLiteralBasename => self
2265 .directory_literal_basename
2266 .entry(pattern.pattern.clone())
2267 .or_default()
2268 .push(index),
2269 IgnoreBucketKind::LiteralPathBasename => self
2270 .literal_path_basename
2271 .entry(path_basename(&pattern.pattern).to_vec())
2272 .or_default()
2273 .push(index),
2274 IgnoreBucketKind::DirectoryLiteralPathBasename => self
2275 .directory_literal_path_basename
2276 .entry(path_basename(&pattern.pattern).to_vec())
2277 .or_default()
2278 .push(index),
2279 IgnoreBucketKind::PathSuffixBasename => {
2280 let suffix = pattern
2281 .pattern
2282 .strip_prefix(b"**/")
2283 .unwrap_or(&pattern.pattern);
2284 self.path_suffix_basename
2285 .entry(path_basename(suffix).to_vec())
2286 .or_default()
2287 .push(index);
2288 }
2289 IgnoreBucketKind::DirectoryPathSuffixBasename => {
2290 let suffix = pattern
2291 .pattern
2292 .strip_prefix(b"**/")
2293 .unwrap_or(&pattern.pattern);
2294 self.directory_path_suffix_basename
2295 .entry(path_basename(suffix).to_vec())
2296 .or_default()
2297 .push(index);
2298 }
2299 IgnoreBucketKind::GlobPathLiteralBasename => self
2300 .glob_path_literal_basename
2301 .entry(path_basename(&pattern.pattern).to_vec())
2302 .or_default()
2303 .push(index),
2304 IgnoreBucketKind::GlobDirectoryLiteralBasename => self
2305 .glob_directory_literal_basename
2306 .entry(path_basename(&pattern.pattern).to_vec())
2307 .or_default()
2308 .push(index),
2309 IgnoreBucketKind::GlobPathSuffixBasename => self.glob_path_suffix_basename.push(index),
2310 IgnoreBucketKind::GlobPathPrefixBasename => self.glob_path_prefix_basename.push(index),
2311 IgnoreBucketKind::GlobDirectorySuffixBasename => {
2312 self.glob_directory_suffix_basename.push(index)
2313 }
2314 IgnoreBucketKind::GlobDirectoryPrefixBasename => {
2315 self.glob_directory_prefix_basename.push(index)
2316 }
2317 IgnoreBucketKind::SuffixBasename => {
2318 if let Some(last) = pattern.pattern.last() {
2319 self.suffix_basename.entry(*last).or_default().push(index);
2320 }
2321 }
2322 IgnoreBucketKind::PrefixBasename => self
2323 .prefix_basename
2324 .entry(pattern.pattern[0])
2325 .or_default()
2326 .push(index),
2327 IgnoreBucketKind::Other => self.other.push(index),
2328 }
2329 }
2330
2331 fn truncate(&mut self, len: usize) {
2332 fn truncate_indices(indices: &mut Vec<usize>, len: usize) {
2333 let keep = indices.partition_point(|index| *index < len);
2334 indices.truncate(keep);
2335 }
2336 for indices in self.literal_basename.values_mut() {
2337 truncate_indices(indices, len);
2338 }
2339 for indices in self.directory_literal_basename.values_mut() {
2340 truncate_indices(indices, len);
2341 }
2342 for indices in self.literal_path_basename.values_mut() {
2343 truncate_indices(indices, len);
2344 }
2345 for indices in self.directory_literal_path_basename.values_mut() {
2346 truncate_indices(indices, len);
2347 }
2348 for indices in self.path_suffix_basename.values_mut() {
2349 truncate_indices(indices, len);
2350 }
2351 for indices in self.directory_path_suffix_basename.values_mut() {
2352 truncate_indices(indices, len);
2353 }
2354 for indices in self.glob_path_literal_basename.values_mut() {
2355 truncate_indices(indices, len);
2356 }
2357 for indices in self.glob_directory_literal_basename.values_mut() {
2358 truncate_indices(indices, len);
2359 }
2360 truncate_indices(&mut self.glob_path_suffix_basename, len);
2361 truncate_indices(&mut self.glob_path_prefix_basename, len);
2362 truncate_indices(&mut self.glob_directory_suffix_basename, len);
2363 truncate_indices(&mut self.glob_directory_prefix_basename, len);
2364 for indices in self.suffix_basename.values_mut() {
2365 truncate_indices(indices, len);
2366 }
2367 for indices in self.prefix_basename.values_mut() {
2368 truncate_indices(indices, len);
2369 }
2370 truncate_indices(&mut self.other, len);
2371 }
2372
2373 fn profile_map_count(&self) -> usize {
2374 self.literal_basename.len()
2375 + self.directory_literal_basename.len()
2376 + self.literal_path_basename.len()
2377 + self.directory_literal_path_basename.len()
2378 + self.path_suffix_basename.len()
2379 + self.directory_path_suffix_basename.len()
2380 + self.glob_path_literal_basename.len()
2381 + self.glob_directory_literal_basename.len()
2382 + self.suffix_basename.len()
2383 + self.prefix_basename.len()
2384 }
2385
2386 fn profile_index_count(&self) -> usize {
2387 fn map_indices<K>(map: &HashMap<K, Vec<usize>>) -> usize {
2388 map.values().map(Vec::len).sum()
2389 }
2390 map_indices(&self.literal_basename)
2391 + map_indices(&self.directory_literal_basename)
2392 + map_indices(&self.literal_path_basename)
2393 + map_indices(&self.directory_literal_path_basename)
2394 + map_indices(&self.path_suffix_basename)
2395 + map_indices(&self.directory_path_suffix_basename)
2396 + map_indices(&self.glob_path_literal_basename)
2397 + map_indices(&self.glob_directory_literal_basename)
2398 + self.glob_path_suffix_basename.len()
2399 + self.glob_path_prefix_basename.len()
2400 + self.glob_directory_suffix_basename.len()
2401 + self.glob_directory_prefix_basename.len()
2402 + map_indices(&self.suffix_basename)
2403 + map_indices(&self.prefix_basename)
2404 + self.other.len()
2405 }
2406
2407 fn profile_index_vec_bytes(&self) -> usize {
2408 fn map_bytes<K>(map: &HashMap<K, Vec<usize>>) -> usize {
2409 map.values()
2410 .map(|indices| indices.capacity() * std::mem::size_of::<usize>())
2411 .sum()
2412 }
2413 map_bytes(&self.literal_basename)
2414 + map_bytes(&self.directory_literal_basename)
2415 + map_bytes(&self.literal_path_basename)
2416 + map_bytes(&self.directory_literal_path_basename)
2417 + map_bytes(&self.path_suffix_basename)
2418 + map_bytes(&self.directory_path_suffix_basename)
2419 + map_bytes(&self.glob_path_literal_basename)
2420 + map_bytes(&self.glob_directory_literal_basename)
2421 + self.glob_path_suffix_basename.capacity() * std::mem::size_of::<usize>()
2422 + self.glob_path_prefix_basename.capacity() * std::mem::size_of::<usize>()
2423 + self.glob_directory_suffix_basename.capacity() * std::mem::size_of::<usize>()
2424 + self.glob_directory_prefix_basename.capacity() * std::mem::size_of::<usize>()
2425 + map_bytes(&self.suffix_basename)
2426 + map_bytes(&self.prefix_basename)
2427 + self.other.capacity() * std::mem::size_of::<usize>()
2428 }
2429}
2430
2431#[derive(Debug, Clone)]
2432pub(crate) struct IgnorePattern {
2433 pub(crate) base: Vec<u8>,
2434 pub(crate) pattern: Vec<u8>,
2435 pub(crate) original: Vec<u8>,
2436 pub(crate) source: Vec<u8>,
2437 pub(crate) line_number: usize,
2438 pub(crate) negated: bool,
2439 pub(crate) directory_only: bool,
2440 pub(crate) anchored: bool,
2441 pub(crate) has_slash: bool,
2442 pub(crate) match_kind: MatchKind,
2447 pub(crate) glob_literal_prefix_len: usize,
2448}
2449
2450#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2455pub(crate) enum MatchKind {
2456 Literal,
2458 Suffix,
2460 Prefix,
2462 PathSuffix,
2464 Glob,
2466}
2467
2468pub(crate) fn path_basename(path: &[u8]) -> &[u8] {
2469 path.rsplit(|byte| *byte == b'/').next().unwrap_or(path)
2470}
2471
2472pub(crate) fn path_component_has_glob_meta(component: &[u8]) -> bool {
2473 component
2474 .iter()
2475 .any(|byte| matches!(byte, b'*' | b'?' | b'[' | b'\\'))
2476}
2477
2478pub(crate) fn final_component_match_kind(pattern: &[u8]) -> MatchKind {
2479 classify_ignore_pattern(path_basename(pattern))
2480}
2481
2482pub(crate) fn visit_directory_match_components(
2483 path: &[u8],
2484 is_dir: bool,
2485 mut visit: impl FnMut(&[u8]),
2486) {
2487 let mut start = 0usize;
2488 for (index, byte) in path.iter().enumerate() {
2489 if *byte == b'/' {
2490 if index > start {
2491 visit(&path[start..index]);
2492 }
2493 start = index + 1;
2494 }
2495 }
2496 if is_dir && start < path.len() {
2497 visit(&path[start..]);
2498 }
2499}
2500
2501#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2502pub(crate) enum IgnoreBucketKind {
2503 LiteralBasename,
2504 DirectoryLiteralBasename,
2505 LiteralPathBasename,
2506 DirectoryLiteralPathBasename,
2507 PathSuffixBasename,
2508 DirectoryPathSuffixBasename,
2509 GlobPathLiteralBasename,
2510 GlobDirectoryLiteralBasename,
2511 GlobPathSuffixBasename,
2512 GlobPathPrefixBasename,
2513 GlobDirectorySuffixBasename,
2514 GlobDirectoryPrefixBasename,
2515 SuffixBasename,
2516 PrefixBasename,
2517 Other,
2518}
2519
2520pub(crate) fn classify_ignore_pattern(pattern: &[u8]) -> MatchKind {
2524 if let Some(suffix) = pattern.strip_prefix(b"**/")
2525 && !suffix.is_empty()
2526 && !suffix
2527 .iter()
2528 .any(|byte| matches!(byte, b'*' | b'?' | b'[' | b'\\'))
2529 {
2530 return MatchKind::PathSuffix;
2531 }
2532 let stars = pattern.iter().filter(|byte| **byte == b'*').count();
2533 let other_meta = pattern
2534 .iter()
2535 .any(|byte| matches!(byte, b'?' | b'[' | b'\\'));
2536 if stars == 0 && !other_meta {
2537 return MatchKind::Literal;
2538 }
2539 if stars == 1 && !other_meta {
2540 let literal = if pattern.first() == Some(&b'*') {
2541 Some((&pattern[1..], MatchKind::Suffix))
2542 } else if pattern.last() == Some(&b'*') {
2543 Some((&pattern[..pattern.len() - 1], MatchKind::Prefix))
2544 } else {
2545 None
2546 };
2547 if let Some((literal, kind)) = literal
2548 && !literal.is_empty()
2549 && !literal.contains(&b'/')
2550 {
2551 return kind;
2552 }
2553 }
2554 MatchKind::Glob
2555}
2556
2557impl IgnoreMatcher {
2558 pub(crate) fn emit_memory_profile(&self, label: &str) {
2559 let pattern_payload_bytes = self
2560 .patterns
2561 .iter()
2562 .map(|pattern| {
2563 pattern.base.capacity()
2564 + pattern.pattern.capacity()
2565 + pattern.original.capacity()
2566 + pattern.source.capacity()
2567 })
2568 .sum();
2569 status_profile_mem(
2570 label,
2571 &[
2572 ("ignore_patterns_len", self.patterns.len()),
2573 ("ignore_patterns_cap", self.patterns.capacity()),
2574 (
2575 "ignore_pattern_struct_bytes",
2576 self.patterns.capacity() * std::mem::size_of::<IgnorePattern>(),
2577 ),
2578 ("ignore_pattern_payload_bytes", pattern_payload_bytes),
2579 ("ignore_bucket_map_count", self.buckets.profile_map_count()),
2580 (
2581 "ignore_bucket_index_count",
2582 self.buckets.profile_index_count(),
2583 ),
2584 (
2585 "ignore_bucket_index_vec_bytes",
2586 self.buckets.profile_index_vec_bytes(),
2587 ),
2588 ],
2589 );
2590 }
2591
2592 fn from_sources(
2593 root: &Path,
2594 exclude_standard: bool,
2595 patterns: &[Vec<u8>],
2596 per_directory: &[String],
2597 ) -> Result<Self> {
2598 let mut matcher = if exclude_standard {
2599 Self::from_worktree_root(root)?
2600 } else {
2601 Self::default()
2602 };
2603 matcher.extend_patterns(patterns);
2604 matcher.extend_per_directory_patterns(root, per_directory)?;
2605 Ok(matcher)
2606 }
2607
2608 pub(crate) fn from_worktree_base(root: &Path) -> Result<Self> {
2614 let mut matcher = Self::default();
2615 if !read_core_excludes_file(root, &mut matcher.patterns) {
2616 read_default_global_excludes_file(&mut matcher.patterns);
2617 }
2618 read_ignore_patterns(
2619 root.join(".git").join("info").join("exclude"),
2620 &mut matcher.patterns,
2621 &[],
2622 b".git/info/exclude",
2623 );
2624 matcher.rebuild_buckets();
2625 Ok(matcher)
2626 }
2627
2628 pub(crate) fn from_worktree_root(root: &Path) -> Result<Self> {
2629 let mut matcher = Self::default();
2630 if !read_core_excludes_file(root, &mut matcher.patterns) {
2631 read_default_global_excludes_file(&mut matcher.patterns);
2632 }
2633 read_ignore_patterns(
2634 root.join(".git").join("info").join("exclude"),
2635 &mut matcher.patterns,
2636 &[],
2637 b".git/info/exclude",
2638 );
2639 matcher.rebuild_buckets();
2640 collect_per_directory_patterns_into_matcher(
2641 root,
2642 root,
2643 &[String::from(".gitignore")],
2644 &mut matcher,
2645 )?;
2646 Ok(matcher)
2647 }
2648
2649 pub(crate) fn extend_patterns(&mut self, patterns: &[Vec<u8>]) {
2650 for pattern in patterns {
2651 self.push_raw_pattern(pattern, &[], &[], 0);
2652 }
2653 }
2654
2655 fn extend_per_directory_patterns(&mut self, root: &Path, names: &[String]) -> Result<()> {
2656 if names.is_empty() {
2657 return Ok(());
2658 }
2659 collect_per_directory_patterns_into_matcher(root, root, names, self)?;
2660 Ok(())
2661 }
2662
2663 pub(crate) fn is_ignored(&self, path: &[u8], is_dir: bool) -> bool {
2664 self.is_ignored_profiled(path, is_dir, None)
2665 }
2666
2667 fn match_for(&self, path: &[u8], is_dir: bool) -> Option<&IgnorePattern> {
2668 self.match_index_for(path, is_dir, None)
2669 .and_then(|index| self.patterns.get(index))
2670 }
2671
2672 fn is_ignored_profiled(
2673 &self,
2674 path: &[u8],
2675 is_dir: bool,
2676 mut profile: Option<&mut StatusProfileCounters>,
2677 ) -> bool {
2678 if let Some(profile) = profile.as_deref_mut() {
2679 profile.ignore_checks += 1;
2680 }
2681 self.match_index_for(path, is_dir, profile)
2682 .is_some_and(|index| !self.patterns[index].negated)
2683 }
2684
2685 fn match_index_for(
2686 &self,
2687 path: &[u8],
2688 is_dir: bool,
2689 mut profile: Option<&mut StatusProfileCounters>,
2690 ) -> Option<usize> {
2691 let basename = path_basename(path);
2692 let mut best = None;
2693 if let Some(indices) = self.buckets.literal_basename.get(basename) {
2694 self.match_bucket_candidates(indices, path, basename, is_dir, &mut best, &mut profile);
2695 }
2696 if let Some(indices) = self.buckets.literal_path_basename.get(basename) {
2697 self.match_bucket_candidates(indices, path, basename, is_dir, &mut best, &mut profile);
2698 }
2699 if let Some(indices) = self.buckets.path_suffix_basename.get(basename) {
2700 self.match_bucket_candidates(indices, path, basename, is_dir, &mut best, &mut profile);
2701 }
2702 if let Some(indices) = self.buckets.glob_path_literal_basename.get(basename) {
2703 self.match_bucket_candidates(indices, path, basename, is_dir, &mut best, &mut profile);
2704 }
2705 self.match_final_component_candidates(
2706 &self.buckets.glob_path_suffix_basename,
2707 MatchKind::Suffix,
2708 basename,
2709 path,
2710 basename,
2711 is_dir,
2712 &mut best,
2713 &mut profile,
2714 );
2715 self.match_final_component_candidates(
2716 &self.buckets.glob_path_prefix_basename,
2717 MatchKind::Prefix,
2718 basename,
2719 path,
2720 basename,
2721 is_dir,
2722 &mut best,
2723 &mut profile,
2724 );
2725 visit_directory_match_components(path, is_dir, |component| {
2726 if let Some(indices) = self.buckets.directory_literal_basename.get(component) {
2727 self.match_bucket_candidates(
2728 indices,
2729 path,
2730 basename,
2731 is_dir,
2732 &mut best,
2733 &mut profile,
2734 );
2735 }
2736 if let Some(indices) = self.buckets.directory_literal_path_basename.get(component) {
2737 self.match_bucket_candidates(
2738 indices,
2739 path,
2740 basename,
2741 is_dir,
2742 &mut best,
2743 &mut profile,
2744 );
2745 }
2746 if let Some(indices) = self.buckets.directory_path_suffix_basename.get(component) {
2747 self.match_bucket_candidates(
2748 indices,
2749 path,
2750 basename,
2751 is_dir,
2752 &mut best,
2753 &mut profile,
2754 );
2755 }
2756 if let Some(indices) = self.buckets.glob_directory_literal_basename.get(component) {
2757 self.match_bucket_candidates(
2758 indices,
2759 path,
2760 basename,
2761 is_dir,
2762 &mut best,
2763 &mut profile,
2764 );
2765 }
2766 self.match_final_component_candidates(
2767 &self.buckets.glob_directory_suffix_basename,
2768 MatchKind::Suffix,
2769 component,
2770 path,
2771 basename,
2772 is_dir,
2773 &mut best,
2774 &mut profile,
2775 );
2776 self.match_final_component_candidates(
2777 &self.buckets.glob_directory_prefix_basename,
2778 MatchKind::Prefix,
2779 component,
2780 path,
2781 basename,
2782 is_dir,
2783 &mut best,
2784 &mut profile,
2785 );
2786 });
2787 if let Some(last) = basename.last()
2788 && let Some(indices) = self.buckets.suffix_basename.get(last)
2789 {
2790 self.match_bucket_candidates(indices, path, basename, is_dir, &mut best, &mut profile);
2791 }
2792 if let Some(first) = basename.first()
2793 && let Some(indices) = self.buckets.prefix_basename.get(first)
2794 {
2795 self.match_bucket_candidates(indices, path, basename, is_dir, &mut best, &mut profile);
2796 }
2797 self.match_bucket_candidates(
2798 &self.buckets.other,
2799 path,
2800 basename,
2801 is_dir,
2802 &mut best,
2803 &mut profile,
2804 );
2805 best
2806 }
2807
2808 fn match_bucket_candidates(
2809 &self,
2810 indices: &[usize],
2811 path: &[u8],
2812 basename: &[u8],
2813 is_dir: bool,
2814 best: &mut Option<usize>,
2815 profile: &mut Option<&mut StatusProfileCounters>,
2816 ) {
2817 for &index in indices.iter().rev() {
2818 if best.is_some_and(|best| index <= best) {
2819 break;
2820 }
2821 let pattern = &self.patterns[index];
2822 if !pattern.base_matches(path) {
2823 continue;
2824 }
2825 if !pattern.glob_literal_prefix_matches(path, basename, is_dir) {
2826 continue;
2827 }
2828 if let Some(profile) = profile.as_deref_mut() {
2829 profile.ignore_pattern_tests += 1;
2830 if pattern.match_kind == MatchKind::Glob {
2831 profile.ignore_glob_fallback_tests += 1;
2832 }
2833 }
2834 if pattern.matches_with_basename(path, basename, is_dir) {
2835 *best = Some(index);
2836 break;
2837 }
2838 }
2839 }
2840
2841 fn match_final_component_candidates(
2842 &self,
2843 indices: &[usize],
2844 kind: MatchKind,
2845 component: &[u8],
2846 path: &[u8],
2847 basename: &[u8],
2848 is_dir: bool,
2849 best: &mut Option<usize>,
2850 profile: &mut Option<&mut StatusProfileCounters>,
2851 ) {
2852 for &index in indices.iter().rev() {
2853 if best.is_some_and(|best| index <= best) {
2854 break;
2855 }
2856 let pattern = &self.patterns[index];
2857 if !pattern.base_matches(path) {
2858 continue;
2859 }
2860 let final_component = path_basename(&pattern.pattern);
2861 let candidate = match kind {
2862 MatchKind::Suffix => component.ends_with(&final_component[1..]),
2863 MatchKind::Prefix => {
2864 component.starts_with(&final_component[..final_component.len() - 1])
2865 }
2866 _ => false,
2867 };
2868 if !candidate {
2869 continue;
2870 }
2871 if !pattern.glob_literal_prefix_matches(path, basename, is_dir) {
2872 continue;
2873 }
2874 if let Some(profile) = profile.as_deref_mut() {
2875 profile.ignore_pattern_tests += 1;
2876 if pattern.match_kind == MatchKind::Glob {
2877 profile.ignore_glob_fallback_tests += 1;
2878 }
2879 }
2880 if pattern.matches_with_basename(path, basename, is_dir) {
2881 *best = Some(index);
2882 break;
2883 }
2884 }
2885 }
2886
2887 fn push_pattern(&mut self, pattern: IgnorePattern) {
2888 let index = self.patterns.len();
2889 self.buckets.push(index, &pattern);
2890 self.patterns.push(pattern);
2891 }
2892
2893 pub(crate) fn push_raw_pattern(
2894 &mut self,
2895 raw: &[u8],
2896 base: &[u8],
2897 source: &[u8],
2898 line_number: usize,
2899 ) {
2900 if let Some(pattern) = parse_ignore_pattern(raw, base, source, line_number) {
2901 self.push_pattern(pattern);
2902 }
2903 }
2904
2905 fn truncate(&mut self, len: usize) {
2906 if self.patterns.len() == len {
2907 return;
2908 }
2909 self.patterns.truncate(len);
2910 self.buckets.truncate(len);
2911 }
2912
2913 fn rebuild_buckets(&mut self) {
2914 let mut buckets = IgnorePatternBuckets::default();
2915 for (index, pattern) in self.patterns.iter().enumerate() {
2916 buckets.push(index, pattern);
2917 }
2918 self.buckets = buckets;
2919 }
2920}
2921
2922#[cfg(test)]
2923mod tests {
2924 use super::*;
2925
2926 #[test]
2927 fn untracked_glob_prefix_stops_at_backslash_escape() {
2928 assert_eq!(literal_prefix_before_glob(br"dir/\*.rs"), b"dir/");
2929 assert_eq!(
2930 literal_prefix_before_glob(br"dir/plain.rs"),
2931 b"dir/plain.rs"
2932 );
2933 }
2934}