1use super::*;
6use crate::checkout::*;
7use crate::ignore::*;
8use crate::index::*;
9use crate::index_io::*;
10use crate::types_admin::*;
11use std::sync::atomic::{AtomicUsize, Ordering};
12
13pub fn stream_short_status<F>(
14 worktree_root: impl AsRef<Path>,
15 git_dir: impl AsRef<Path>,
16 format: ObjectFormat,
17 emit: F,
18) -> Result<()>
19where
20 F: for<'a> FnMut(ShortStatusRow<'a>) -> Result<StreamControl>,
21{
22 stream_short_status_with_options(
23 worktree_root,
24 git_dir,
25 format,
26 ShortStatusOptions::default(),
27 emit,
28 )
29}
30
31pub fn short_status_count(
32 worktree_root: impl AsRef<Path>,
33 git_dir: impl AsRef<Path>,
34 format: ObjectFormat,
35) -> Result<usize> {
36 short_status_count_with_options(
37 worktree_root,
38 git_dir,
39 format,
40 ShortStatusOptions::default(),
41 )
42}
43
44pub fn short_status_count_with_options(
45 worktree_root: impl AsRef<Path>,
46 git_dir: impl AsRef<Path>,
47 format: ObjectFormat,
48 options: ShortStatusOptions,
49) -> Result<usize> {
50 let worktree_root = worktree_root.as_ref();
51 let git_dir = git_dir.as_ref();
52 let db = FileObjectDatabase::from_git_dir(git_dir, format);
53 if !options.include_ignored
54 && let Some(count) = short_status_borrowed_head_matches_index_count_if_possible(
55 worktree_root,
56 git_dir,
57 format,
58 &db,
59 options.untracked_mode,
60 )?
61 {
62 return Ok(count);
63 }
64 let mut count = 0usize;
65 stream_short_status_with_options(worktree_root, git_dir, format, options, |_| {
66 count += 1;
67 Ok(StreamControl::Continue)
68 })?;
69 Ok(count)
70}
71
72#[derive(Debug, Clone, Default)]
73pub(crate) struct StatusProfileCounters {
74 pub(crate) fast_path_borrowed: bool,
75 pub(crate) read_dir_calls: u64,
76 pub(crate) dir_entries_seen: u64,
77 pub(crate) file_type_calls: u64,
78 pub(crate) ignore_checks: u64,
79 pub(crate) ignore_pattern_tests: u64,
80 pub(crate) ignore_glob_fallback_tests: u64,
81 pub(crate) tracked_exact_hits: u64,
82 pub(crate) tracked_dir_prefix_hits: u64,
83 pub(crate) tracked_skip_worktree_prefix_hits: u64,
84 pub(crate) read_dir_entry_vec_cap_bytes: u64,
85 pub(crate) read_dir_entry_vec_max_len: u64,
86 pub(crate) read_dir_entry_vec_max_cap: u64,
87 pub(crate) read_dir_name_vec_cap_bytes: u64,
88 pub(crate) read_dir_name_vec_max_len: u64,
89 pub(crate) read_dir_name_vec_max_cap: u64,
90 pub(crate) untracked_rows: u64,
91 pub(crate) tracked_elapsed_us: u128,
92 pub(crate) untracked_elapsed_us: u128,
93 pub(crate) render_elapsed_us: u128,
94 pub(crate) overlap_enabled: bool,
95}
96
97pub(crate) const STATUS_BORROWED_OVERLAP_MIN_STAGE0: usize = 8192;
98
99#[derive(Debug, Clone, Copy)]
100pub(crate) struct StatusExecutor {
101 workers: usize,
102}
103
104impl StatusExecutor {
105 pub(crate) fn new() -> Self {
106 let workers = std::thread::available_parallelism()
107 .map(|count| count.get())
108 .unwrap_or(1)
109 .max(1);
110 Self { workers }
111 }
112
113 pub(crate) fn worker_count_for(
114 self,
115 item_count: usize,
116 min_items_per_worker: usize,
117 cap: usize,
118 ) -> usize {
119 if item_count == 0 {
120 return 0;
121 }
122 let requested = item_count.div_ceil(min_items_per_worker.max(1));
123 self.workers.min(cap.max(1)).min(requested).max(1)
124 }
125
126 pub(crate) fn spawn<'scope, 'env, F, T>(
127 self,
128 scope: &'scope std::thread::Scope<'scope, 'env>,
129 name: &str,
130 f: F,
131 ) -> Result<StatusTask<'scope, T>>
132 where
133 F: FnOnce() -> Result<T> + Send + 'scope,
134 T: Send + 'scope,
135 {
136 let handle = std::thread::Builder::new()
137 .name(name.to_string())
138 .spawn_scoped(scope, f)
139 .map_err(|err| {
140 GitError::Command(format!("failed to spawn status worker `{name}`: {err}"))
141 })?;
142 Ok(StatusTask {
143 name: name.to_string(),
144 handle,
145 })
146 }
147}
148
149pub(crate) struct StatusTask<'scope, T> {
150 name: String,
151 handle: std::thread::ScopedJoinHandle<'scope, Result<T>>,
152}
153
154impl<T> StatusTask<'_, T> {
155 pub(crate) fn join(self) -> Result<T> {
156 self.handle
157 .join()
158 .map_err(|_| GitError::Command(format!("status worker `{}` panicked", self.name)))?
159 }
160}
161
162pub(crate) enum BorrowedIndexBytes {
163 Owned(Vec<u8>),
164 Mapped(sley_mmap::MappedFile),
165}
166
167impl AsRef<[u8]> for BorrowedIndexBytes {
168 fn as_ref(&self) -> &[u8] {
169 match self {
170 Self::Owned(bytes) => bytes,
171 Self::Mapped(bytes) => bytes.as_bytes(),
172 }
173 }
174}
175
176pub(crate) fn read_borrowed_index_bytes(index_path: &Path) -> Result<BorrowedIndexBytes> {
177 match sley_mmap::MappedFile::open_index(index_path) {
178 Ok(mapped) => Ok(BorrowedIndexBytes::Mapped(mapped)),
179 Err(_) => Ok(BorrowedIndexBytes::Owned(fs::read(index_path)?)),
180 }
181}
182
183impl StatusProfileCounters {
184 fn enabled() -> bool {
185 std::env::var_os("SLEY_STATUS_PROFILE").is_some_and(|value| value != "0")
186 }
187
188 fn memory_enabled() -> bool {
189 std::env::var_os("SLEY_STATUS_PROFILE")
190 .and_then(|value| value.into_string().ok())
191 .is_some_and(|value| value == "mem" || value == "memory")
192 }
193
194 pub(crate) fn merge_untracked(&mut self, other: StatusProfileCounters) {
195 self.read_dir_calls += other.read_dir_calls;
196 self.dir_entries_seen += other.dir_entries_seen;
197 self.file_type_calls += other.file_type_calls;
198 self.ignore_checks += other.ignore_checks;
199 self.ignore_pattern_tests += other.ignore_pattern_tests;
200 self.ignore_glob_fallback_tests += other.ignore_glob_fallback_tests;
201 self.tracked_exact_hits += other.tracked_exact_hits;
202 self.tracked_dir_prefix_hits += other.tracked_dir_prefix_hits;
203 self.tracked_skip_worktree_prefix_hits += other.tracked_skip_worktree_prefix_hits;
204 self.read_dir_entry_vec_cap_bytes += other.read_dir_entry_vec_cap_bytes;
205 self.read_dir_entry_vec_max_len = self
206 .read_dir_entry_vec_max_len
207 .max(other.read_dir_entry_vec_max_len);
208 self.read_dir_entry_vec_max_cap = self
209 .read_dir_entry_vec_max_cap
210 .max(other.read_dir_entry_vec_max_cap);
211 self.read_dir_name_vec_cap_bytes += other.read_dir_name_vec_cap_bytes;
212 self.read_dir_name_vec_max_len = self
213 .read_dir_name_vec_max_len
214 .max(other.read_dir_name_vec_max_len);
215 self.read_dir_name_vec_max_cap = self
216 .read_dir_name_vec_max_cap
217 .max(other.read_dir_name_vec_max_cap);
218 self.untracked_rows += other.untracked_rows;
219 self.untracked_elapsed_us += other.untracked_elapsed_us;
220 }
221
222 fn emit(&self) {
223 eprintln!(
224 "{{\"schema\":\"sley.status.profile.v1\",\
225 \"fast_path_borrowed\":{},\
226 \"read_dir_calls\":{},\
227 \"dir_entries_seen\":{},\
228 \"file_type_calls\":{},\
229 \"ignore_checks\":{},\
230 \"ignore_pattern_tests\":{},\
231 \"ignore_glob_fallback_tests\":{},\
232 \"tracked_exact_hits\":{},\
233 \"tracked_dir_prefix_hits\":{},\
234 \"tracked_skip_worktree_prefix_hits\":{},\
235 \"read_dir_entry_size\":{},\
236 \"read_dir_entry_vec_cap_bytes\":{},\
237 \"read_dir_entry_vec_max_len\":{},\
238 \"read_dir_entry_vec_max_cap\":{},\
239 \"read_dir_name_size\":{},\
240 \"read_dir_name_vec_cap_bytes\":{},\
241 \"read_dir_name_vec_max_len\":{},\
242 \"read_dir_name_vec_max_cap\":{},\
243 \"untracked_rows\":{},\
244 \"tracked_elapsed_us\":{},\
245 \"untracked_elapsed_us\":{},\
246 \"render_elapsed_us\":{},\
247 \"overlap_enabled\":{}}}",
248 self.fast_path_borrowed,
249 self.read_dir_calls,
250 self.dir_entries_seen,
251 self.file_type_calls,
252 self.ignore_checks,
253 self.ignore_pattern_tests,
254 self.ignore_glob_fallback_tests,
255 self.tracked_exact_hits,
256 self.tracked_dir_prefix_hits,
257 self.tracked_skip_worktree_prefix_hits,
258 std::mem::size_of::<StatusDirEntry>(),
259 self.read_dir_entry_vec_cap_bytes,
260 self.read_dir_entry_vec_max_len,
261 self.read_dir_entry_vec_max_cap,
262 std::mem::size_of::<std::ffi::OsString>(),
263 self.read_dir_name_vec_cap_bytes,
264 self.read_dir_name_vec_max_len,
265 self.read_dir_name_vec_max_cap,
266 self.untracked_rows,
267 self.tracked_elapsed_us,
268 self.untracked_elapsed_us,
269 self.render_elapsed_us,
270 self.overlap_enabled
271 );
272 }
273}
274
275pub(crate) fn status_profile_rss_vsz_bytes() -> Option<(u64, u64)> {
276 let pid = std::process::id().to_string();
277 let output = Command::new("ps")
278 .args(["-o", "rss=", "-o", "vsz=", "-p", &pid])
279 .output()
280 .ok()?;
281 if !output.status.success() {
282 return None;
283 }
284 let text = String::from_utf8(output.stdout).ok()?;
285 let mut parts = text.split_whitespace();
286 let rss_kib = parts.next()?.parse::<u64>().ok()?;
287 let vsz_kib = parts.next()?.parse::<u64>().ok()?;
288 Some((rss_kib * 1024, vsz_kib * 1024))
289}
290
291pub(crate) fn status_profile_pause(label: &str) {
292 let Some(target) =
293 std::env::var_os("SLEY_STATUS_PROFILE_PAUSE_AT").and_then(|value| value.into_string().ok())
294 else {
295 return;
296 };
297 if target != label && target != "*" {
298 return;
299 }
300 let seconds = std::env::var("SLEY_STATUS_PROFILE_PAUSE_SECS")
301 .ok()
302 .and_then(|value| value.parse::<u64>().ok())
303 .unwrap_or(30);
304 eprintln!(
305 "{{\"schema\":\"sley.status.mem.pause.v1\",\"label\":\"{}\",\"pid\":{},\"seconds\":{}}}",
306 label,
307 std::process::id(),
308 seconds
309 );
310 std::thread::sleep(std::time::Duration::from_secs(seconds));
311}
312
313pub(crate) fn status_profile_mem(label: &str, details: &[(&str, usize)]) {
314 if !StatusProfileCounters::memory_enabled() {
315 return;
316 }
317 let (rss_bytes, vsz_bytes) = status_profile_rss_vsz_bytes().unwrap_or((0, 0));
318 eprint!(
319 "{{\"schema\":\"sley.status.mem.v1\",\"label\":\"{}\",\"pid\":{},\"rss_bytes\":{},\"vsz_bytes\":{}",
320 label,
321 std::process::id(),
322 rss_bytes,
323 vsz_bytes
324 );
325 for (key, value) in details {
326 eprint!(",\"{}\":{}", key, value);
327 }
328 eprintln!("}}");
329 status_profile_pause(label);
330}
331
332pub fn worktree_entry_state(
338 worktree_root: impl AsRef<Path>,
339 git_dir: impl AsRef<Path>,
340 format: ObjectFormat,
341 path: impl AsRef<Path>,
342 expected_oid: &ObjectId,
343 expected_mode: u32,
344 index_probe: Option<&IndexStatProbe>,
345) -> Result<WorktreeEntryState> {
346 let path = path.as_ref();
347 if path.is_absolute() {
348 return Err(GitError::InvalidPath(format!(
349 "worktree entry path {} is absolute",
350 path.display()
351 )));
352 }
353 let git_path = git_path_bytes(path)?;
354 worktree_entry_state_by_git_path(
355 worktree_root,
356 git_dir,
357 format,
358 &git_path,
359 expected_oid,
360 expected_mode,
361 index_probe,
362 )
363}
364
365pub fn worktree_entry_state_by_git_path(
371 worktree_root: impl AsRef<Path>,
372 git_dir: impl AsRef<Path>,
373 format: ObjectFormat,
374 git_path: &[u8],
375 expected_oid: &ObjectId,
376 expected_mode: u32,
377 index_probe: Option<&IndexStatProbe>,
378) -> Result<WorktreeEntryState> {
379 let worktree_root = worktree_root.as_ref();
380 let git_dir = git_dir.as_ref();
381 let stat_cache =
382 index_probe.and_then(|probe| probe.stat_cache_for(git_path, expected_oid, expected_mode));
383 let Some(worktree_entry) = worktree_entry_for_git_path(
384 worktree_root,
385 git_dir,
386 format,
387 git_path,
388 expected_oid,
389 expected_mode,
390 stat_cache.as_ref(),
391 )?
392 else {
393 return Ok(WorktreeEntryState::Deleted);
394 };
395 if worktree_entry.mode == expected_mode && worktree_entry.oid == *expected_oid {
396 Ok(WorktreeEntryState::Clean)
397 } else {
398 Ok(WorktreeEntryState::Modified)
399 }
400}
401
402pub fn stream_short_status_with_options<F>(
403 worktree_root: impl AsRef<Path>,
404 git_dir: impl AsRef<Path>,
405 format: ObjectFormat,
406 options: ShortStatusOptions,
407 mut emit: F,
408) -> Result<()>
409where
410 F: for<'a> FnMut(ShortStatusRow<'a>) -> Result<StreamControl>,
411{
412 let worktree_root = worktree_root.as_ref();
413 let git_dir = git_dir.as_ref();
414 let db = FileObjectDatabase::from_git_dir(git_dir, format);
415 if !options.include_ignored
416 && let Some(()) = stream_short_status_borrowed_head_matches_index_if_possible(
417 worktree_root,
418 git_dir,
419 format,
420 &db,
421 options.untracked_mode,
422 &mut emit,
423 )?
424 {
425 return Ok(());
426 }
427 for entry in collect_short_status_with_options(worktree_root, git_dir, format, options)? {
428 if emit(entry.as_row())?.is_stop() {
429 break;
430 }
431 }
432 Ok(())
433}
434
435pub(crate) fn collect_short_status_with_options(
436 worktree_root: impl AsRef<Path>,
437 git_dir: impl AsRef<Path>,
438 format: ObjectFormat,
439 options: ShortStatusOptions,
440) -> Result<Vec<ShortStatusEntry>> {
441 let worktree_root = worktree_root.as_ref();
442 let git_dir = git_dir.as_ref();
443 let db = FileObjectDatabase::from_git_dir(git_dir, format);
444 if !options.include_ignored
445 && let Some(entries) = short_status_borrowed_head_matches_index_if_possible(
446 worktree_root,
447 git_dir,
448 format,
449 &db,
450 options.untracked_mode,
451 )?
452 {
453 return Ok(entries);
454 }
455 let (mut parsed_index, mut stat_cache, mut head_matches_index) =
461 read_index_with_stat_cache(git_dir, format, &db)?;
462 let sparse_checkout_active = sparse_checkout_active_for_status(git_dir, &parsed_index);
463 if sparse_checkout_active && parsed_index.entries.iter().any(IndexEntry::is_sparse_dir) {
464 expand_sparse_index(&mut parsed_index, &db, format)?;
465 stat_cache = IndexStatCache::from_index_mtime(&parsed_index, stat_cache.index_mtime);
466 head_matches_index = false;
467 }
468 let mut unmerged_entries = short_status_unmerged_entries(&parsed_index);
469 let unmerged_paths = unmerged_entries
470 .iter()
471 .map(|entry| entry.path.clone())
472 .collect::<BTreeSet<_>>();
473 if head_matches_index && !options.include_ignored {
474 let mut ignores = IgnoreMatcher::from_worktree_base(worktree_root)?;
475 let entries = short_status_tracked_only(
476 worktree_root,
477 git_dir,
478 format,
479 &db,
480 &parsed_index,
481 &stat_cache,
482 true,
483 sparse_checkout_active,
484 options.untracked_mode,
485 );
486 let mut entries = entries?;
487 entries.retain(|entry| !unmerged_paths.contains(&entry.path));
488 let untracked_paths = status_untracked_paths_from_index(
489 worktree_root,
490 git_dir,
491 &parsed_index,
492 &stat_cache,
493 &mut ignores,
494 options.untracked_mode,
495 None,
496 )?;
497 for path in untracked_paths {
498 entries.push(ShortStatusEntry {
499 index: b'?',
500 worktree: b'?',
501 path,
502 head_mode: None,
503 index_mode: None,
504 worktree_mode: None,
505 head_oid: None,
506 index_oid: None,
507 submodule: None,
508 });
509 }
510 entries.append(&mut unmerged_entries);
511 entries.sort_by(|left, right| {
512 status_sort_category(left)
513 .cmp(&status_sort_category(right))
514 .then_with(|| left.path.cmp(&right.path))
515 });
516 return Ok(entries);
517 }
518 let index = index_entries_from_index(parsed_index);
519 let head = if head_matches_index {
520 None
521 } else {
522 Some(head_tree_entries(git_dir, format, &db)?)
523 };
524 let known_tracked_paths = index.keys().cloned().collect::<BTreeSet<_>>();
525 let tracked_paths = if options.untracked_mode == StatusUntrackedMode::None {
526 Some(&known_tracked_paths)
527 } else {
528 None
529 };
530 let mut ignores = IgnoreMatcher::from_worktree_base(worktree_root)?;
531 let (worktree, submodule_dirt_map, tracked_presence) =
532 status_worktree_entries_with_submodule_dirt(
533 worktree_root,
534 git_dir,
535 format,
536 &stat_cache,
537 Some(&known_tracked_paths),
538 tracked_paths,
539 Some(&mut ignores),
540 )?;
541 let mut entries = Vec::new();
542 if head_matches_index {
543 collect_status_entries_head_matches_index(
544 &index,
545 &worktree,
546 &tracked_presence,
547 &stat_cache,
548 sparse_checkout_active,
549 &submodule_dirt_map,
550 options.untracked_mode,
551 &mut entries,
552 );
553 } else if let Some(head) = head.as_ref() {
554 collect_status_entries_with_head(
555 StatusComparisonInputs {
556 head,
557 index: &index,
558 worktree: &worktree,
559 tracked_presence: &tracked_presence,
560 stat_cache: &stat_cache,
561 sparse_checkout_active,
562 submodule_dirt_map: &submodule_dirt_map,
563 ignores: &ignores,
564 },
565 options.untracked_mode,
566 &mut entries,
567 );
568 }
569 entries.retain(|entry| !unmerged_paths.contains(&entry.path));
570 entries.append(&mut unmerged_entries);
571 if options.include_ignored {
572 let ignored_directory_rows = matches!(options.ignored_mode, StatusIgnoredMode::Matching)
573 || !matches!(options.untracked_mode, StatusUntrackedMode::All);
574 let ignored_paths = ignored_untracked_paths(
575 worktree_root,
576 git_dir,
577 &index,
578 &ignores,
579 ignored_directory_rows,
580 )?;
581 let ignored_paths: Vec<Vec<u8>> = match options.ignored_mode {
582 StatusIgnoredMode::Matching => ignored_paths,
583 StatusIgnoredMode::Traditional
584 if matches!(options.untracked_mode, StatusUntrackedMode::All) =>
585 {
586 ignored_paths
587 }
588 StatusIgnoredMode::Traditional => {
589 let mut rolled = BTreeSet::new();
590 for path in ignored_paths {
591 let path = ignored_traditional_rollup_path(
592 worktree_root,
593 git_dir,
594 &path,
595 &index,
596 &ignores,
597 )?;
598 if ignored_traditional_path_is_empty_directory(worktree_root, &path)? {
599 continue;
600 }
601 rolled.insert(path);
602 }
603 rolled.into_iter().collect()
604 }
605 };
606 for path in ignored_paths {
607 entries.push(ShortStatusEntry {
608 index: b'!',
609 worktree: b'!',
610 path,
611 head_mode: None,
612 index_mode: None,
613 worktree_mode: None,
614 head_oid: None,
615 index_oid: None,
616 submodule: None,
617 });
618 }
619 }
620 let untracked_paths: Vec<Vec<u8>> = match options.untracked_mode {
621 StatusUntrackedMode::All => worktree
622 .iter()
623 .filter_map(|(path, entry)| {
624 let is_directory = entry.mode == 0o040000 && entry.oid.is_null();
625 if index.contains_key(path)
626 || path_or_parent_is_ignored(&ignores, path, is_directory)
627 {
628 return None;
629 }
630 if is_directory {
631 let mut directory = path.clone();
632 directory.push(b'/');
633 Some(directory)
634 } else {
635 Some(path.clone())
636 }
637 })
638 .collect(),
639 StatusUntrackedMode::Normal => {
640 normal_untracked_paths_from_worktree(&worktree, &index, &ignores)
641 }
642 StatusUntrackedMode::None => Vec::new(),
643 };
644 for path in untracked_paths {
645 entries.push(ShortStatusEntry {
646 index: b'?',
647 worktree: b'?',
648 path,
649 head_mode: None,
650 index_mode: None,
651 worktree_mode: None,
652 head_oid: None,
653 index_oid: None,
654 submodule: None,
655 });
656 }
657 entries.sort_by(|left, right| {
658 status_sort_category(left)
659 .cmp(&status_sort_category(right))
660 .then_with(|| left.path.cmp(&right.path))
661 });
662 Ok(entries)
663}
664
665pub(crate) fn short_status_unmerged_entries(index: &Index) -> Vec<ShortStatusEntry> {
666 let mut by_path: BTreeMap<Vec<u8>, BTreeSet<u16>> = BTreeMap::new();
667 for entry in &index.entries {
668 let stage = entry.stage().as_u16();
669 if stage > 0 {
670 by_path
671 .entry(entry.path.as_bytes().to_vec())
672 .or_default()
673 .insert(stage);
674 }
675 }
676 by_path
677 .into_iter()
678 .map(|(path, stages)| {
679 let (index, worktree) = short_status_unmerged_codes(&stages);
680 ShortStatusEntry {
681 index,
682 worktree,
683 path,
684 head_mode: None,
685 index_mode: None,
686 worktree_mode: None,
687 head_oid: None,
688 index_oid: None,
689 submodule: None,
690 }
691 })
692 .collect()
693}
694
695pub(crate) fn short_status_unmerged_codes(stages: &BTreeSet<u16>) -> (u8, u8) {
696 match (
697 stages.contains(&1),
698 stages.contains(&2),
699 stages.contains(&3),
700 ) {
701 (true, false, false) => (b'D', b'D'),
702 (false, true, false) => (b'A', b'U'),
703 (true, true, false) => (b'U', b'D'),
704 (false, false, true) => (b'U', b'A'),
705 (true, false, true) => (b'D', b'U'),
706 (false, true, true) => (b'A', b'A'),
707 (true, true, true) => (b'U', b'U'),
708 (false, false, false) => (b'U', b'U'),
709 }
710}
711
712pub(crate) fn sparse_checkout_active_for_status(git_dir: &Path, index: &Index) -> bool {
713 index.is_sparse()
714 || index.entries.iter().any(IndexEntry::is_sparse_dir)
715 || sparse_checkout_config_enabled(git_dir)
716}
717
718pub(crate) fn sparse_checkout_active_for_borrowed_status(
719 git_dir: &Path,
720 index: &BorrowedIndex<'_>,
721) -> bool {
722 index
723 .entries
724 .iter()
725 .any(|entry| entry.mode == SPARSE_DIR_MODE && entry.is_skip_worktree())
726 || sparse_checkout_config_enabled(git_dir)
727}
728
729pub(crate) fn sparse_checkout_config_enabled(git_dir: &Path) -> bool {
730 GitConfig::read(git_dir.join("config"))
731 .ok()
732 .and_then(|config| config.get_bool("core", None, "sparseCheckout"))
733 == Some(true)
734 || GitConfig::read(git_dir.join("config.worktree"))
735 .ok()
736 .and_then(|config| config.get_bool("core", None, "sparseCheckout"))
737 == Some(true)
738}
739
740pub(crate) fn collect_status_entries_head_matches_index(
741 index: &BTreeMap<Vec<u8>, TrackedEntry>,
742 worktree: &BTreeMap<Vec<u8>, TrackedEntry>,
743 tracked_presence: &HashSet<Vec<u8>>,
744 stat_cache: &IndexStatCache,
745 sparse_checkout_active: bool,
746 submodule_dirt_map: &BTreeMap<Vec<u8>, u8>,
747 untracked_mode: StatusUntrackedMode,
748 entries: &mut Vec<ShortStatusEntry>,
749) {
750 for (path, index_entry) in index {
751 let intent_to_add = stat_cache
752 .index_entry(path)
753 .is_some_and(IndexEntry::is_intent_to_add);
754 let visible_index_entry = (!intent_to_add).then_some(index_entry);
755 let worktree_entry = worktree.get(path);
756 let worktree_present =
757 worktree_entry.is_some() || tracked_presence.contains(path.as_slice());
758 let skip_worktree = stat_cache
761 .index_entry(path)
762 .is_some_and(index_entry_skip_worktree)
763 && (!sparse_checkout_active || !worktree_present);
764 let submodule = status_submodule_from_entries(
765 path,
766 index_entry,
767 worktree_entry,
768 submodule_dirt_map,
769 untracked_mode,
770 );
771 let worktree_code = match worktree_entry {
772 _ if skip_worktree => b' ',
773 None if intent_to_add => b' ',
774 None if !worktree_present => b'D',
775 Some(_) if intent_to_add => b'A',
776 Some(worktree_entry) if Some(worktree_entry) != visible_index_entry => {
777 status_change_code(index_entry.mode, worktree_entry.mode)
778 }
779 _ if submodule.is_some_and(|sub| sub.any()) => b'M',
780 _ => b' ',
781 };
782 if worktree_code != b' ' {
783 entries.push(ShortStatusEntry {
784 index: b' ',
785 worktree: worktree_code,
786 path: path.clone(),
787 head_mode: visible_index_entry.map(|entry| entry.mode),
788 index_mode: visible_index_entry.map(|entry| entry.mode),
789 worktree_mode: status_worktree_mode(
790 visible_index_entry,
791 worktree_entry,
792 worktree_present,
793 ),
794 head_oid: visible_index_entry.map(|entry| entry.oid),
795 index_oid: visible_index_entry.map(|entry| entry.oid),
796 submodule: submodule.filter(|sub| sub.any()),
797 });
798 }
799 }
800}
801
802pub(crate) struct StatusComparisonInputs<'a> {
803 head: &'a BTreeMap<Vec<u8>, TrackedEntry>,
804 index: &'a BTreeMap<Vec<u8>, TrackedEntry>,
805 worktree: &'a BTreeMap<Vec<u8>, TrackedEntry>,
806 tracked_presence: &'a HashSet<Vec<u8>>,
807 stat_cache: &'a IndexStatCache,
808 sparse_checkout_active: bool,
809 submodule_dirt_map: &'a BTreeMap<Vec<u8>, u8>,
810 ignores: &'a IgnoreMatcher,
811}
812
813pub(crate) fn collect_status_entries_with_head(
814 inputs: StatusComparisonInputs<'_>,
815 untracked_mode: StatusUntrackedMode,
816 entries: &mut Vec<ShortStatusEntry>,
817) {
818 let mut paths = BTreeSet::new();
819 paths.extend(inputs.head.keys().cloned());
820 paths.extend(inputs.index.keys().cloned());
821 paths.extend(
822 inputs
823 .worktree
824 .keys()
825 .filter(|path| inputs.index.contains_key(*path))
826 .cloned(),
827 );
828
829 for path in paths {
830 let head_entry = inputs.head.get(&path);
831 let index_entry = inputs.index.get(&path);
832 let intent_to_add = inputs
833 .stat_cache
834 .index_entry(&path)
835 .is_some_and(IndexEntry::is_intent_to_add);
836 let visible_index_entry = index_entry.filter(|_| !intent_to_add);
837 let worktree_entry = inputs.worktree.get(&path);
838 let worktree_present =
839 worktree_entry.is_some() || inputs.tracked_presence.contains(path.as_slice());
840 if head_entry.is_none()
841 && index_entry.is_none()
842 && worktree_entry.is_some()
843 && inputs.ignores.is_ignored(&path, false)
844 {
845 continue;
846 }
847 let submodule = match visible_index_entry {
848 Some(index_entry) => status_submodule_from_entries(
849 &path,
850 index_entry,
851 worktree_entry,
852 inputs.submodule_dirt_map,
853 untracked_mode,
854 ),
855 None => None,
856 };
857 let skip_worktree = visible_index_entry.is_some_and(|_| {
860 inputs
861 .stat_cache
862 .index_entry(&path)
863 .is_some_and(index_entry_skip_worktree)
864 }) && (!inputs.sparse_checkout_active || !worktree_present);
865 let (index_code, worktree_code) =
866 if head_entry.is_none() && index_entry.is_none() && worktree_entry.is_some() {
867 (b'?', b'?')
868 } else {
869 let index_code = match (head_entry, visible_index_entry) {
870 (None, Some(_)) => b'A',
871 (Some(_), None) => b'D',
872 (Some(left), Some(right)) if left != right => {
873 status_change_code(left.mode, right.mode)
874 }
875 _ => b' ',
876 };
877 let worktree_code = match (visible_index_entry, worktree_entry) {
878 (None, Some(_)) if intent_to_add => b'A',
879 (None, Some(_)) => b'?',
880 (None, None) if intent_to_add => b' ',
881 (Some(_), _) if skip_worktree => b' ',
882 (Some(_), None) if !worktree_present => b'D',
883 (Some(left), Some(right)) if left != right => {
884 status_change_code(left.mode, right.mode)
885 }
886 _ if submodule.is_some_and(|sub| sub.any()) => b'M',
887 _ => b' ',
888 };
889 (index_code, worktree_code)
890 };
891 if index_code != b' ' || worktree_code != b' ' {
892 let worktree_mode = if skip_worktree {
893 visible_index_entry.map(|entry| entry.mode)
894 } else {
895 status_worktree_mode(visible_index_entry, worktree_entry, worktree_present)
896 };
897 entries.push(ShortStatusEntry {
898 index: index_code,
899 worktree: worktree_code,
900 path,
901 head_mode: head_entry.map(|entry| entry.mode),
902 index_mode: visible_index_entry.map(|entry| entry.mode),
903 worktree_mode,
904 head_oid: head_entry.map(|entry| entry.oid),
905 index_oid: visible_index_entry.map(|entry| entry.oid),
906 submodule: submodule.filter(|sub| sub.any()),
907 });
908 }
909 }
910}
911
912pub(crate) fn status_worktree_mode(
913 index_entry: Option<&TrackedEntry>,
914 worktree_entry: Option<&TrackedEntry>,
915 worktree_present: bool,
916) -> Option<u32> {
917 worktree_entry.map(|entry| entry.mode).or_else(|| {
918 worktree_present
919 .then(|| index_entry.map(|entry| entry.mode))
920 .flatten()
921 })
922}
923
924pub(crate) fn status_submodule_from_entries(
925 path: &[u8],
926 index_entry: &TrackedEntry,
927 worktree_entry: Option<&TrackedEntry>,
928 submodule_dirt_map: &BTreeMap<Vec<u8>, u8>,
929 _untracked_mode: StatusUntrackedMode,
930) -> Option<SubmoduleStatus> {
931 let worktree_entry = worktree_entry?;
932 if !sley_index::is_gitlink(index_entry.mode) || !sley_index::is_gitlink(worktree_entry.mode) {
933 return None;
934 }
935 let dirt = submodule_dirt_map.get(path).copied().unwrap_or(0);
936 Some(SubmoduleStatus {
937 new_commits: index_entry.oid != worktree_entry.oid,
938 modified_content: dirt & DIRTY_SUBMODULE_MODIFIED != 0,
939 untracked_content: dirt & DIRTY_SUBMODULE_UNTRACKED != 0,
940 })
941}
942
943pub(crate) fn short_status_tracked_only(
944 worktree_root: &Path,
945 git_dir: &Path,
946 format: ObjectFormat,
947 db: &FileObjectDatabase,
948 index: &Index,
949 stat_cache: &IndexStatCache,
950 head_matches_index: bool,
951 sparse_checkout_active: bool,
952 untracked_mode: StatusUntrackedMode,
953) -> Result<Vec<ShortStatusEntry>> {
954 let normal_entry_count = index
955 .entries
956 .iter()
957 .filter(|entry| entry.stage() == Stage::Normal)
958 .count();
959 if head_matches_index && normal_entry_count >= 512 {
960 return short_status_tracked_only_head_matches_index_parallel(
961 worktree_root,
962 git_dir,
963 format,
964 index,
965 stat_cache,
966 sparse_checkout_active,
967 untracked_mode,
968 );
969 }
970 let head = if head_matches_index {
971 None
972 } else {
973 Some(head_tree_entries(git_dir, format, db)?)
974 };
975 if !head_matches_index && normal_entry_count >= 512 {
976 if let Some(head) = head.as_ref() {
977 return short_status_tracked_only_with_head_parallel(
978 worktree_root,
979 git_dir,
980 format,
981 index,
982 stat_cache,
983 head,
984 sparse_checkout_active,
985 untracked_mode,
986 );
987 }
988 }
989 let mut clean_filter = None;
990 let mut entries = Vec::new();
991 for entry in index
992 .entries
993 .iter()
994 .filter(|entry| entry.stage() == Stage::Normal)
995 {
996 let path = entry.path.as_bytes();
997 let index_entry = TrackedEntry {
998 mode: entry.mode,
999 oid: entry.oid,
1000 };
1001 let head_entry = if head_matches_index {
1002 (!entry.is_intent_to_add()).then_some(&index_entry)
1003 } else {
1004 head.as_ref().and_then(|head| head.get(path))
1005 };
1006 let worktree_entry = worktree_entry_for_index_entry_with_attributes(
1007 worktree_root,
1008 git_dir,
1009 format,
1010 entry,
1011 stat_cache,
1012 &mut clean_filter,
1013 )?;
1014 let submodule = tracked_only_submodule_status(
1015 worktree_root,
1016 path,
1017 &index_entry,
1018 worktree_entry.as_ref(),
1019 untracked_mode,
1020 )?;
1021 let visible_index_entry = (!entry.is_intent_to_add()).then_some(&index_entry);
1022 let index_code = match (head_entry, visible_index_entry) {
1023 (None, Some(_)) => b'A',
1024 (Some(_), None) => b'D',
1025 (Some(head_entry), Some(index_entry)) if *head_entry != *index_entry => {
1026 status_change_code(head_entry.mode, index_entry.mode)
1027 }
1028 _ => b' ',
1029 };
1030 let skip_worktree =
1033 entry.is_skip_worktree() && (!sparse_checkout_active || worktree_entry.is_none());
1034 let worktree_code = match worktree_entry.as_ref() {
1035 _ if skip_worktree => b' ',
1036 None if entry.is_intent_to_add() => b' ',
1037 None => b'D',
1038 Some(_) if entry.is_intent_to_add() => b'A',
1039 Some(worktree_entry) if Some(worktree_entry) != visible_index_entry => {
1040 status_change_code(index_entry.mode, worktree_entry.mode)
1041 }
1042 _ if submodule.is_some_and(|sub| sub.any()) => b'M',
1043 _ => b' ',
1044 };
1045 if index_code != b' ' || worktree_code != b' ' {
1046 entries.push(ShortStatusEntry {
1047 index: index_code,
1048 worktree: worktree_code,
1049 path: path.to_vec(),
1050 head_mode: head_entry.map(|entry| entry.mode),
1051 index_mode: visible_index_entry.map(|entry| entry.mode),
1052 worktree_mode: if skip_worktree {
1053 visible_index_entry.map(|entry| entry.mode)
1054 } else {
1055 worktree_entry.as_ref().map(|entry| entry.mode)
1056 },
1057 head_oid: head_entry.map(|entry| entry.oid),
1058 index_oid: visible_index_entry.map(|entry| entry.oid),
1059 submodule: submodule.filter(|sub| sub.any()),
1060 });
1061 }
1062 }
1063 if let Some(head) = head.as_ref() {
1064 let index_paths = index
1065 .entries
1066 .iter()
1067 .filter(|entry| entry.stage() == Stage::Normal)
1068 .map(|entry| entry.path.as_bytes().to_vec())
1069 .collect::<HashSet<_>>();
1070 for (path, head_entry) in head {
1071 if index_paths.contains(path.as_slice()) {
1072 continue;
1073 }
1074 entries.push(ShortStatusEntry {
1075 index: b'D',
1076 worktree: b' ',
1077 path: path.clone(),
1078 head_mode: Some(head_entry.mode),
1079 index_mode: None,
1080 worktree_mode: None,
1081 head_oid: Some(head_entry.oid),
1082 index_oid: None,
1083 submodule: None,
1084 });
1085 }
1086 }
1087 entries.sort_by(|left, right| {
1088 status_sort_category(left)
1089 .cmp(&status_sort_category(right))
1090 .then_with(|| left.path.cmp(&right.path))
1091 });
1092 Ok(entries)
1093}
1094
1095pub(crate) fn short_status_borrowed_head_matches_index_if_possible(
1096 worktree_root: &Path,
1097 git_dir: &Path,
1098 format: ObjectFormat,
1099 db: &FileObjectDatabase,
1100 untracked_mode: StatusUntrackedMode,
1101) -> Result<Option<Vec<ShortStatusEntry>>> {
1102 let index_path = repository_index_path(git_dir);
1103 let index_metadata = match fs::metadata(&index_path) {
1104 Ok(metadata) => metadata,
1105 Err(err)
1106 if err.kind() == std::io::ErrorKind::NotFound
1107 && matches!(untracked_mode, StatusUntrackedMode::None) =>
1108 {
1109 return Ok(Some(Vec::new()));
1110 }
1111 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
1112 Err(err) => return Err(err.into()),
1113 };
1114 let index_bytes = read_borrowed_index_bytes(&index_path)?;
1115 status_profile_mem(
1116 "after_index_bytes",
1117 &[
1118 ("index_file_bytes", index_metadata.len() as usize),
1119 ("index_bytes_len", index_bytes.as_ref().len()),
1120 (
1121 "index_bytes_mapped",
1122 usize::from(matches!(index_bytes, BorrowedIndexBytes::Mapped(_))),
1123 ),
1124 ],
1125 );
1126 let borrowed = match BorrowedIndex::parse(index_bytes.as_ref(), format) {
1127 Ok(index) => index,
1128 Err(GitError::Unsupported(_)) => return Ok(None),
1129 Err(err) => return Err(err),
1130 };
1131 status_profile_mem(
1132 "after_borrowed_parse",
1133 &[
1134 ("index_file_bytes", index_metadata.len() as usize),
1135 ("index_bytes_len", index_bytes.as_ref().len()),
1136 (
1137 "index_bytes_mapped",
1138 usize::from(matches!(index_bytes, BorrowedIndexBytes::Mapped(_))),
1139 ),
1140 ("borrowed_entries_len", borrowed.entries.len()),
1141 ("borrowed_entries_cap", borrowed.entries.capacity()),
1142 (
1143 "borrowed_entry_size",
1144 std::mem::size_of::<IndexEntryRef<'_>>(),
1145 ),
1146 (
1147 "borrowed_entries_cap_bytes",
1148 borrowed.entries.capacity() * std::mem::size_of::<IndexEntryRef<'_>>(),
1149 ),
1150 ("borrowed_extensions_len", borrowed.extensions.len()),
1151 ],
1152 );
1153 let sparse_checkout_active = sparse_checkout_active_for_borrowed_status(git_dir, &borrowed);
1154 if borrowed
1155 .entries
1156 .iter()
1157 .any(|entry| entry.mode == SPARSE_DIR_MODE && entry.is_skip_worktree())
1158 {
1159 return Ok(None);
1160 }
1161 if borrowed
1162 .entries
1163 .iter()
1164 .any(|entry| entry.stage() != Stage::Normal)
1165 {
1166 return Ok(None);
1167 }
1168 status_profile_mem(
1169 "after_sparse_scan",
1170 &[
1171 ("borrowed_entries_len", borrowed.entries.len()),
1172 (
1173 "borrowed_entries_cap_bytes",
1174 borrowed.entries.capacity() * std::mem::size_of::<IndexEntryRef<'_>>(),
1175 ),
1176 (
1177 "sparse_checkout_active",
1178 usize::from(sparse_checkout_active),
1179 ),
1180 ],
1181 );
1182 let Some(head_tree_oid) = resolve_head_tree_oid(git_dir, format, db)? else {
1183 return Ok(None);
1184 };
1185 status_profile_mem(
1186 "after_head_tree_oid",
1187 &[
1188 ("borrowed_entries_len", borrowed.entries.len()),
1189 (
1190 "borrowed_entries_cap_bytes",
1191 borrowed.entries.capacity() * std::mem::size_of::<IndexEntryRef<'_>>(),
1192 ),
1193 ],
1194 );
1195 let stage0_entry_count = borrowed
1196 .entries
1197 .iter()
1198 .filter(|entry| entry.stage() == Stage::Normal)
1199 .count();
1200 status_profile_mem(
1201 "after_stage0_count",
1202 &[
1203 ("stage0_entry_count", stage0_entry_count),
1204 ("borrowed_entries_len", borrowed.entries.len()),
1205 ],
1206 );
1207 if !head_matches_borrowed_index_from_entries(&borrowed, format, &head_tree_oid)? {
1208 return Ok(None);
1209 }
1210 status_profile_mem(
1211 "after_head_matches_index",
1212 &[
1213 ("stage0_entry_count", stage0_entry_count),
1214 ("borrowed_entries_len", borrowed.entries.len()),
1215 (
1216 "borrowed_entries_cap_bytes",
1217 borrowed.entries.capacity() * std::mem::size_of::<IndexEntryRef<'_>>(),
1218 ),
1219 ],
1220 );
1221
1222 let index_mtime = file_mtime_parts(&index_metadata);
1223 let stat_cache = IndexStatCache::from_index_mtime_only(index_mtime);
1224 let profile_enabled = StatusProfileCounters::enabled();
1225 let mut profile = profile_enabled.then(|| StatusProfileCounters {
1226 fast_path_borrowed: true,
1227 ..StatusProfileCounters::default()
1228 });
1229
1230 if matches!(untracked_mode, StatusUntrackedMode::None) {
1231 let tracked_start = Instant::now();
1232 let entries = short_status_borrowed_tracked_only_head_matches_index_parallel(
1233 worktree_root,
1234 git_dir,
1235 format,
1236 &borrowed,
1237 &stat_cache,
1238 sparse_checkout_active,
1239 untracked_mode,
1240 )?;
1241 if let Some(profile) = profile.as_mut() {
1242 profile.tracked_elapsed_us = tracked_start.elapsed().as_micros();
1243 profile.emit();
1244 }
1245 return Ok(Some(entries));
1246 }
1247
1248 if stage0_entry_count < STATUS_BORROWED_OVERLAP_MIN_STAGE0 {
1249 let tracked_start = Instant::now();
1250 let mut entries = short_status_borrowed_tracked_only_head_matches_index_parallel(
1251 worktree_root,
1252 git_dir,
1253 format,
1254 &borrowed,
1255 &stat_cache,
1256 sparse_checkout_active,
1257 untracked_mode,
1258 )?;
1259 if let Some(profile) = profile.as_mut() {
1260 profile.tracked_elapsed_us = tracked_start.elapsed().as_micros();
1261 }
1262 let mut ignores = IgnoreMatcher::from_worktree_base(worktree_root)?;
1263 let untracked_start = Instant::now();
1264 let untracked_paths = status_untracked_paths_from_borrowed_index(
1265 worktree_root,
1266 git_dir,
1267 &borrowed,
1268 &mut ignores,
1269 untracked_mode,
1270 profile.as_mut(),
1271 )?;
1272 if let Some(profile) = profile.as_mut() {
1273 profile.untracked_elapsed_us = untracked_start.elapsed().as_micros();
1274 profile.untracked_rows = untracked_paths.len() as u64;
1275 }
1276 let render_start = Instant::now();
1277 append_untracked_status_entries(&mut entries, untracked_paths);
1278 if let Some(profile) = profile.as_mut() {
1279 profile.render_elapsed_us = render_start.elapsed().as_micros();
1280 profile.emit();
1281 }
1282 return Ok(Some(entries));
1283 }
1284
1285 if let Some(profile) = profile.as_mut() {
1286 profile.overlap_enabled = true;
1287 }
1288 let executor = StatusExecutor::new();
1289 if profile_enabled {
1290 let (mut entries, untracked_paths, untracked_profile) =
1291 std::thread::scope(|scope| -> Result<_> {
1292 let tracked = executor.spawn(scope, "status-tracked", || {
1293 let start = Instant::now();
1294 short_status_borrowed_tracked_only_head_matches_index_parallel(
1295 worktree_root,
1296 git_dir,
1297 format,
1298 &borrowed,
1299 &stat_cache,
1300 sparse_checkout_active,
1301 untracked_mode,
1302 )
1303 .map(|entries| (entries, start.elapsed().as_micros()))
1304 })?;
1305 let untracked = executor.spawn(
1306 scope,
1307 "status-untracked",
1308 || -> Result<(Vec<Vec<u8>>, StatusProfileCounters)> {
1309 let mut local_profile = StatusProfileCounters::default();
1310 let mut ignores = IgnoreMatcher::from_worktree_base(worktree_root)?;
1311 let start = Instant::now();
1312 let paths = status_untracked_paths_from_borrowed_index(
1313 worktree_root,
1314 git_dir,
1315 &borrowed,
1316 &mut ignores,
1317 untracked_mode,
1318 Some(&mut local_profile),
1319 )?;
1320 local_profile.untracked_elapsed_us = start.elapsed().as_micros();
1321 local_profile.untracked_rows = paths.len() as u64;
1322 Ok((paths, local_profile))
1323 },
1324 )?;
1325 let (entries, tracked_elapsed_us) = tracked.join()?;
1326 let (untracked_paths, untracked_profile) = untracked.join()?;
1327 if let Some(profile) = profile.as_mut() {
1328 profile.tracked_elapsed_us = tracked_elapsed_us;
1329 }
1330 Ok((entries, untracked_paths, Some(untracked_profile)))
1331 })?;
1332 if let Some(profile) = profile.as_mut() {
1333 if let Some(untracked_profile) = untracked_profile {
1334 profile.merge_untracked(untracked_profile);
1335 }
1336 }
1337 let render_start = Instant::now();
1338 append_untracked_status_entries(&mut entries, untracked_paths);
1339 if let Some(profile) = profile.as_mut() {
1340 profile.render_elapsed_us = render_start.elapsed().as_micros();
1341 profile.emit();
1342 }
1343 return Ok(Some(entries));
1344 }
1345 let (mut entries, untracked_paths) = std::thread::scope(|scope| -> Result<_> {
1346 let tracked = executor.spawn(scope, "status-tracked", || {
1347 short_status_borrowed_tracked_only_head_matches_index_parallel(
1348 worktree_root,
1349 git_dir,
1350 format,
1351 &borrowed,
1352 &stat_cache,
1353 sparse_checkout_active,
1354 untracked_mode,
1355 )
1356 })?;
1357 let untracked = executor.spawn(scope, "status-untracked", || -> Result<Vec<Vec<u8>>> {
1358 let mut ignores = IgnoreMatcher::from_worktree_base(worktree_root)?;
1359 status_untracked_paths_from_borrowed_index(
1360 worktree_root,
1361 git_dir,
1362 &borrowed,
1363 &mut ignores,
1364 untracked_mode,
1365 None,
1366 )
1367 })?;
1368 let entries = tracked.join()?;
1369 let untracked_paths = untracked.join()?;
1370 Ok((entries, untracked_paths))
1371 })?;
1372 let render_start = Instant::now();
1373 append_untracked_status_entries(&mut entries, untracked_paths);
1374 if let Some(profile) = profile.as_mut() {
1375 profile.render_elapsed_us = render_start.elapsed().as_micros();
1376 profile.emit();
1377 }
1378 Ok(Some(entries))
1379}
1380
1381pub(crate) fn stream_short_status_borrowed_head_matches_index_if_possible<F>(
1382 worktree_root: &Path,
1383 git_dir: &Path,
1384 format: ObjectFormat,
1385 db: &FileObjectDatabase,
1386 untracked_mode: StatusUntrackedMode,
1387 emit: &mut F,
1388) -> Result<Option<()>>
1389where
1390 F: for<'a> FnMut(ShortStatusRow<'a>) -> Result<StreamControl>,
1391{
1392 let index_path = repository_index_path(git_dir);
1393 let index_metadata = match fs::metadata(&index_path) {
1394 Ok(metadata) => metadata,
1395 Err(err)
1396 if err.kind() == std::io::ErrorKind::NotFound
1397 && matches!(untracked_mode, StatusUntrackedMode::None) =>
1398 {
1399 return Ok(Some(()));
1400 }
1401 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
1402 Err(err) => return Err(err.into()),
1403 };
1404 let index_bytes = read_borrowed_index_bytes(&index_path)?;
1405 status_profile_mem(
1406 "after_index_bytes",
1407 &[
1408 ("index_file_bytes", index_metadata.len() as usize),
1409 ("index_bytes_len", index_bytes.as_ref().len()),
1410 (
1411 "index_bytes_mapped",
1412 usize::from(matches!(index_bytes, BorrowedIndexBytes::Mapped(_))),
1413 ),
1414 ],
1415 );
1416 let borrowed = match BorrowedIndex::parse(index_bytes.as_ref(), format) {
1417 Ok(index) => index,
1418 Err(GitError::Unsupported(_)) => return Ok(None),
1419 Err(err) => return Err(err),
1420 };
1421 status_profile_mem(
1422 "after_borrowed_parse",
1423 &[
1424 ("index_file_bytes", index_metadata.len() as usize),
1425 ("index_bytes_len", index_bytes.as_ref().len()),
1426 (
1427 "index_bytes_mapped",
1428 usize::from(matches!(index_bytes, BorrowedIndexBytes::Mapped(_))),
1429 ),
1430 ("borrowed_entries_len", borrowed.entries.len()),
1431 ("borrowed_entries_cap", borrowed.entries.capacity()),
1432 (
1433 "borrowed_entry_size",
1434 std::mem::size_of::<IndexEntryRef<'_>>(),
1435 ),
1436 (
1437 "borrowed_entries_cap_bytes",
1438 borrowed.entries.capacity() * std::mem::size_of::<IndexEntryRef<'_>>(),
1439 ),
1440 ("borrowed_extensions_len", borrowed.extensions.len()),
1441 ],
1442 );
1443 let sparse_checkout_active = sparse_checkout_active_for_borrowed_status(git_dir, &borrowed);
1444 if borrowed
1445 .entries
1446 .iter()
1447 .any(|entry| entry.mode == SPARSE_DIR_MODE && entry.is_skip_worktree())
1448 {
1449 return Ok(None);
1450 }
1451 if borrowed
1452 .entries
1453 .iter()
1454 .any(|entry| entry.stage() != Stage::Normal)
1455 {
1456 return Ok(None);
1457 }
1458 status_profile_mem(
1459 "after_sparse_scan",
1460 &[
1461 ("borrowed_entries_len", borrowed.entries.len()),
1462 (
1463 "borrowed_entries_cap_bytes",
1464 borrowed.entries.capacity() * std::mem::size_of::<IndexEntryRef<'_>>(),
1465 ),
1466 (
1467 "sparse_checkout_active",
1468 usize::from(sparse_checkout_active),
1469 ),
1470 ],
1471 );
1472 let Some(head_tree_oid) = resolve_head_tree_oid(git_dir, format, db)? else {
1473 return Ok(None);
1474 };
1475 status_profile_mem(
1476 "after_head_tree_oid",
1477 &[
1478 ("borrowed_entries_len", borrowed.entries.len()),
1479 (
1480 "borrowed_entries_cap_bytes",
1481 borrowed.entries.capacity() * std::mem::size_of::<IndexEntryRef<'_>>(),
1482 ),
1483 ],
1484 );
1485 let stage0_entry_count = borrowed
1486 .entries
1487 .iter()
1488 .filter(|entry| entry.stage() == Stage::Normal)
1489 .count();
1490 status_profile_mem(
1491 "after_stage0_count",
1492 &[
1493 ("stage0_entry_count", stage0_entry_count),
1494 ("borrowed_entries_len", borrowed.entries.len()),
1495 ],
1496 );
1497 if !head_matches_borrowed_index_from_entries(&borrowed, format, &head_tree_oid)? {
1498 return Ok(None);
1499 }
1500 status_profile_mem(
1501 "after_head_matches_index",
1502 &[
1503 ("stage0_entry_count", stage0_entry_count),
1504 ("borrowed_entries_len", borrowed.entries.len()),
1505 (
1506 "borrowed_entries_cap_bytes",
1507 borrowed.entries.capacity() * std::mem::size_of::<IndexEntryRef<'_>>(),
1508 ),
1509 ],
1510 );
1511
1512 let index_mtime = file_mtime_parts(&index_metadata);
1513 let stat_cache = IndexStatCache::from_index_mtime_only(index_mtime);
1514 let profile_enabled = StatusProfileCounters::enabled();
1515 let mut profile = profile_enabled.then(|| StatusProfileCounters {
1516 fast_path_borrowed: true,
1517 ..StatusProfileCounters::default()
1518 });
1519
1520 if matches!(untracked_mode, StatusUntrackedMode::None) {
1521 let tracked_start = Instant::now();
1522 let tracked_control =
1523 stream_short_status_borrowed_tracked_only_head_matches_index_parallel(
1524 worktree_root,
1525 git_dir,
1526 format,
1527 &borrowed,
1528 &stat_cache,
1529 sparse_checkout_active,
1530 untracked_mode,
1531 emit,
1532 )?;
1533 if let Some(profile) = profile.as_mut() {
1534 profile.tracked_elapsed_us = tracked_start.elapsed().as_micros();
1535 }
1536 if let Some(profile) = profile.as_ref() {
1537 profile.emit();
1538 }
1539 if tracked_control.is_stop() {
1540 return Ok(Some(()));
1541 }
1542 return Ok(Some(()));
1543 }
1544
1545 if stage0_entry_count < STATUS_BORROWED_OVERLAP_MIN_STAGE0 {
1546 let tracked_start = Instant::now();
1547 let tracked_control =
1548 stream_short_status_borrowed_tracked_only_head_matches_index_parallel(
1549 worktree_root,
1550 git_dir,
1551 format,
1552 &borrowed,
1553 &stat_cache,
1554 sparse_checkout_active,
1555 untracked_mode,
1556 emit,
1557 )?;
1558 if let Some(profile) = profile.as_mut() {
1559 profile.tracked_elapsed_us = tracked_start.elapsed().as_micros();
1560 }
1561 if tracked_control.is_stop() {
1562 if let Some(profile) = profile.as_ref() {
1563 profile.emit();
1564 }
1565 return Ok(Some(()));
1566 }
1567 let mut ignores = IgnoreMatcher::from_worktree_base(worktree_root)?;
1568 let untracked_start = Instant::now();
1569 stream_status_untracked_paths_from_borrowed_index(
1570 worktree_root,
1571 git_dir,
1572 &borrowed,
1573 &mut ignores,
1574 untracked_mode,
1575 profile.as_mut(),
1576 emit_untracked_status_entry(emit),
1577 )?;
1578 if let Some(profile) = profile.as_mut() {
1579 profile.untracked_elapsed_us = untracked_start.elapsed().as_micros();
1580 profile.emit();
1581 }
1582 return Ok(Some(()));
1583 }
1584
1585 if let Some(profile) = profile.as_mut() {
1586 profile.overlap_enabled = true;
1587 }
1588 let executor = StatusExecutor::new();
1589 let (tracked_control, untracked_paths, untracked_profile) =
1590 std::thread::scope(|scope| -> Result<_> {
1591 let untracked = executor.spawn(
1592 scope,
1593 "status-untracked",
1594 || -> Result<(Vec<Vec<u8>>, StatusProfileCounters)> {
1595 let mut local_profile = StatusProfileCounters::default();
1596 let mut ignores = IgnoreMatcher::from_worktree_base(worktree_root)?;
1597 ignores.emit_memory_profile("after_untracked_ignore");
1598 let start = Instant::now();
1599 let paths = status_untracked_paths_from_borrowed_index(
1600 worktree_root,
1601 git_dir,
1602 &borrowed,
1603 &mut ignores,
1604 untracked_mode,
1605 profile_enabled.then_some(&mut local_profile),
1606 )?;
1607 status_profile_mem(
1608 "after_untracked_collect",
1609 &[
1610 ("untracked_paths_len", paths.len()),
1611 ("untracked_paths_cap", paths.capacity()),
1612 (
1613 "untracked_paths_cap_bytes",
1614 paths.capacity() * std::mem::size_of::<Vec<u8>>(),
1615 ),
1616 (
1617 "untracked_path_payload_bytes",
1618 paths.iter().map(Vec::capacity).sum(),
1619 ),
1620 ],
1621 );
1622 local_profile.untracked_elapsed_us = start.elapsed().as_micros();
1623 local_profile.untracked_rows = paths.len() as u64;
1624 Ok((paths, local_profile))
1625 },
1626 )?;
1627 let tracked_start = Instant::now();
1628 let tracked_control =
1629 stream_short_status_borrowed_tracked_only_head_matches_index_parallel(
1630 worktree_root,
1631 git_dir,
1632 format,
1633 &borrowed,
1634 &stat_cache,
1635 sparse_checkout_active,
1636 untracked_mode,
1637 emit,
1638 )?;
1639 let tracked_elapsed_us = tracked_start.elapsed().as_micros();
1640 let (untracked_paths, untracked_profile) = untracked.join()?;
1641 if let Some(profile) = profile.as_mut() {
1642 profile.tracked_elapsed_us = tracked_elapsed_us;
1643 }
1644 Ok((
1645 tracked_control,
1646 untracked_paths,
1647 profile_enabled.then_some(untracked_profile),
1648 ))
1649 })?;
1650 status_profile_mem(
1651 "after_join",
1652 &[
1653 ("untracked_paths_len", untracked_paths.len()),
1654 ("untracked_paths_cap", untracked_paths.capacity()),
1655 (
1656 "untracked_paths_cap_bytes",
1657 untracked_paths.capacity() * std::mem::size_of::<Vec<u8>>(),
1658 ),
1659 (
1660 "untracked_path_payload_bytes",
1661 untracked_paths.iter().map(Vec::capacity).sum(),
1662 ),
1663 ],
1664 );
1665 if tracked_control.is_stop() {
1666 if let Some(profile) = profile.as_mut()
1667 && let Some(untracked_profile) = untracked_profile
1668 {
1669 profile.merge_untracked(untracked_profile);
1670 profile.emit();
1671 }
1672 return Ok(Some(()));
1673 }
1674 if let Some(profile) = profile.as_mut()
1675 && let Some(untracked_profile) = untracked_profile
1676 {
1677 profile.merge_untracked(untracked_profile);
1678 }
1679 let render_start = Instant::now();
1680 for path in untracked_paths {
1681 let row = untracked_status_row(&path);
1682 if emit(row)?.is_stop() {
1683 break;
1684 }
1685 }
1686 if let Some(profile) = profile.as_mut() {
1687 profile.render_elapsed_us = render_start.elapsed().as_micros();
1688 profile.emit();
1689 }
1690 status_profile_mem("after_render", &[]);
1691 Ok(Some(()))
1692}
1693
1694pub(crate) fn short_status_borrowed_head_matches_index_count_if_possible(
1695 worktree_root: &Path,
1696 git_dir: &Path,
1697 format: ObjectFormat,
1698 db: &FileObjectDatabase,
1699 untracked_mode: StatusUntrackedMode,
1700) -> Result<Option<usize>> {
1701 let index_path = repository_index_path(git_dir);
1702 let index_metadata = match fs::metadata(&index_path) {
1703 Ok(metadata) => metadata,
1704 Err(err)
1705 if err.kind() == std::io::ErrorKind::NotFound
1706 && matches!(untracked_mode, StatusUntrackedMode::None) =>
1707 {
1708 return Ok(Some(0));
1709 }
1710 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
1711 Err(err) => return Err(err.into()),
1712 };
1713 let index_bytes = read_borrowed_index_bytes(&index_path)?;
1714 let borrowed = match BorrowedIndex::parse(index_bytes.as_ref(), format) {
1715 Ok(index) => index,
1716 Err(GitError::Unsupported(_)) => return Ok(None),
1717 Err(err) => return Err(err),
1718 };
1719 let sparse_checkout_active = sparse_checkout_active_for_borrowed_status(git_dir, &borrowed);
1720 if borrowed
1721 .entries
1722 .iter()
1723 .any(|entry| entry.mode == SPARSE_DIR_MODE && entry.is_skip_worktree())
1724 {
1725 return Ok(None);
1726 }
1727 let Some(head_tree_oid) = resolve_head_tree_oid(git_dir, format, db)? else {
1728 return Ok(None);
1729 };
1730 let stage0_entry_count = borrowed
1731 .entries
1732 .iter()
1733 .filter(|entry| entry.stage() == Stage::Normal)
1734 .count();
1735 if !head_matches_borrowed_index_from_entries(&borrowed, format, &head_tree_oid)? {
1736 return Ok(None);
1737 }
1738
1739 let index_mtime = file_mtime_parts(&index_metadata);
1740 let stat_cache = IndexStatCache::from_index_mtime_only(index_mtime);
1741 let profile_enabled = StatusProfileCounters::enabled();
1742 let mut profile = profile_enabled.then(|| StatusProfileCounters {
1743 fast_path_borrowed: true,
1744 ..StatusProfileCounters::default()
1745 });
1746
1747 if matches!(untracked_mode, StatusUntrackedMode::None) {
1748 let tracked_start = Instant::now();
1749 let count = short_status_borrowed_tracked_only_head_matches_index_count_parallel(
1750 worktree_root,
1751 git_dir,
1752 format,
1753 &borrowed,
1754 &stat_cache,
1755 sparse_checkout_active,
1756 untracked_mode,
1757 )?;
1758 if let Some(profile) = profile.as_mut() {
1759 profile.tracked_elapsed_us = tracked_start.elapsed().as_micros();
1760 profile.emit();
1761 }
1762 return Ok(Some(count));
1763 }
1764
1765 if stage0_entry_count < STATUS_BORROWED_OVERLAP_MIN_STAGE0 {
1766 let tracked_start = Instant::now();
1767 let tracked_count = short_status_borrowed_tracked_only_head_matches_index_count_parallel(
1768 worktree_root,
1769 git_dir,
1770 format,
1771 &borrowed,
1772 &stat_cache,
1773 sparse_checkout_active,
1774 untracked_mode,
1775 )?;
1776 if let Some(profile) = profile.as_mut() {
1777 profile.tracked_elapsed_us = tracked_start.elapsed().as_micros();
1778 }
1779 let mut ignores = IgnoreMatcher::from_worktree_base(worktree_root)?;
1780 let untracked_start = Instant::now();
1781 let untracked_count = status_untracked_count_from_borrowed_index(
1782 worktree_root,
1783 git_dir,
1784 &borrowed,
1785 &mut ignores,
1786 untracked_mode,
1787 profile.as_mut(),
1788 )?;
1789 if let Some(profile) = profile.as_mut() {
1790 profile.untracked_elapsed_us = untracked_start.elapsed().as_micros();
1791 profile.untracked_rows = untracked_count as u64;
1792 profile.emit();
1793 }
1794 return Ok(Some(tracked_count + untracked_count));
1795 }
1796
1797 if let Some(profile) = profile.as_mut() {
1798 profile.overlap_enabled = true;
1799 }
1800 let executor = StatusExecutor::new();
1801 let (tracked_count, untracked_count, untracked_profile) =
1802 std::thread::scope(|scope| -> Result<_> {
1803 let tracked = executor.spawn(scope, "status-tracked", || {
1804 let start = Instant::now();
1805 short_status_borrowed_tracked_only_head_matches_index_count_parallel(
1806 worktree_root,
1807 git_dir,
1808 format,
1809 &borrowed,
1810 &stat_cache,
1811 sparse_checkout_active,
1812 untracked_mode,
1813 )
1814 .map(|count| (count, start.elapsed().as_micros()))
1815 })?;
1816 let untracked = executor.spawn(
1817 scope,
1818 "status-untracked",
1819 || -> Result<(usize, StatusProfileCounters)> {
1820 let mut local_profile = StatusProfileCounters::default();
1821 let mut ignores = IgnoreMatcher::from_worktree_base(worktree_root)?;
1822 let start = Instant::now();
1823 let count = status_untracked_count_from_borrowed_index(
1824 worktree_root,
1825 git_dir,
1826 &borrowed,
1827 &mut ignores,
1828 untracked_mode,
1829 profile_enabled.then_some(&mut local_profile),
1830 )?;
1831 local_profile.untracked_elapsed_us = start.elapsed().as_micros();
1832 local_profile.untracked_rows = count as u64;
1833 Ok((count, local_profile))
1834 },
1835 )?;
1836 let (tracked_count, tracked_elapsed_us) = tracked.join()?;
1837 let (untracked_count, untracked_profile) = untracked.join()?;
1838 if let Some(profile) = profile.as_mut() {
1839 profile.tracked_elapsed_us = tracked_elapsed_us;
1840 }
1841 Ok((
1842 tracked_count,
1843 untracked_count,
1844 profile_enabled.then_some(untracked_profile),
1845 ))
1846 })?;
1847 if let Some(profile) = profile.as_mut() {
1848 if let Some(untracked_profile) = untracked_profile {
1849 profile.merge_untracked(untracked_profile);
1850 }
1851 profile.emit();
1852 }
1853 Ok(Some(tracked_count + untracked_count))
1854}
1855
1856pub(crate) fn emit_untracked_status_entry<'a, F>(
1857 emit: &'a mut F,
1858) -> impl FnMut(&[u8]) -> Result<StreamControl> + 'a
1859where
1860 F: for<'row> FnMut(ShortStatusRow<'row>) -> Result<StreamControl>,
1861{
1862 |path| emit(untracked_status_row(path))
1863}
1864
1865pub(crate) fn untracked_status_entry(path: Vec<u8>) -> ShortStatusEntry {
1866 ShortStatusEntry {
1867 index: b'?',
1868 worktree: b'?',
1869 path,
1870 head_mode: None,
1871 index_mode: None,
1872 worktree_mode: None,
1873 head_oid: None,
1874 index_oid: None,
1875 submodule: None,
1876 }
1877}
1878
1879pub(crate) fn untracked_status_row(path: &[u8]) -> ShortStatusRow<'_> {
1880 ShortStatusRow {
1881 index: b'?',
1882 worktree: b'?',
1883 path,
1884 head_mode: None,
1885 index_mode: None,
1886 worktree_mode: None,
1887 head_oid: None,
1888 index_oid: None,
1889 submodule: None,
1890 }
1891}
1892
1893pub(crate) fn append_untracked_status_entries(
1894 entries: &mut Vec<ShortStatusEntry>,
1895 untracked_paths: Vec<Vec<u8>>,
1896) {
1897 for path in untracked_paths {
1898 entries.push(untracked_status_entry(path));
1899 }
1900}
1901
1902#[derive(Debug, Clone, Copy)]
1903pub(crate) enum TrackedOnlyPrecheck {
1904 Deleted(usize),
1905 Slow(usize),
1906}
1907
1908#[derive(Debug)]
1909pub(crate) enum TrackedOnlyPrecheckOutcome {
1910 Clean,
1911 Deleted,
1912 Slow,
1913}
1914
1915pub(crate) fn short_status_tracked_only_head_matches_index_parallel(
1916 worktree_root: &Path,
1917 git_dir: &Path,
1918 format: ObjectFormat,
1919 index: &Index,
1920 stat_cache: &IndexStatCache,
1921 sparse_checkout_active: bool,
1922 untracked_mode: StatusUntrackedMode,
1923) -> Result<Vec<ShortStatusEntry>> {
1924 let prechecks = tracked_only_non_clean_prechecks_parallel(
1925 worktree_root,
1926 index,
1927 stat_cache,
1928 sparse_checkout_active,
1929 )?;
1930
1931 let mut clean_filter = None;
1932 let mut entries = Vec::new();
1933 for precheck in prechecks {
1934 match precheck {
1935 TrackedOnlyPrecheck::Deleted(idx) => {
1936 let entry = &index.entries[idx];
1937 if entry.is_intent_to_add() {
1938 continue;
1939 }
1940 let path = entry.path.as_bytes();
1941 entries.push(ShortStatusEntry {
1942 index: b' ',
1943 worktree: b'D',
1944 path: path.to_vec(),
1945 head_mode: Some(entry.mode),
1946 index_mode: Some(entry.mode),
1947 worktree_mode: None,
1948 head_oid: Some(entry.oid),
1949 index_oid: Some(entry.oid),
1950 submodule: None,
1951 });
1952 }
1953 TrackedOnlyPrecheck::Slow(idx) => {
1954 let entry = &index.entries[idx];
1955 let path = entry.path.as_bytes();
1956 let index_entry = TrackedEntry {
1957 mode: entry.mode,
1958 oid: entry.oid,
1959 };
1960 let worktree_entry = worktree_entry_for_index_entry_with_attributes(
1961 worktree_root,
1962 git_dir,
1963 format,
1964 entry,
1965 stat_cache,
1966 &mut clean_filter,
1967 )?;
1968 let submodule = tracked_only_submodule_status(
1969 worktree_root,
1970 path,
1971 &index_entry,
1972 worktree_entry.as_ref(),
1973 untracked_mode,
1974 )?;
1975 let worktree_code = match worktree_entry.as_ref() {
1976 None if entry.is_intent_to_add() => b' ',
1977 None => b'D',
1978 Some(_) if entry.is_intent_to_add() => b'A',
1979 Some(worktree_entry) if *worktree_entry != index_entry => {
1980 status_change_code(index_entry.mode, worktree_entry.mode)
1981 }
1982 _ if submodule.is_some_and(|sub| sub.any()) => b'M',
1983 _ => b' ',
1984 };
1985 if worktree_code != b' ' {
1986 entries.push(ShortStatusEntry {
1987 index: b' ',
1988 worktree: worktree_code,
1989 path: path.to_vec(),
1990 head_mode: (!entry.is_intent_to_add()).then_some(index_entry.mode),
1991 index_mode: (!entry.is_intent_to_add()).then_some(index_entry.mode),
1992 worktree_mode: worktree_entry.as_ref().map(|entry| entry.mode),
1993 head_oid: (!entry.is_intent_to_add()).then_some(index_entry.oid),
1994 index_oid: (!entry.is_intent_to_add()).then_some(index_entry.oid),
1995 submodule: submodule.filter(|sub| sub.any()),
1996 });
1997 }
1998 }
1999 }
2000 }
2001 entries.sort_by(|left, right| {
2002 status_sort_category(left)
2003 .cmp(&status_sort_category(right))
2004 .then_with(|| left.path.cmp(&right.path))
2005 });
2006 Ok(entries)
2007}
2008
2009pub(crate) fn short_status_borrowed_tracked_only_head_matches_index_parallel(
2010 worktree_root: &Path,
2011 git_dir: &Path,
2012 format: ObjectFormat,
2013 index: &BorrowedIndex<'_>,
2014 stat_cache: &IndexStatCache,
2015 sparse_checkout_active: bool,
2016 untracked_mode: StatusUntrackedMode,
2017) -> Result<Vec<ShortStatusEntry>> {
2018 let prechecks = tracked_only_borrowed_non_clean_prechecks_parallel(
2019 worktree_root,
2020 index,
2021 stat_cache,
2022 sparse_checkout_active,
2023 )?;
2024
2025 let mut clean_filter = None;
2026 let mut entries = Vec::new();
2027 for precheck in prechecks {
2028 match precheck {
2029 TrackedOnlyPrecheck::Deleted(idx) => {
2030 let entry = &index.entries[idx];
2031 if entry.is_intent_to_add() {
2032 continue;
2033 }
2034 entries.push(ShortStatusEntry {
2035 index: b' ',
2036 worktree: b'D',
2037 path: entry.path.to_vec(),
2038 head_mode: Some(entry.mode),
2039 index_mode: Some(entry.mode),
2040 worktree_mode: None,
2041 head_oid: Some(entry.oid),
2042 index_oid: Some(entry.oid),
2043 submodule: None,
2044 });
2045 }
2046 TrackedOnlyPrecheck::Slow(idx) => {
2047 let entry = &index.entries[idx];
2048 let index_entry = TrackedEntry {
2049 mode: entry.mode,
2050 oid: entry.oid,
2051 };
2052 let worktree_entry = worktree_entry_for_index_entry_ref_with_attributes(
2053 worktree_root,
2054 git_dir,
2055 format,
2056 entry,
2057 stat_cache,
2058 &mut clean_filter,
2059 )?;
2060 let submodule = tracked_only_submodule_status(
2061 worktree_root,
2062 entry.path,
2063 &index_entry,
2064 worktree_entry.as_ref(),
2065 untracked_mode,
2066 )?;
2067 let worktree_code = match worktree_entry.as_ref() {
2068 None if entry.is_intent_to_add() => b' ',
2069 None => b'D',
2070 Some(_) if entry.is_intent_to_add() => b'A',
2071 Some(worktree_entry) if *worktree_entry != index_entry => {
2072 status_change_code(index_entry.mode, worktree_entry.mode)
2073 }
2074 _ if submodule.is_some_and(|sub| sub.any()) => b'M',
2075 _ => b' ',
2076 };
2077 if worktree_code != b' ' {
2078 entries.push(ShortStatusEntry {
2079 index: b' ',
2080 worktree: worktree_code,
2081 path: entry.path.to_vec(),
2082 head_mode: (!entry.is_intent_to_add()).then_some(index_entry.mode),
2083 index_mode: (!entry.is_intent_to_add()).then_some(index_entry.mode),
2084 worktree_mode: worktree_entry.as_ref().map(|entry| entry.mode),
2085 head_oid: (!entry.is_intent_to_add()).then_some(index_entry.oid),
2086 index_oid: (!entry.is_intent_to_add()).then_some(index_entry.oid),
2087 submodule: submodule.filter(|sub| sub.any()),
2088 });
2089 }
2090 }
2091 }
2092 }
2093 entries.sort_by(|left, right| {
2094 status_sort_category(left)
2095 .cmp(&status_sort_category(right))
2096 .then_with(|| left.path.cmp(&right.path))
2097 });
2098 Ok(entries)
2099}
2100
2101pub(crate) fn stream_short_status_borrowed_tracked_only_head_matches_index_parallel<F>(
2102 worktree_root: &Path,
2103 git_dir: &Path,
2104 format: ObjectFormat,
2105 index: &BorrowedIndex<'_>,
2106 stat_cache: &IndexStatCache,
2107 sparse_checkout_active: bool,
2108 untracked_mode: StatusUntrackedMode,
2109 emit: &mut F,
2110) -> Result<StreamControl>
2111where
2112 F: for<'a> FnMut(ShortStatusRow<'a>) -> Result<StreamControl>,
2113{
2114 let prechecks = tracked_only_borrowed_non_clean_prechecks_parallel(
2115 worktree_root,
2116 index,
2117 stat_cache,
2118 sparse_checkout_active,
2119 )?;
2120
2121 let mut clean_filter = None;
2122 for precheck in prechecks {
2123 match precheck {
2124 TrackedOnlyPrecheck::Deleted(idx) => {
2125 let entry = &index.entries[idx];
2126 if entry.is_intent_to_add() {
2127 continue;
2128 }
2129 if emit(ShortStatusRow {
2130 index: b' ',
2131 worktree: b'D',
2132 path: entry.path,
2133 head_mode: Some(entry.mode),
2134 index_mode: Some(entry.mode),
2135 worktree_mode: None,
2136 head_oid: Some(entry.oid),
2137 index_oid: Some(entry.oid),
2138 submodule: None,
2139 })?
2140 .is_stop()
2141 {
2142 return Ok(StreamControl::Stop);
2143 }
2144 }
2145 TrackedOnlyPrecheck::Slow(idx) => {
2146 let entry = &index.entries[idx];
2147 let index_entry = TrackedEntry {
2148 mode: entry.mode,
2149 oid: entry.oid,
2150 };
2151 let worktree_entry = worktree_entry_for_index_entry_ref_with_attributes(
2152 worktree_root,
2153 git_dir,
2154 format,
2155 entry,
2156 stat_cache,
2157 &mut clean_filter,
2158 )?;
2159 let submodule = tracked_only_submodule_status(
2160 worktree_root,
2161 entry.path,
2162 &index_entry,
2163 worktree_entry.as_ref(),
2164 untracked_mode,
2165 )?;
2166 let worktree_code = match worktree_entry.as_ref() {
2167 None if entry.is_intent_to_add() => b' ',
2168 None => b'D',
2169 Some(_) if entry.is_intent_to_add() => b'A',
2170 Some(worktree_entry) if *worktree_entry != index_entry => {
2171 status_change_code(index_entry.mode, worktree_entry.mode)
2172 }
2173 _ if submodule.is_some_and(|sub| sub.any()) => b'M',
2174 _ => b' ',
2175 };
2176 if worktree_code != b' ' {
2177 if emit(ShortStatusRow {
2178 index: b' ',
2179 worktree: worktree_code,
2180 path: entry.path,
2181 head_mode: (!entry.is_intent_to_add()).then_some(index_entry.mode),
2182 index_mode: (!entry.is_intent_to_add()).then_some(index_entry.mode),
2183 worktree_mode: worktree_entry.as_ref().map(|entry| entry.mode),
2184 head_oid: (!entry.is_intent_to_add()).then_some(index_entry.oid),
2185 index_oid: (!entry.is_intent_to_add()).then_some(index_entry.oid),
2186 submodule: submodule.filter(|sub| sub.any()),
2187 })?
2188 .is_stop()
2189 {
2190 return Ok(StreamControl::Stop);
2191 }
2192 }
2193 }
2194 }
2195 }
2196 Ok(StreamControl::Continue)
2197}
2198
2199pub(crate) fn short_status_borrowed_tracked_only_head_matches_index_count_parallel(
2200 worktree_root: &Path,
2201 git_dir: &Path,
2202 format: ObjectFormat,
2203 index: &BorrowedIndex<'_>,
2204 stat_cache: &IndexStatCache,
2205 sparse_checkout_active: bool,
2206 untracked_mode: StatusUntrackedMode,
2207) -> Result<usize> {
2208 let prechecks = tracked_only_borrowed_non_clean_prechecks_parallel(
2209 worktree_root,
2210 index,
2211 stat_cache,
2212 sparse_checkout_active,
2213 )?;
2214
2215 let mut clean_filter = None;
2216 let mut count = 0usize;
2217 for precheck in prechecks {
2218 match precheck {
2219 TrackedOnlyPrecheck::Deleted(_) => count += 1,
2220 TrackedOnlyPrecheck::Slow(idx) => {
2221 let entry = &index.entries[idx];
2222 let index_entry = TrackedEntry {
2223 mode: entry.mode,
2224 oid: entry.oid,
2225 };
2226 let worktree_entry = worktree_entry_for_index_entry_ref_with_attributes(
2227 worktree_root,
2228 git_dir,
2229 format,
2230 entry,
2231 stat_cache,
2232 &mut clean_filter,
2233 )?;
2234 let submodule = tracked_only_submodule_status(
2235 worktree_root,
2236 entry.path,
2237 &index_entry,
2238 worktree_entry.as_ref(),
2239 untracked_mode,
2240 )?;
2241 let worktree_code = match worktree_entry.as_ref() {
2242 None => b'D',
2243 Some(worktree_entry) if *worktree_entry != index_entry => {
2244 status_change_code(index_entry.mode, worktree_entry.mode)
2245 }
2246 _ if submodule.is_some_and(|sub| sub.any()) => b'M',
2247 _ => b' ',
2248 };
2249 if worktree_code != b' ' {
2250 count += 1;
2251 }
2252 }
2253 }
2254 }
2255 Ok(count)
2256}
2257
2258pub(crate) fn short_status_tracked_only_with_head_parallel(
2259 worktree_root: &Path,
2260 git_dir: &Path,
2261 format: ObjectFormat,
2262 index: &Index,
2263 stat_cache: &IndexStatCache,
2264 head: &BTreeMap<Vec<u8>, TrackedEntry>,
2265 sparse_checkout_active: bool,
2266 untracked_mode: StatusUntrackedMode,
2267) -> Result<Vec<ShortStatusEntry>> {
2268 let prechecks = tracked_only_non_clean_prechecks_parallel(
2269 worktree_root,
2270 index,
2271 stat_cache,
2272 sparse_checkout_active,
2273 )?;
2274 let mut precheck_cursor = 0usize;
2275 let mut clean_filter = None;
2276 let mut entries = Vec::new();
2277
2278 for (idx, entry) in index.entries.iter().enumerate() {
2279 if entry.stage() != Stage::Normal {
2280 continue;
2281 }
2282 let path = entry.path.as_bytes();
2283 let index_entry = TrackedEntry {
2284 mode: entry.mode,
2285 oid: entry.oid,
2286 };
2287 let head_entry = head.get(path);
2288 let visible_index_entry = (!entry.is_intent_to_add()).then_some(&index_entry);
2289 let index_code = match (head_entry, visible_index_entry) {
2290 (None, Some(_)) => b'A',
2291 (Some(_), None) => b'D',
2292 (Some(head_entry), Some(index_entry)) if *head_entry != *index_entry => {
2293 status_change_code(head_entry.mode, index_entry.mode)
2294 }
2295 _ => b' ',
2296 };
2297 let precheck = prechecks
2298 .get(precheck_cursor)
2299 .copied()
2300 .and_then(|precheck| {
2301 if tracked_only_precheck_index(precheck) == idx {
2302 precheck_cursor += 1;
2303 Some(precheck)
2304 } else {
2305 None
2306 }
2307 });
2308 let (worktree_code, worktree_mode, submodule) = match precheck {
2309 None if entry.is_intent_to_add() => (b' ', None, None),
2310 None => (b' ', Some(index_entry.mode), None),
2311 Some(TrackedOnlyPrecheck::Deleted(_)) if entry.is_intent_to_add() => (b' ', None, None),
2312 Some(TrackedOnlyPrecheck::Deleted(_)) => (b'D', None, None),
2313 Some(TrackedOnlyPrecheck::Slow(_)) => {
2314 let worktree_entry = worktree_entry_for_index_entry_with_attributes(
2315 worktree_root,
2316 git_dir,
2317 format,
2318 entry,
2319 stat_cache,
2320 &mut clean_filter,
2321 )?;
2322 let submodule = tracked_only_submodule_status(
2323 worktree_root,
2324 path,
2325 &index_entry,
2326 worktree_entry.as_ref(),
2327 untracked_mode,
2328 )?;
2329 let worktree_code = match worktree_entry.as_ref() {
2330 None if entry.is_intent_to_add() => b' ',
2331 None => b'D',
2332 Some(_) if entry.is_intent_to_add() => b'A',
2333 Some(worktree_entry) if *worktree_entry != index_entry => {
2334 status_change_code(index_entry.mode, worktree_entry.mode)
2335 }
2336 _ if submodule.is_some_and(|sub| sub.any()) => b'M',
2337 _ => b' ',
2338 };
2339 (
2340 worktree_code,
2341 worktree_entry.as_ref().map(|entry| entry.mode),
2342 submodule.filter(|sub| sub.any()),
2343 )
2344 }
2345 };
2346 if index_code != b' ' || worktree_code != b' ' {
2347 entries.push(ShortStatusEntry {
2348 index: index_code,
2349 worktree: worktree_code,
2350 path: path.to_vec(),
2351 head_mode: head_entry.map(|entry| entry.mode),
2352 index_mode: visible_index_entry.map(|entry| entry.mode),
2353 worktree_mode,
2354 head_oid: head_entry.map(|entry| entry.oid),
2355 index_oid: visible_index_entry.map(|entry| entry.oid),
2356 submodule,
2357 });
2358 }
2359 }
2360
2361 let index_paths = index
2362 .entries
2363 .iter()
2364 .filter(|entry| entry.stage() == Stage::Normal)
2365 .map(|entry| entry.path.as_bytes().to_vec())
2366 .collect::<HashSet<_>>();
2367 for (path, head_entry) in head {
2368 if index_paths.contains(path.as_slice()) {
2369 continue;
2370 }
2371 entries.push(ShortStatusEntry {
2372 index: b'D',
2373 worktree: b' ',
2374 path: path.clone(),
2375 head_mode: Some(head_entry.mode),
2376 index_mode: None,
2377 worktree_mode: None,
2378 head_oid: Some(head_entry.oid),
2379 index_oid: None,
2380 submodule: None,
2381 });
2382 }
2383 entries.sort_by(|left, right| {
2384 status_sort_category(left)
2385 .cmp(&status_sort_category(right))
2386 .then_with(|| left.path.cmp(&right.path))
2387 });
2388 Ok(entries)
2389}
2390
2391pub(crate) fn tracked_only_precheck_index(precheck: TrackedOnlyPrecheck) -> usize {
2392 match precheck {
2393 TrackedOnlyPrecheck::Deleted(idx) | TrackedOnlyPrecheck::Slow(idx) => idx,
2394 }
2395}
2396
2397pub(crate) fn stage0_index_entry_count<E>(
2398 entries: &[E],
2399 mut stage: impl FnMut(&E) -> Stage,
2400) -> usize {
2401 entries
2402 .iter()
2403 .filter(|entry| stage(entry) == Stage::Normal)
2404 .count()
2405}
2406
2407pub(crate) fn stage0_index_chunk_ranges<E>(
2408 entries: &[E],
2409 chunk_size: usize,
2410 mut stage: impl FnMut(&E) -> Stage,
2411) -> Vec<std::ops::Range<usize>> {
2412 debug_assert!(chunk_size > 0);
2413 let mut ranges = Vec::new();
2414 let mut start = None;
2415 let mut end = 0usize;
2416 let mut normals_in_chunk = 0usize;
2417 for (idx, entry) in entries.iter().enumerate() {
2418 if stage(entry) != Stage::Normal {
2419 continue;
2420 }
2421 if start.is_none() {
2422 start = Some(idx);
2423 }
2424 end = idx + 1;
2425 normals_in_chunk += 1;
2426 if normals_in_chunk == chunk_size {
2427 if let Some(start) = start {
2428 ranges.push(start..end);
2429 }
2430 start = None;
2431 normals_in_chunk = 0;
2432 }
2433 }
2434 if let Some(start) = start {
2435 ranges.push(start..end);
2436 }
2437 ranges
2438}
2439
2440pub(crate) fn tracked_only_non_clean_prechecks_parallel(
2441 worktree_root: &Path,
2442 index: &Index,
2443 stat_cache: &IndexStatCache,
2444 sparse_checkout_active: bool,
2445) -> Result<Vec<TrackedOnlyPrecheck>> {
2446 let normal_count = stage0_index_entry_count(&index.entries, IndexEntry::stage);
2447 if normal_count == 0 {
2448 return Ok(Vec::new());
2449 }
2450 let executor = StatusExecutor::new();
2451 let worker_count = executor.worker_count_for(normal_count, 512, 4);
2452 if worker_count == 1 {
2453 let mut prechecks = Vec::new();
2454 let mut absolute = PathBuf::new();
2455 for (idx, entry) in index.entries.iter().enumerate() {
2456 if entry.stage() != Stage::Normal {
2457 continue;
2458 }
2459 match tracked_only_stat_precheck(
2460 worktree_root,
2461 entry,
2462 stat_cache,
2463 sparse_checkout_active,
2464 &mut absolute,
2465 )? {
2466 TrackedOnlyPrecheckOutcome::Clean => {}
2467 TrackedOnlyPrecheckOutcome::Deleted => {
2468 prechecks.push(TrackedOnlyPrecheck::Deleted(idx));
2469 }
2470 TrackedOnlyPrecheckOutcome::Slow => {
2471 prechecks.push(TrackedOnlyPrecheck::Slow(idx));
2472 }
2473 }
2474 }
2475 return Ok(prechecks);
2476 }
2477 let chunk_size = normal_count.div_ceil(worker_count);
2478 let chunk_ranges = stage0_index_chunk_ranges(&index.entries, chunk_size, IndexEntry::stage);
2479 let next_chunk = AtomicUsize::new(0);
2480 let mut prechecks = std::thread::scope(|scope| -> Result<Vec<TrackedOnlyPrecheck>> {
2481 let mut handles = Vec::new();
2482 for _ in 0..worker_count {
2483 let chunk_ranges = &chunk_ranges;
2484 let next_chunk = &next_chunk;
2485 handles.push(executor.spawn(
2486 scope,
2487 "status-precheck",
2488 move || -> Result<Vec<TrackedOnlyPrecheck>> {
2489 let mut prechecks = Vec::new();
2490 let mut absolute = PathBuf::new();
2491 loop {
2492 let chunk_idx = next_chunk.fetch_add(1, Ordering::Relaxed);
2493 let Some(range) = chunk_ranges.get(chunk_idx) else {
2494 break;
2495 };
2496 for idx in range.clone() {
2497 let entry = &index.entries[idx];
2498 if entry.stage() != Stage::Normal {
2499 continue;
2500 }
2501 match tracked_only_stat_precheck(
2502 worktree_root,
2503 entry,
2504 stat_cache,
2505 sparse_checkout_active,
2506 &mut absolute,
2507 )? {
2508 TrackedOnlyPrecheckOutcome::Clean => {}
2509 TrackedOnlyPrecheckOutcome::Deleted => {
2510 prechecks.push(TrackedOnlyPrecheck::Deleted(idx));
2511 }
2512 TrackedOnlyPrecheckOutcome::Slow => {
2513 prechecks.push(TrackedOnlyPrecheck::Slow(idx));
2514 }
2515 }
2516 }
2517 }
2518 Ok(prechecks)
2519 },
2520 )?);
2521 }
2522 let mut prechecks = Vec::new();
2523 for handle in handles {
2524 let mut chunk = handle.join()?;
2525 prechecks.append(&mut chunk);
2526 }
2527 Ok(prechecks)
2528 })?;
2529 prechecks.sort_by_key(|precheck| tracked_only_precheck_index(*precheck));
2530 Ok(prechecks)
2531}
2532
2533pub(crate) fn tracked_only_borrowed_non_clean_prechecks_parallel(
2534 worktree_root: &Path,
2535 index: &BorrowedIndex<'_>,
2536 stat_cache: &IndexStatCache,
2537 sparse_checkout_active: bool,
2538) -> Result<Vec<TrackedOnlyPrecheck>> {
2539 let normal_count = stage0_index_entry_count(&index.entries, IndexEntryRef::stage);
2540 if normal_count == 0 {
2541 return Ok(Vec::new());
2542 }
2543 let executor = StatusExecutor::new();
2544 let worker_count = executor.worker_count_for(normal_count, 512, 4);
2545 if worker_count == 1 {
2546 let mut prechecks = Vec::new();
2547 let mut absolute = PathBuf::new();
2548 for (idx, entry) in index.entries.iter().enumerate() {
2549 if entry.stage() != Stage::Normal {
2550 continue;
2551 }
2552 match tracked_only_borrowed_stat_precheck(
2553 worktree_root,
2554 entry,
2555 stat_cache,
2556 sparse_checkout_active,
2557 &mut absolute,
2558 )? {
2559 TrackedOnlyPrecheckOutcome::Clean => {}
2560 TrackedOnlyPrecheckOutcome::Deleted => {
2561 prechecks.push(TrackedOnlyPrecheck::Deleted(idx));
2562 }
2563 TrackedOnlyPrecheckOutcome::Slow => {
2564 prechecks.push(TrackedOnlyPrecheck::Slow(idx));
2565 }
2566 }
2567 }
2568 return Ok(prechecks);
2569 }
2570 let chunk_size = normal_count.div_ceil(worker_count);
2571 let chunk_ranges = stage0_index_chunk_ranges(&index.entries, chunk_size, IndexEntryRef::stage);
2572 let next_chunk = AtomicUsize::new(0);
2573 let mut prechecks = std::thread::scope(|scope| -> Result<Vec<TrackedOnlyPrecheck>> {
2574 let mut handles = Vec::new();
2575 for _ in 0..worker_count {
2576 let chunk_ranges = &chunk_ranges;
2577 let next_chunk = &next_chunk;
2578 handles.push(executor.spawn(
2579 scope,
2580 "status-precheck",
2581 move || -> Result<Vec<TrackedOnlyPrecheck>> {
2582 let mut prechecks = Vec::new();
2583 let mut absolute = PathBuf::new();
2584 loop {
2585 let chunk_idx = next_chunk.fetch_add(1, Ordering::Relaxed);
2586 let Some(range) = chunk_ranges.get(chunk_idx) else {
2587 break;
2588 };
2589 for idx in range.clone() {
2590 let entry = &index.entries[idx];
2591 if entry.stage() != Stage::Normal {
2592 continue;
2593 }
2594 match tracked_only_borrowed_stat_precheck(
2595 worktree_root,
2596 entry,
2597 stat_cache,
2598 sparse_checkout_active,
2599 &mut absolute,
2600 )? {
2601 TrackedOnlyPrecheckOutcome::Clean => {}
2602 TrackedOnlyPrecheckOutcome::Deleted => {
2603 prechecks.push(TrackedOnlyPrecheck::Deleted(idx));
2604 }
2605 TrackedOnlyPrecheckOutcome::Slow => {
2606 prechecks.push(TrackedOnlyPrecheck::Slow(idx));
2607 }
2608 }
2609 }
2610 }
2611 Ok(prechecks)
2612 },
2613 )?);
2614 }
2615 let mut prechecks = Vec::new();
2616 for handle in handles {
2617 let mut chunk = handle.join()?;
2618 prechecks.append(&mut chunk);
2619 }
2620 Ok(prechecks)
2621 })?;
2622 prechecks.sort_by_key(|precheck| tracked_only_precheck_index(*precheck));
2623 Ok(prechecks)
2624}
2625
2626pub(crate) fn tracked_only_stat_precheck(
2627 worktree_root: &Path,
2628 index_entry: &IndexEntry,
2629 stat_cache: &IndexStatCache,
2630 sparse_checkout_active: bool,
2631 absolute: &mut PathBuf,
2632) -> Result<TrackedOnlyPrecheckOutcome> {
2633 if index_entry.is_skip_worktree() && !sparse_checkout_active {
2640 return Ok(TrackedOnlyPrecheckOutcome::Clean);
2641 }
2642 if sley_index::is_gitlink(index_entry.mode) {
2643 return Ok(TrackedOnlyPrecheckOutcome::Slow);
2644 }
2645 let git_path = index_entry.path.as_bytes();
2646 if crate::index_io::git_path_has_symlink_parent(worktree_root, git_path)? {
2647 return Ok(TrackedOnlyPrecheckOutcome::Deleted);
2648 }
2649 set_worktree_path_from_repo_path(worktree_root, git_path, absolute)?;
2650 let metadata = match fs::symlink_metadata(&absolute) {
2651 Ok(metadata) => metadata,
2652 Err(err)
2653 if matches!(
2654 err.kind(),
2655 std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
2656 ) =>
2657 {
2658 if index_entry.is_skip_worktree() {
2660 return Ok(TrackedOnlyPrecheckOutcome::Clean);
2661 }
2662 return Ok(TrackedOnlyPrecheckOutcome::Deleted);
2663 }
2664 Err(err) => return Err(err.into()),
2665 };
2666 let file_type = metadata.file_type();
2667 if file_type.is_dir() || !(file_type.is_file() || file_type.is_symlink()) {
2668 return Ok(TrackedOnlyPrecheckOutcome::Slow);
2669 }
2670 if stat_cache
2671 .reuse_index_entry(index_entry, &metadata)
2672 .is_some()
2673 {
2674 Ok(TrackedOnlyPrecheckOutcome::Clean)
2675 } else {
2676 Ok(TrackedOnlyPrecheckOutcome::Slow)
2677 }
2678}
2679
2680pub(crate) fn tracked_only_borrowed_stat_precheck(
2681 worktree_root: &Path,
2682 index_entry: &IndexEntryRef<'_>,
2683 stat_cache: &IndexStatCache,
2684 sparse_checkout_active: bool,
2685 absolute: &mut PathBuf,
2686) -> Result<TrackedOnlyPrecheckOutcome> {
2687 if index_entry.is_skip_worktree() && !sparse_checkout_active {
2692 return Ok(TrackedOnlyPrecheckOutcome::Clean);
2693 }
2694 if sley_index::is_gitlink(index_entry.mode) {
2695 return Ok(TrackedOnlyPrecheckOutcome::Slow);
2696 }
2697 if crate::index_io::git_path_has_symlink_parent(worktree_root, index_entry.path)? {
2698 return Ok(TrackedOnlyPrecheckOutcome::Deleted);
2699 }
2700 set_worktree_path_from_repo_path(worktree_root, index_entry.path, absolute)?;
2701 let metadata = match fs::symlink_metadata(&absolute) {
2702 Ok(metadata) => metadata,
2703 Err(err)
2704 if matches!(
2705 err.kind(),
2706 std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
2707 ) =>
2708 {
2709 if index_entry.is_skip_worktree() {
2711 return Ok(TrackedOnlyPrecheckOutcome::Clean);
2712 }
2713 return Ok(TrackedOnlyPrecheckOutcome::Deleted);
2714 }
2715 Err(err) => return Err(err.into()),
2716 };
2717 let file_type = metadata.file_type();
2718 if file_type.is_dir() || !(file_type.is_file() || file_type.is_symlink()) {
2719 return Ok(TrackedOnlyPrecheckOutcome::Slow);
2720 }
2721 if stat_cache
2722 .reuse_index_entry_ref(index_entry, &metadata)
2723 .is_some()
2724 {
2725 Ok(TrackedOnlyPrecheckOutcome::Clean)
2726 } else {
2727 Ok(TrackedOnlyPrecheckOutcome::Slow)
2728 }
2729}
2730
2731pub(crate) fn set_worktree_path_from_repo_path(
2732 worktree_root: &Path,
2733 git_path: &[u8],
2734 out: &mut PathBuf,
2735) -> Result<()> {
2736 out.clear();
2737 out.push(worktree_root);
2738 push_repo_path(out, git_path)
2739}
2740
2741#[cfg(unix)]
2742pub(crate) fn push_repo_path(out: &mut PathBuf, path: &[u8]) -> Result<()> {
2743 use std::os::unix::ffi::OsStrExt;
2744
2745 out.push(Path::new(std::ffi::OsStr::from_bytes(path)));
2746 Ok(())
2747}
2748
2749#[cfg(not(unix))]
2750pub(crate) fn push_repo_path(out: &mut PathBuf, path: &[u8]) -> Result<()> {
2751 let path = std::str::from_utf8(path)
2752 .map_err(|_| GitError::InvalidPath("index path is not utf8".into()))?;
2753 for component in path.split('/') {
2754 out.push(component);
2755 }
2756 Ok(())
2757}
2758
2759pub(crate) fn tracked_only_submodule_status(
2760 worktree_root: &Path,
2761 path: &[u8],
2762 index_entry: &TrackedEntry,
2763 worktree_entry: Option<&TrackedEntry>,
2764 _untracked_mode: StatusUntrackedMode,
2765) -> Result<Option<SubmoduleStatus>> {
2766 let Some(worktree_entry) = worktree_entry else {
2767 return Ok(None);
2768 };
2769 if !sley_index::is_gitlink(index_entry.mode) || !sley_index::is_gitlink(worktree_entry.mode) {
2770 return Ok(None);
2771 }
2772 let absolute = worktree_root.join(repo_path_to_os_path(path)?);
2773 let dirt = if absolute.is_dir() {
2774 submodule_dirt_checked(&absolute)?
2775 } else {
2776 0
2777 };
2778 Ok(Some(SubmoduleStatus {
2779 new_commits: index_entry.oid != worktree_entry.oid,
2780 modified_content: dirt & DIRTY_SUBMODULE_MODIFIED != 0,
2781 untracked_content: dirt & DIRTY_SUBMODULE_UNTRACKED != 0,
2782 }))
2783}
2784
2785pub(crate) fn status_sort_category(entry: &ShortStatusEntry) -> u8 {
2786 match (entry.index, entry.worktree) {
2787 (b'?', b'?') => 1,
2788 (b'!', b'!') => 2,
2789 _ => 0,
2790 }
2791}