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 = 1024;
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::<fs::DirEntry>(),
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.untracked_mode, StatusUntrackedMode::All);
573 let ignored_paths = ignored_untracked_paths(
574 worktree_root,
575 git_dir,
576 &index,
577 &ignores,
578 ignored_directory_rows,
579 )?;
580 let ignored_paths: Vec<Vec<u8>> = match options.ignored_mode {
581 StatusIgnoredMode::Matching => ignored_paths,
582 StatusIgnoredMode::Traditional
583 if matches!(options.untracked_mode, StatusUntrackedMode::All) =>
584 {
585 ignored_paths
586 }
587 StatusIgnoredMode::Traditional => {
588 let mut rolled = BTreeSet::new();
589 for path in ignored_paths {
590 let path = ignored_traditional_rollup_path(
591 worktree_root,
592 git_dir,
593 &path,
594 &index,
595 &ignores,
596 )?;
597 if ignored_traditional_path_is_empty_directory(worktree_root, &path)? {
598 continue;
599 }
600 rolled.insert(path);
601 }
602 rolled.into_iter().collect()
603 }
604 };
605 for path in ignored_paths {
606 entries.push(ShortStatusEntry {
607 index: b'!',
608 worktree: b'!',
609 path,
610 head_mode: None,
611 index_mode: None,
612 worktree_mode: None,
613 head_oid: None,
614 index_oid: None,
615 submodule: None,
616 });
617 }
618 }
619 let untracked_paths: Vec<Vec<u8>> = match options.untracked_mode {
620 StatusUntrackedMode::All => worktree
621 .iter()
622 .filter_map(|(path, entry)| {
623 let is_directory = entry.mode == 0o040000 && entry.oid.is_null();
624 if index.contains_key(path)
625 || path_or_parent_is_ignored(&ignores, path, is_directory)
626 {
627 return None;
628 }
629 if is_directory {
630 let mut directory = path.clone();
631 directory.push(b'/');
632 Some(directory)
633 } else {
634 Some(path.clone())
635 }
636 })
637 .collect(),
638 StatusUntrackedMode::Normal => {
639 normal_untracked_paths_from_worktree(&worktree, &index, &ignores)
640 }
641 StatusUntrackedMode::None => Vec::new(),
642 };
643 for path in untracked_paths {
644 entries.push(ShortStatusEntry {
645 index: b'?',
646 worktree: b'?',
647 path,
648 head_mode: None,
649 index_mode: None,
650 worktree_mode: None,
651 head_oid: None,
652 index_oid: None,
653 submodule: None,
654 });
655 }
656 entries.sort_by(|left, right| {
657 status_sort_category(left)
658 .cmp(&status_sort_category(right))
659 .then_with(|| left.path.cmp(&right.path))
660 });
661 Ok(entries)
662}
663
664pub(crate) fn short_status_unmerged_entries(index: &Index) -> Vec<ShortStatusEntry> {
665 let mut by_path: BTreeMap<Vec<u8>, BTreeSet<u16>> = BTreeMap::new();
666 for entry in &index.entries {
667 let stage = entry.stage().as_u16();
668 if stage > 0 {
669 by_path
670 .entry(entry.path.as_bytes().to_vec())
671 .or_default()
672 .insert(stage);
673 }
674 }
675 by_path
676 .into_iter()
677 .map(|(path, stages)| {
678 let (index, worktree) = short_status_unmerged_codes(&stages);
679 ShortStatusEntry {
680 index,
681 worktree,
682 path,
683 head_mode: None,
684 index_mode: None,
685 worktree_mode: None,
686 head_oid: None,
687 index_oid: None,
688 submodule: None,
689 }
690 })
691 .collect()
692}
693
694pub(crate) fn short_status_unmerged_codes(stages: &BTreeSet<u16>) -> (u8, u8) {
695 match (
696 stages.contains(&1),
697 stages.contains(&2),
698 stages.contains(&3),
699 ) {
700 (true, false, false) => (b'D', b'D'),
701 (false, true, false) => (b'A', b'U'),
702 (true, true, false) => (b'U', b'D'),
703 (false, false, true) => (b'U', b'A'),
704 (true, false, true) => (b'D', b'U'),
705 (false, true, true) => (b'A', b'A'),
706 (true, true, true) => (b'U', b'U'),
707 (false, false, false) => (b'U', b'U'),
708 }
709}
710
711pub(crate) fn sparse_checkout_active_for_status(git_dir: &Path, index: &Index) -> bool {
712 index.is_sparse()
713 || index.entries.iter().any(IndexEntry::is_sparse_dir)
714 || sparse_checkout_config_enabled(git_dir)
715}
716
717pub(crate) fn sparse_checkout_active_for_borrowed_status(
718 git_dir: &Path,
719 index: &BorrowedIndex<'_>,
720) -> bool {
721 index
722 .entries
723 .iter()
724 .any(|entry| entry.mode == SPARSE_DIR_MODE && entry.is_skip_worktree())
725 || sparse_checkout_config_enabled(git_dir)
726}
727
728pub(crate) fn sparse_checkout_config_enabled(git_dir: &Path) -> bool {
729 GitConfig::read(git_dir.join("config"))
730 .ok()
731 .and_then(|config| config.get_bool("core", None, "sparseCheckout"))
732 == Some(true)
733 || GitConfig::read(git_dir.join("config.worktree"))
734 .ok()
735 .and_then(|config| config.get_bool("core", None, "sparseCheckout"))
736 == Some(true)
737}
738
739pub(crate) fn collect_status_entries_head_matches_index(
740 index: &BTreeMap<Vec<u8>, TrackedEntry>,
741 worktree: &BTreeMap<Vec<u8>, TrackedEntry>,
742 tracked_presence: &HashSet<Vec<u8>>,
743 stat_cache: &IndexStatCache,
744 sparse_checkout_active: bool,
745 submodule_dirt_map: &BTreeMap<Vec<u8>, u8>,
746 untracked_mode: StatusUntrackedMode,
747 entries: &mut Vec<ShortStatusEntry>,
748) {
749 for (path, index_entry) in index {
750 let intent_to_add = stat_cache
751 .index_entry(path)
752 .is_some_and(IndexEntry::is_intent_to_add);
753 let visible_index_entry = (!intent_to_add).then_some(index_entry);
754 let worktree_entry = worktree.get(path);
755 let worktree_present =
756 worktree_entry.is_some() || tracked_presence.contains(path.as_slice());
757 let skip_worktree = stat_cache
760 .index_entry(path)
761 .is_some_and(index_entry_skip_worktree)
762 && (!sparse_checkout_active || !worktree_present);
763 let submodule = status_submodule_from_entries(
764 path,
765 index_entry,
766 worktree_entry,
767 submodule_dirt_map,
768 untracked_mode,
769 );
770 let worktree_code = match worktree_entry {
771 _ if skip_worktree => b' ',
772 None if intent_to_add => b' ',
773 None if !worktree_present => b'D',
774 Some(_) if intent_to_add => b'A',
775 Some(worktree_entry) if Some(worktree_entry) != visible_index_entry => {
776 status_change_code(index_entry.mode, worktree_entry.mode)
777 }
778 _ if submodule.is_some_and(|sub| sub.any()) => b'M',
779 _ => b' ',
780 };
781 if worktree_code != b' ' {
782 entries.push(ShortStatusEntry {
783 index: b' ',
784 worktree: worktree_code,
785 path: path.clone(),
786 head_mode: visible_index_entry.map(|entry| entry.mode),
787 index_mode: visible_index_entry.map(|entry| entry.mode),
788 worktree_mode: status_worktree_mode(
789 visible_index_entry,
790 worktree_entry,
791 worktree_present,
792 ),
793 head_oid: visible_index_entry.map(|entry| entry.oid),
794 index_oid: visible_index_entry.map(|entry| entry.oid),
795 submodule: submodule.filter(|sub| sub.any()),
796 });
797 }
798 }
799}
800
801pub(crate) struct StatusComparisonInputs<'a> {
802 head: &'a BTreeMap<Vec<u8>, TrackedEntry>,
803 index: &'a BTreeMap<Vec<u8>, TrackedEntry>,
804 worktree: &'a BTreeMap<Vec<u8>, TrackedEntry>,
805 tracked_presence: &'a HashSet<Vec<u8>>,
806 stat_cache: &'a IndexStatCache,
807 sparse_checkout_active: bool,
808 submodule_dirt_map: &'a BTreeMap<Vec<u8>, u8>,
809 ignores: &'a IgnoreMatcher,
810}
811
812pub(crate) fn collect_status_entries_with_head(
813 inputs: StatusComparisonInputs<'_>,
814 untracked_mode: StatusUntrackedMode,
815 entries: &mut Vec<ShortStatusEntry>,
816) {
817 let mut paths = BTreeSet::new();
818 paths.extend(inputs.head.keys().cloned());
819 paths.extend(inputs.index.keys().cloned());
820 paths.extend(
821 inputs
822 .worktree
823 .keys()
824 .filter(|path| inputs.index.contains_key(*path))
825 .cloned(),
826 );
827
828 for path in paths {
829 let head_entry = inputs.head.get(&path);
830 let index_entry = inputs.index.get(&path);
831 let intent_to_add = inputs
832 .stat_cache
833 .index_entry(&path)
834 .is_some_and(IndexEntry::is_intent_to_add);
835 let visible_index_entry = index_entry.filter(|_| !intent_to_add);
836 let worktree_entry = inputs.worktree.get(&path);
837 let worktree_present =
838 worktree_entry.is_some() || inputs.tracked_presence.contains(path.as_slice());
839 if head_entry.is_none()
840 && index_entry.is_none()
841 && worktree_entry.is_some()
842 && inputs.ignores.is_ignored(&path, false)
843 {
844 continue;
845 }
846 let submodule = match visible_index_entry {
847 Some(index_entry) => status_submodule_from_entries(
848 &path,
849 index_entry,
850 worktree_entry,
851 inputs.submodule_dirt_map,
852 untracked_mode,
853 ),
854 None => None,
855 };
856 let skip_worktree = visible_index_entry.is_some_and(|_| {
859 inputs
860 .stat_cache
861 .index_entry(&path)
862 .is_some_and(index_entry_skip_worktree)
863 }) && (!inputs.sparse_checkout_active || !worktree_present);
864 let (index_code, worktree_code) =
865 if head_entry.is_none() && index_entry.is_none() && worktree_entry.is_some() {
866 (b'?', b'?')
867 } else {
868 let index_code = match (head_entry, visible_index_entry) {
869 (None, Some(_)) => b'A',
870 (Some(_), None) => b'D',
871 (Some(left), Some(right)) if left != right => {
872 status_change_code(left.mode, right.mode)
873 }
874 _ => b' ',
875 };
876 let worktree_code = match (visible_index_entry, worktree_entry) {
877 (None, Some(_)) if intent_to_add => b'A',
878 (None, Some(_)) => b'?',
879 (None, None) if intent_to_add => b' ',
880 (Some(_), _) if skip_worktree => b' ',
881 (Some(_), None) if !worktree_present => b'D',
882 (Some(left), Some(right)) if left != right => {
883 status_change_code(left.mode, right.mode)
884 }
885 _ if submodule.is_some_and(|sub| sub.any()) => b'M',
886 _ => b' ',
887 };
888 (index_code, worktree_code)
889 };
890 if index_code != b' ' || worktree_code != b' ' {
891 let worktree_mode = if skip_worktree {
892 visible_index_entry.map(|entry| entry.mode)
893 } else {
894 status_worktree_mode(visible_index_entry, worktree_entry, worktree_present)
895 };
896 entries.push(ShortStatusEntry {
897 index: index_code,
898 worktree: worktree_code,
899 path,
900 head_mode: head_entry.map(|entry| entry.mode),
901 index_mode: visible_index_entry.map(|entry| entry.mode),
902 worktree_mode,
903 head_oid: head_entry.map(|entry| entry.oid),
904 index_oid: visible_index_entry.map(|entry| entry.oid),
905 submodule: submodule.filter(|sub| sub.any()),
906 });
907 }
908 }
909}
910
911pub(crate) fn status_worktree_mode(
912 index_entry: Option<&TrackedEntry>,
913 worktree_entry: Option<&TrackedEntry>,
914 worktree_present: bool,
915) -> Option<u32> {
916 worktree_entry.map(|entry| entry.mode).or_else(|| {
917 worktree_present
918 .then(|| index_entry.map(|entry| entry.mode))
919 .flatten()
920 })
921}
922
923pub(crate) fn status_submodule_from_entries(
924 path: &[u8],
925 index_entry: &TrackedEntry,
926 worktree_entry: Option<&TrackedEntry>,
927 submodule_dirt_map: &BTreeMap<Vec<u8>, u8>,
928 _untracked_mode: StatusUntrackedMode,
929) -> Option<SubmoduleStatus> {
930 let worktree_entry = worktree_entry?;
931 if !sley_index::is_gitlink(index_entry.mode) || !sley_index::is_gitlink(worktree_entry.mode) {
932 return None;
933 }
934 let dirt = submodule_dirt_map.get(path).copied().unwrap_or(0);
935 Some(SubmoduleStatus {
936 new_commits: index_entry.oid != worktree_entry.oid,
937 modified_content: dirt & DIRTY_SUBMODULE_MODIFIED != 0,
938 untracked_content: dirt & DIRTY_SUBMODULE_UNTRACKED != 0,
939 })
940}
941
942pub(crate) fn short_status_tracked_only(
943 worktree_root: &Path,
944 git_dir: &Path,
945 format: ObjectFormat,
946 db: &FileObjectDatabase,
947 index: &Index,
948 stat_cache: &IndexStatCache,
949 head_matches_index: bool,
950 sparse_checkout_active: bool,
951 untracked_mode: StatusUntrackedMode,
952) -> Result<Vec<ShortStatusEntry>> {
953 let normal_entry_count = index
954 .entries
955 .iter()
956 .filter(|entry| entry.stage() == Stage::Normal)
957 .count();
958 if head_matches_index && normal_entry_count >= 512 {
959 return short_status_tracked_only_head_matches_index_parallel(
960 worktree_root,
961 git_dir,
962 format,
963 index,
964 stat_cache,
965 sparse_checkout_active,
966 untracked_mode,
967 );
968 }
969 let head = if head_matches_index {
970 None
971 } else {
972 Some(head_tree_entries(git_dir, format, db)?)
973 };
974 if !head_matches_index && normal_entry_count >= 512 {
975 if let Some(head) = head.as_ref() {
976 return short_status_tracked_only_with_head_parallel(
977 worktree_root,
978 git_dir,
979 format,
980 index,
981 stat_cache,
982 head,
983 sparse_checkout_active,
984 untracked_mode,
985 );
986 }
987 }
988 let mut clean_filter = None;
989 let mut entries = Vec::new();
990 for entry in index
991 .entries
992 .iter()
993 .filter(|entry| entry.stage() == Stage::Normal)
994 {
995 let path = entry.path.as_bytes();
996 let index_entry = TrackedEntry {
997 mode: entry.mode,
998 oid: entry.oid,
999 };
1000 let head_entry = if head_matches_index {
1001 (!entry.is_intent_to_add()).then_some(&index_entry)
1002 } else {
1003 head.as_ref().and_then(|head| head.get(path))
1004 };
1005 let worktree_entry = worktree_entry_for_index_entry_with_attributes(
1006 worktree_root,
1007 git_dir,
1008 format,
1009 entry,
1010 stat_cache,
1011 &mut clean_filter,
1012 )?;
1013 let submodule = tracked_only_submodule_status(
1014 worktree_root,
1015 path,
1016 &index_entry,
1017 worktree_entry.as_ref(),
1018 untracked_mode,
1019 )?;
1020 let visible_index_entry = (!entry.is_intent_to_add()).then_some(&index_entry);
1021 let index_code = match (head_entry, visible_index_entry) {
1022 (None, Some(_)) => b'A',
1023 (Some(_), None) => b'D',
1024 (Some(head_entry), Some(index_entry)) if *head_entry != *index_entry => {
1025 status_change_code(head_entry.mode, index_entry.mode)
1026 }
1027 _ => b' ',
1028 };
1029 let skip_worktree =
1032 entry.is_skip_worktree() && (!sparse_checkout_active || worktree_entry.is_none());
1033 let worktree_code = match worktree_entry.as_ref() {
1034 _ if skip_worktree => b' ',
1035 None if entry.is_intent_to_add() => b' ',
1036 None => b'D',
1037 Some(_) if entry.is_intent_to_add() => b'A',
1038 Some(worktree_entry) if Some(worktree_entry) != visible_index_entry => {
1039 status_change_code(index_entry.mode, worktree_entry.mode)
1040 }
1041 _ if submodule.is_some_and(|sub| sub.any()) => b'M',
1042 _ => b' ',
1043 };
1044 if index_code != b' ' || worktree_code != b' ' {
1045 entries.push(ShortStatusEntry {
1046 index: index_code,
1047 worktree: worktree_code,
1048 path: path.to_vec(),
1049 head_mode: head_entry.map(|entry| entry.mode),
1050 index_mode: visible_index_entry.map(|entry| entry.mode),
1051 worktree_mode: if skip_worktree {
1052 visible_index_entry.map(|entry| entry.mode)
1053 } else {
1054 worktree_entry.as_ref().map(|entry| entry.mode)
1055 },
1056 head_oid: head_entry.map(|entry| entry.oid),
1057 index_oid: visible_index_entry.map(|entry| entry.oid),
1058 submodule: submodule.filter(|sub| sub.any()),
1059 });
1060 }
1061 }
1062 if let Some(head) = head.as_ref() {
1063 let index_paths = index
1064 .entries
1065 .iter()
1066 .filter(|entry| entry.stage() == Stage::Normal)
1067 .map(|entry| entry.path.as_bytes().to_vec())
1068 .collect::<HashSet<_>>();
1069 for (path, head_entry) in head {
1070 if index_paths.contains(path.as_slice()) {
1071 continue;
1072 }
1073 entries.push(ShortStatusEntry {
1074 index: b'D',
1075 worktree: b' ',
1076 path: path.clone(),
1077 head_mode: Some(head_entry.mode),
1078 index_mode: None,
1079 worktree_mode: None,
1080 head_oid: Some(head_entry.oid),
1081 index_oid: None,
1082 submodule: None,
1083 });
1084 }
1085 }
1086 entries.sort_by(|left, right| {
1087 status_sort_category(left)
1088 .cmp(&status_sort_category(right))
1089 .then_with(|| left.path.cmp(&right.path))
1090 });
1091 Ok(entries)
1092}
1093
1094pub(crate) fn short_status_borrowed_head_matches_index_if_possible(
1095 worktree_root: &Path,
1096 git_dir: &Path,
1097 format: ObjectFormat,
1098 db: &FileObjectDatabase,
1099 untracked_mode: StatusUntrackedMode,
1100) -> Result<Option<Vec<ShortStatusEntry>>> {
1101 let index_path = repository_index_path(git_dir);
1102 let index_metadata = match fs::metadata(&index_path) {
1103 Ok(metadata) => metadata,
1104 Err(err)
1105 if err.kind() == std::io::ErrorKind::NotFound
1106 && matches!(untracked_mode, StatusUntrackedMode::None) =>
1107 {
1108 return Ok(Some(Vec::new()));
1109 }
1110 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
1111 Err(err) => return Err(err.into()),
1112 };
1113 let index_bytes = read_borrowed_index_bytes(&index_path)?;
1114 status_profile_mem(
1115 "after_index_bytes",
1116 &[
1117 ("index_file_bytes", index_metadata.len() as usize),
1118 ("index_bytes_len", index_bytes.as_ref().len()),
1119 (
1120 "index_bytes_mapped",
1121 usize::from(matches!(index_bytes, BorrowedIndexBytes::Mapped(_))),
1122 ),
1123 ],
1124 );
1125 let borrowed = match BorrowedIndex::parse(index_bytes.as_ref(), format) {
1126 Ok(index) => index,
1127 Err(GitError::Unsupported(_)) => return Ok(None),
1128 Err(err) => return Err(err),
1129 };
1130 status_profile_mem(
1131 "after_borrowed_parse",
1132 &[
1133 ("index_file_bytes", index_metadata.len() as usize),
1134 ("index_bytes_len", index_bytes.as_ref().len()),
1135 (
1136 "index_bytes_mapped",
1137 usize::from(matches!(index_bytes, BorrowedIndexBytes::Mapped(_))),
1138 ),
1139 ("borrowed_entries_len", borrowed.entries.len()),
1140 ("borrowed_entries_cap", borrowed.entries.capacity()),
1141 (
1142 "borrowed_entry_size",
1143 std::mem::size_of::<IndexEntryRef<'_>>(),
1144 ),
1145 (
1146 "borrowed_entries_cap_bytes",
1147 borrowed.entries.capacity() * std::mem::size_of::<IndexEntryRef<'_>>(),
1148 ),
1149 ("borrowed_extensions_len", borrowed.extensions.len()),
1150 ],
1151 );
1152 let sparse_checkout_active = sparse_checkout_active_for_borrowed_status(git_dir, &borrowed);
1153 if borrowed
1154 .entries
1155 .iter()
1156 .any(|entry| entry.mode == SPARSE_DIR_MODE && entry.is_skip_worktree())
1157 {
1158 return Ok(None);
1159 }
1160 if borrowed
1161 .entries
1162 .iter()
1163 .any(|entry| entry.stage() != Stage::Normal)
1164 {
1165 return Ok(None);
1166 }
1167 status_profile_mem(
1168 "after_sparse_scan",
1169 &[
1170 ("borrowed_entries_len", borrowed.entries.len()),
1171 (
1172 "borrowed_entries_cap_bytes",
1173 borrowed.entries.capacity() * std::mem::size_of::<IndexEntryRef<'_>>(),
1174 ),
1175 (
1176 "sparse_checkout_active",
1177 usize::from(sparse_checkout_active),
1178 ),
1179 ],
1180 );
1181 let Some(head_tree_oid) = resolve_head_tree_oid(git_dir, format, db)? else {
1182 return Ok(None);
1183 };
1184 status_profile_mem(
1185 "after_head_tree_oid",
1186 &[
1187 ("borrowed_entries_len", borrowed.entries.len()),
1188 (
1189 "borrowed_entries_cap_bytes",
1190 borrowed.entries.capacity() * std::mem::size_of::<IndexEntryRef<'_>>(),
1191 ),
1192 ],
1193 );
1194 let stage0_entry_count = borrowed
1195 .entries
1196 .iter()
1197 .filter(|entry| entry.stage() == Stage::Normal)
1198 .count();
1199 status_profile_mem(
1200 "after_stage0_count",
1201 &[
1202 ("stage0_entry_count", stage0_entry_count),
1203 ("borrowed_entries_len", borrowed.entries.len()),
1204 ],
1205 );
1206 if !head_matches_borrowed_index_from_cache_tree(
1207 &borrowed,
1208 format,
1209 &head_tree_oid,
1210 stage0_entry_count,
1211 )? {
1212 return Ok(None);
1213 }
1214 status_profile_mem(
1215 "after_head_matches_index",
1216 &[
1217 ("stage0_entry_count", stage0_entry_count),
1218 ("borrowed_entries_len", borrowed.entries.len()),
1219 (
1220 "borrowed_entries_cap_bytes",
1221 borrowed.entries.capacity() * std::mem::size_of::<IndexEntryRef<'_>>(),
1222 ),
1223 ],
1224 );
1225
1226 let index_mtime = file_mtime_parts(&index_metadata);
1227 let stat_cache = IndexStatCache::from_index_mtime_only(index_mtime);
1228 let profile_enabled = StatusProfileCounters::enabled();
1229 let mut profile = profile_enabled.then(|| StatusProfileCounters {
1230 fast_path_borrowed: true,
1231 ..StatusProfileCounters::default()
1232 });
1233
1234 if matches!(untracked_mode, StatusUntrackedMode::None) {
1235 let tracked_start = Instant::now();
1236 let entries = short_status_borrowed_tracked_only_head_matches_index_parallel(
1237 worktree_root,
1238 git_dir,
1239 format,
1240 &borrowed,
1241 &stat_cache,
1242 sparse_checkout_active,
1243 untracked_mode,
1244 )?;
1245 if let Some(profile) = profile.as_mut() {
1246 profile.tracked_elapsed_us = tracked_start.elapsed().as_micros();
1247 profile.emit();
1248 }
1249 return Ok(Some(entries));
1250 }
1251
1252 if stage0_entry_count < STATUS_BORROWED_OVERLAP_MIN_STAGE0 {
1253 let tracked_start = Instant::now();
1254 let mut entries = short_status_borrowed_tracked_only_head_matches_index_parallel(
1255 worktree_root,
1256 git_dir,
1257 format,
1258 &borrowed,
1259 &stat_cache,
1260 sparse_checkout_active,
1261 untracked_mode,
1262 )?;
1263 if let Some(profile) = profile.as_mut() {
1264 profile.tracked_elapsed_us = tracked_start.elapsed().as_micros();
1265 }
1266 let mut ignores = IgnoreMatcher::from_worktree_base(worktree_root)?;
1267 let untracked_start = Instant::now();
1268 let untracked_paths = status_untracked_paths_from_borrowed_index(
1269 worktree_root,
1270 git_dir,
1271 &borrowed,
1272 &mut ignores,
1273 untracked_mode,
1274 profile.as_mut(),
1275 )?;
1276 if let Some(profile) = profile.as_mut() {
1277 profile.untracked_elapsed_us = untracked_start.elapsed().as_micros();
1278 profile.untracked_rows = untracked_paths.len() as u64;
1279 }
1280 let render_start = Instant::now();
1281 append_untracked_status_entries(&mut entries, untracked_paths);
1282 if let Some(profile) = profile.as_mut() {
1283 profile.render_elapsed_us = render_start.elapsed().as_micros();
1284 profile.emit();
1285 }
1286 return Ok(Some(entries));
1287 }
1288
1289 if let Some(profile) = profile.as_mut() {
1290 profile.overlap_enabled = true;
1291 }
1292 let executor = StatusExecutor::new();
1293 if profile_enabled {
1294 let (mut entries, untracked_paths, untracked_profile) =
1295 std::thread::scope(|scope| -> Result<_> {
1296 let tracked = executor.spawn(scope, "status-tracked", || {
1297 let start = Instant::now();
1298 short_status_borrowed_tracked_only_head_matches_index_parallel(
1299 worktree_root,
1300 git_dir,
1301 format,
1302 &borrowed,
1303 &stat_cache,
1304 sparse_checkout_active,
1305 untracked_mode,
1306 )
1307 .map(|entries| (entries, start.elapsed().as_micros()))
1308 })?;
1309 let untracked = executor.spawn(
1310 scope,
1311 "status-untracked",
1312 || -> Result<(Vec<Vec<u8>>, StatusProfileCounters)> {
1313 let mut local_profile = StatusProfileCounters::default();
1314 let mut ignores = IgnoreMatcher::from_worktree_base(worktree_root)?;
1315 let start = Instant::now();
1316 let paths = status_untracked_paths_from_borrowed_index(
1317 worktree_root,
1318 git_dir,
1319 &borrowed,
1320 &mut ignores,
1321 untracked_mode,
1322 Some(&mut local_profile),
1323 )?;
1324 local_profile.untracked_elapsed_us = start.elapsed().as_micros();
1325 local_profile.untracked_rows = paths.len() as u64;
1326 Ok((paths, local_profile))
1327 },
1328 )?;
1329 let (entries, tracked_elapsed_us) = tracked.join()?;
1330 let (untracked_paths, untracked_profile) = untracked.join()?;
1331 if let Some(profile) = profile.as_mut() {
1332 profile.tracked_elapsed_us = tracked_elapsed_us;
1333 }
1334 Ok((entries, untracked_paths, Some(untracked_profile)))
1335 })?;
1336 if let Some(profile) = profile.as_mut() {
1337 if let Some(untracked_profile) = untracked_profile {
1338 profile.merge_untracked(untracked_profile);
1339 }
1340 }
1341 let render_start = Instant::now();
1342 append_untracked_status_entries(&mut entries, untracked_paths);
1343 if let Some(profile) = profile.as_mut() {
1344 profile.render_elapsed_us = render_start.elapsed().as_micros();
1345 profile.emit();
1346 }
1347 return Ok(Some(entries));
1348 }
1349 let (mut entries, untracked_paths) = std::thread::scope(|scope| -> Result<_> {
1350 let tracked = executor.spawn(scope, "status-tracked", || {
1351 short_status_borrowed_tracked_only_head_matches_index_parallel(
1352 worktree_root,
1353 git_dir,
1354 format,
1355 &borrowed,
1356 &stat_cache,
1357 sparse_checkout_active,
1358 untracked_mode,
1359 )
1360 })?;
1361 let untracked = executor.spawn(scope, "status-untracked", || -> Result<Vec<Vec<u8>>> {
1362 let mut ignores = IgnoreMatcher::from_worktree_base(worktree_root)?;
1363 status_untracked_paths_from_borrowed_index(
1364 worktree_root,
1365 git_dir,
1366 &borrowed,
1367 &mut ignores,
1368 untracked_mode,
1369 None,
1370 )
1371 })?;
1372 let entries = tracked.join()?;
1373 let untracked_paths = untracked.join()?;
1374 Ok((entries, untracked_paths))
1375 })?;
1376 let render_start = Instant::now();
1377 append_untracked_status_entries(&mut entries, untracked_paths);
1378 if let Some(profile) = profile.as_mut() {
1379 profile.render_elapsed_us = render_start.elapsed().as_micros();
1380 profile.emit();
1381 }
1382 Ok(Some(entries))
1383}
1384
1385pub(crate) fn stream_short_status_borrowed_head_matches_index_if_possible<F>(
1386 worktree_root: &Path,
1387 git_dir: &Path,
1388 format: ObjectFormat,
1389 db: &FileObjectDatabase,
1390 untracked_mode: StatusUntrackedMode,
1391 emit: &mut F,
1392) -> Result<Option<()>>
1393where
1394 F: for<'a> FnMut(ShortStatusRow<'a>) -> Result<StreamControl>,
1395{
1396 let index_path = repository_index_path(git_dir);
1397 let index_metadata = match fs::metadata(&index_path) {
1398 Ok(metadata) => metadata,
1399 Err(err)
1400 if err.kind() == std::io::ErrorKind::NotFound
1401 && matches!(untracked_mode, StatusUntrackedMode::None) =>
1402 {
1403 return Ok(Some(()));
1404 }
1405 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
1406 Err(err) => return Err(err.into()),
1407 };
1408 let index_bytes = read_borrowed_index_bytes(&index_path)?;
1409 status_profile_mem(
1410 "after_index_bytes",
1411 &[
1412 ("index_file_bytes", index_metadata.len() as usize),
1413 ("index_bytes_len", index_bytes.as_ref().len()),
1414 (
1415 "index_bytes_mapped",
1416 usize::from(matches!(index_bytes, BorrowedIndexBytes::Mapped(_))),
1417 ),
1418 ],
1419 );
1420 let borrowed = match BorrowedIndex::parse(index_bytes.as_ref(), format) {
1421 Ok(index) => index,
1422 Err(GitError::Unsupported(_)) => return Ok(None),
1423 Err(err) => return Err(err),
1424 };
1425 status_profile_mem(
1426 "after_borrowed_parse",
1427 &[
1428 ("index_file_bytes", index_metadata.len() as usize),
1429 ("index_bytes_len", index_bytes.as_ref().len()),
1430 (
1431 "index_bytes_mapped",
1432 usize::from(matches!(index_bytes, BorrowedIndexBytes::Mapped(_))),
1433 ),
1434 ("borrowed_entries_len", borrowed.entries.len()),
1435 ("borrowed_entries_cap", borrowed.entries.capacity()),
1436 (
1437 "borrowed_entry_size",
1438 std::mem::size_of::<IndexEntryRef<'_>>(),
1439 ),
1440 (
1441 "borrowed_entries_cap_bytes",
1442 borrowed.entries.capacity() * std::mem::size_of::<IndexEntryRef<'_>>(),
1443 ),
1444 ("borrowed_extensions_len", borrowed.extensions.len()),
1445 ],
1446 );
1447 let sparse_checkout_active = sparse_checkout_active_for_borrowed_status(git_dir, &borrowed);
1448 if borrowed
1449 .entries
1450 .iter()
1451 .any(|entry| entry.mode == SPARSE_DIR_MODE && entry.is_skip_worktree())
1452 {
1453 return Ok(None);
1454 }
1455 if borrowed
1456 .entries
1457 .iter()
1458 .any(|entry| entry.stage() != Stage::Normal)
1459 {
1460 return Ok(None);
1461 }
1462 status_profile_mem(
1463 "after_sparse_scan",
1464 &[
1465 ("borrowed_entries_len", borrowed.entries.len()),
1466 (
1467 "borrowed_entries_cap_bytes",
1468 borrowed.entries.capacity() * std::mem::size_of::<IndexEntryRef<'_>>(),
1469 ),
1470 (
1471 "sparse_checkout_active",
1472 usize::from(sparse_checkout_active),
1473 ),
1474 ],
1475 );
1476 let Some(head_tree_oid) = resolve_head_tree_oid(git_dir, format, db)? else {
1477 return Ok(None);
1478 };
1479 status_profile_mem(
1480 "after_head_tree_oid",
1481 &[
1482 ("borrowed_entries_len", borrowed.entries.len()),
1483 (
1484 "borrowed_entries_cap_bytes",
1485 borrowed.entries.capacity() * std::mem::size_of::<IndexEntryRef<'_>>(),
1486 ),
1487 ],
1488 );
1489 let stage0_entry_count = borrowed
1490 .entries
1491 .iter()
1492 .filter(|entry| entry.stage() == Stage::Normal)
1493 .count();
1494 status_profile_mem(
1495 "after_stage0_count",
1496 &[
1497 ("stage0_entry_count", stage0_entry_count),
1498 ("borrowed_entries_len", borrowed.entries.len()),
1499 ],
1500 );
1501 if !head_matches_borrowed_index_from_cache_tree(
1502 &borrowed,
1503 format,
1504 &head_tree_oid,
1505 stage0_entry_count,
1506 )? {
1507 return Ok(None);
1508 }
1509 status_profile_mem(
1510 "after_head_matches_index",
1511 &[
1512 ("stage0_entry_count", stage0_entry_count),
1513 ("borrowed_entries_len", borrowed.entries.len()),
1514 (
1515 "borrowed_entries_cap_bytes",
1516 borrowed.entries.capacity() * std::mem::size_of::<IndexEntryRef<'_>>(),
1517 ),
1518 ],
1519 );
1520
1521 let index_mtime = file_mtime_parts(&index_metadata);
1522 let stat_cache = IndexStatCache::from_index_mtime_only(index_mtime);
1523 let profile_enabled = StatusProfileCounters::enabled();
1524 let mut profile = profile_enabled.then(|| StatusProfileCounters {
1525 fast_path_borrowed: true,
1526 ..StatusProfileCounters::default()
1527 });
1528
1529 if matches!(untracked_mode, StatusUntrackedMode::None) {
1530 let tracked_start = Instant::now();
1531 let tracked_control =
1532 stream_short_status_borrowed_tracked_only_head_matches_index_parallel(
1533 worktree_root,
1534 git_dir,
1535 format,
1536 &borrowed,
1537 &stat_cache,
1538 sparse_checkout_active,
1539 untracked_mode,
1540 emit,
1541 )?;
1542 if let Some(profile) = profile.as_mut() {
1543 profile.tracked_elapsed_us = tracked_start.elapsed().as_micros();
1544 }
1545 if let Some(profile) = profile.as_ref() {
1546 profile.emit();
1547 }
1548 if tracked_control.is_stop() {
1549 return Ok(Some(()));
1550 }
1551 return Ok(Some(()));
1552 }
1553
1554 if stage0_entry_count < STATUS_BORROWED_OVERLAP_MIN_STAGE0 {
1555 let tracked_start = Instant::now();
1556 let tracked_control =
1557 stream_short_status_borrowed_tracked_only_head_matches_index_parallel(
1558 worktree_root,
1559 git_dir,
1560 format,
1561 &borrowed,
1562 &stat_cache,
1563 sparse_checkout_active,
1564 untracked_mode,
1565 emit,
1566 )?;
1567 if let Some(profile) = profile.as_mut() {
1568 profile.tracked_elapsed_us = tracked_start.elapsed().as_micros();
1569 }
1570 if tracked_control.is_stop() {
1571 if let Some(profile) = profile.as_ref() {
1572 profile.emit();
1573 }
1574 return Ok(Some(()));
1575 }
1576 let mut ignores = IgnoreMatcher::from_worktree_base(worktree_root)?;
1577 let untracked_start = Instant::now();
1578 stream_status_untracked_paths_from_borrowed_index(
1579 worktree_root,
1580 git_dir,
1581 &borrowed,
1582 &mut ignores,
1583 untracked_mode,
1584 profile.as_mut(),
1585 emit_untracked_status_entry(emit),
1586 )?;
1587 if let Some(profile) = profile.as_mut() {
1588 profile.untracked_elapsed_us = untracked_start.elapsed().as_micros();
1589 profile.emit();
1590 }
1591 return Ok(Some(()));
1592 }
1593
1594 if let Some(profile) = profile.as_mut() {
1595 profile.overlap_enabled = true;
1596 }
1597 let executor = StatusExecutor::new();
1598 let (tracked_control, untracked_paths, untracked_profile) =
1599 std::thread::scope(|scope| -> Result<_> {
1600 let untracked = executor.spawn(
1601 scope,
1602 "status-untracked",
1603 || -> Result<(Vec<Vec<u8>>, StatusProfileCounters)> {
1604 let mut local_profile = StatusProfileCounters::default();
1605 let mut ignores = IgnoreMatcher::from_worktree_base(worktree_root)?;
1606 ignores.emit_memory_profile("after_untracked_ignore");
1607 let start = Instant::now();
1608 let paths = status_untracked_paths_from_borrowed_index(
1609 worktree_root,
1610 git_dir,
1611 &borrowed,
1612 &mut ignores,
1613 untracked_mode,
1614 profile_enabled.then_some(&mut local_profile),
1615 )?;
1616 status_profile_mem(
1617 "after_untracked_collect",
1618 &[
1619 ("untracked_paths_len", paths.len()),
1620 ("untracked_paths_cap", paths.capacity()),
1621 (
1622 "untracked_paths_cap_bytes",
1623 paths.capacity() * std::mem::size_of::<Vec<u8>>(),
1624 ),
1625 (
1626 "untracked_path_payload_bytes",
1627 paths.iter().map(Vec::capacity).sum(),
1628 ),
1629 ],
1630 );
1631 local_profile.untracked_elapsed_us = start.elapsed().as_micros();
1632 local_profile.untracked_rows = paths.len() as u64;
1633 Ok((paths, local_profile))
1634 },
1635 )?;
1636 let tracked_start = Instant::now();
1637 let tracked_control =
1638 stream_short_status_borrowed_tracked_only_head_matches_index_parallel(
1639 worktree_root,
1640 git_dir,
1641 format,
1642 &borrowed,
1643 &stat_cache,
1644 sparse_checkout_active,
1645 untracked_mode,
1646 emit,
1647 )?;
1648 let tracked_elapsed_us = tracked_start.elapsed().as_micros();
1649 let (untracked_paths, untracked_profile) = untracked.join()?;
1650 if let Some(profile) = profile.as_mut() {
1651 profile.tracked_elapsed_us = tracked_elapsed_us;
1652 }
1653 Ok((
1654 tracked_control,
1655 untracked_paths,
1656 profile_enabled.then_some(untracked_profile),
1657 ))
1658 })?;
1659 status_profile_mem(
1660 "after_join",
1661 &[
1662 ("untracked_paths_len", untracked_paths.len()),
1663 ("untracked_paths_cap", untracked_paths.capacity()),
1664 (
1665 "untracked_paths_cap_bytes",
1666 untracked_paths.capacity() * std::mem::size_of::<Vec<u8>>(),
1667 ),
1668 (
1669 "untracked_path_payload_bytes",
1670 untracked_paths.iter().map(Vec::capacity).sum(),
1671 ),
1672 ],
1673 );
1674 if tracked_control.is_stop() {
1675 if let Some(profile) = profile.as_mut()
1676 && let Some(untracked_profile) = untracked_profile
1677 {
1678 profile.merge_untracked(untracked_profile);
1679 profile.emit();
1680 }
1681 return Ok(Some(()));
1682 }
1683 if let Some(profile) = profile.as_mut()
1684 && let Some(untracked_profile) = untracked_profile
1685 {
1686 profile.merge_untracked(untracked_profile);
1687 }
1688 let render_start = Instant::now();
1689 for path in untracked_paths {
1690 let row = untracked_status_row(&path);
1691 if emit(row)?.is_stop() {
1692 break;
1693 }
1694 }
1695 if let Some(profile) = profile.as_mut() {
1696 profile.render_elapsed_us = render_start.elapsed().as_micros();
1697 profile.emit();
1698 }
1699 status_profile_mem("after_render", &[]);
1700 Ok(Some(()))
1701}
1702
1703pub(crate) fn short_status_borrowed_head_matches_index_count_if_possible(
1704 worktree_root: &Path,
1705 git_dir: &Path,
1706 format: ObjectFormat,
1707 db: &FileObjectDatabase,
1708 untracked_mode: StatusUntrackedMode,
1709) -> Result<Option<usize>> {
1710 let index_path = repository_index_path(git_dir);
1711 let index_metadata = match fs::metadata(&index_path) {
1712 Ok(metadata) => metadata,
1713 Err(err)
1714 if err.kind() == std::io::ErrorKind::NotFound
1715 && matches!(untracked_mode, StatusUntrackedMode::None) =>
1716 {
1717 return Ok(Some(0));
1718 }
1719 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
1720 Err(err) => return Err(err.into()),
1721 };
1722 let index_bytes = read_borrowed_index_bytes(&index_path)?;
1723 let borrowed = match BorrowedIndex::parse(index_bytes.as_ref(), format) {
1724 Ok(index) => index,
1725 Err(GitError::Unsupported(_)) => return Ok(None),
1726 Err(err) => return Err(err),
1727 };
1728 let sparse_checkout_active = sparse_checkout_active_for_borrowed_status(git_dir, &borrowed);
1729 if borrowed
1730 .entries
1731 .iter()
1732 .any(|entry| entry.mode == SPARSE_DIR_MODE && entry.is_skip_worktree())
1733 {
1734 return Ok(None);
1735 }
1736 let Some(head_tree_oid) = resolve_head_tree_oid(git_dir, format, db)? else {
1737 return Ok(None);
1738 };
1739 let stage0_entry_count = borrowed
1740 .entries
1741 .iter()
1742 .filter(|entry| entry.stage() == Stage::Normal)
1743 .count();
1744 if !head_matches_borrowed_index_from_cache_tree(
1745 &borrowed,
1746 format,
1747 &head_tree_oid,
1748 stage0_entry_count,
1749 )? {
1750 return Ok(None);
1751 }
1752
1753 let index_mtime = file_mtime_parts(&index_metadata);
1754 let stat_cache = IndexStatCache::from_index_mtime_only(index_mtime);
1755 let profile_enabled = StatusProfileCounters::enabled();
1756 let mut profile = profile_enabled.then(|| StatusProfileCounters {
1757 fast_path_borrowed: true,
1758 ..StatusProfileCounters::default()
1759 });
1760
1761 if matches!(untracked_mode, StatusUntrackedMode::None) {
1762 let tracked_start = Instant::now();
1763 let count = short_status_borrowed_tracked_only_head_matches_index_count_parallel(
1764 worktree_root,
1765 git_dir,
1766 format,
1767 &borrowed,
1768 &stat_cache,
1769 sparse_checkout_active,
1770 untracked_mode,
1771 )?;
1772 if let Some(profile) = profile.as_mut() {
1773 profile.tracked_elapsed_us = tracked_start.elapsed().as_micros();
1774 profile.emit();
1775 }
1776 return Ok(Some(count));
1777 }
1778
1779 if stage0_entry_count < STATUS_BORROWED_OVERLAP_MIN_STAGE0 {
1780 let tracked_start = Instant::now();
1781 let tracked_count = short_status_borrowed_tracked_only_head_matches_index_count_parallel(
1782 worktree_root,
1783 git_dir,
1784 format,
1785 &borrowed,
1786 &stat_cache,
1787 sparse_checkout_active,
1788 untracked_mode,
1789 )?;
1790 if let Some(profile) = profile.as_mut() {
1791 profile.tracked_elapsed_us = tracked_start.elapsed().as_micros();
1792 }
1793 let mut ignores = IgnoreMatcher::from_worktree_base(worktree_root)?;
1794 let untracked_start = Instant::now();
1795 let untracked_count = status_untracked_count_from_borrowed_index(
1796 worktree_root,
1797 git_dir,
1798 &borrowed,
1799 &mut ignores,
1800 untracked_mode,
1801 profile.as_mut(),
1802 )?;
1803 if let Some(profile) = profile.as_mut() {
1804 profile.untracked_elapsed_us = untracked_start.elapsed().as_micros();
1805 profile.untracked_rows = untracked_count as u64;
1806 profile.emit();
1807 }
1808 return Ok(Some(tracked_count + untracked_count));
1809 }
1810
1811 if let Some(profile) = profile.as_mut() {
1812 profile.overlap_enabled = true;
1813 }
1814 let executor = StatusExecutor::new();
1815 let (tracked_count, untracked_count, untracked_profile) =
1816 std::thread::scope(|scope| -> Result<_> {
1817 let tracked = executor.spawn(scope, "status-tracked", || {
1818 let start = Instant::now();
1819 short_status_borrowed_tracked_only_head_matches_index_count_parallel(
1820 worktree_root,
1821 git_dir,
1822 format,
1823 &borrowed,
1824 &stat_cache,
1825 sparse_checkout_active,
1826 untracked_mode,
1827 )
1828 .map(|count| (count, start.elapsed().as_micros()))
1829 })?;
1830 let untracked = executor.spawn(
1831 scope,
1832 "status-untracked",
1833 || -> Result<(usize, StatusProfileCounters)> {
1834 let mut local_profile = StatusProfileCounters::default();
1835 let mut ignores = IgnoreMatcher::from_worktree_base(worktree_root)?;
1836 let start = Instant::now();
1837 let count = status_untracked_count_from_borrowed_index(
1838 worktree_root,
1839 git_dir,
1840 &borrowed,
1841 &mut ignores,
1842 untracked_mode,
1843 profile_enabled.then_some(&mut local_profile),
1844 )?;
1845 local_profile.untracked_elapsed_us = start.elapsed().as_micros();
1846 local_profile.untracked_rows = count as u64;
1847 Ok((count, local_profile))
1848 },
1849 )?;
1850 let (tracked_count, tracked_elapsed_us) = tracked.join()?;
1851 let (untracked_count, untracked_profile) = untracked.join()?;
1852 if let Some(profile) = profile.as_mut() {
1853 profile.tracked_elapsed_us = tracked_elapsed_us;
1854 }
1855 Ok((
1856 tracked_count,
1857 untracked_count,
1858 profile_enabled.then_some(untracked_profile),
1859 ))
1860 })?;
1861 if let Some(profile) = profile.as_mut() {
1862 if let Some(untracked_profile) = untracked_profile {
1863 profile.merge_untracked(untracked_profile);
1864 }
1865 profile.emit();
1866 }
1867 Ok(Some(tracked_count + untracked_count))
1868}
1869
1870pub(crate) fn emit_untracked_status_entry<'a, F>(
1871 emit: &'a mut F,
1872) -> impl FnMut(&[u8]) -> Result<StreamControl> + 'a
1873where
1874 F: for<'row> FnMut(ShortStatusRow<'row>) -> Result<StreamControl>,
1875{
1876 |path| emit(untracked_status_row(path))
1877}
1878
1879pub(crate) fn untracked_status_entry(path: Vec<u8>) -> ShortStatusEntry {
1880 ShortStatusEntry {
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 untracked_status_row(path: &[u8]) -> ShortStatusRow<'_> {
1894 ShortStatusRow {
1895 index: b'?',
1896 worktree: b'?',
1897 path,
1898 head_mode: None,
1899 index_mode: None,
1900 worktree_mode: None,
1901 head_oid: None,
1902 index_oid: None,
1903 submodule: None,
1904 }
1905}
1906
1907pub(crate) fn append_untracked_status_entries(
1908 entries: &mut Vec<ShortStatusEntry>,
1909 untracked_paths: Vec<Vec<u8>>,
1910) {
1911 for path in untracked_paths {
1912 entries.push(untracked_status_entry(path));
1913 }
1914}
1915
1916#[derive(Debug, Clone, Copy)]
1917pub(crate) enum TrackedOnlyPrecheck {
1918 Deleted(usize),
1919 Slow(usize),
1920}
1921
1922#[derive(Debug)]
1923pub(crate) enum TrackedOnlyPrecheckOutcome {
1924 Clean,
1925 Deleted,
1926 Slow,
1927}
1928
1929pub(crate) fn short_status_tracked_only_head_matches_index_parallel(
1930 worktree_root: &Path,
1931 git_dir: &Path,
1932 format: ObjectFormat,
1933 index: &Index,
1934 stat_cache: &IndexStatCache,
1935 sparse_checkout_active: bool,
1936 untracked_mode: StatusUntrackedMode,
1937) -> Result<Vec<ShortStatusEntry>> {
1938 let prechecks = tracked_only_non_clean_prechecks_parallel(
1939 worktree_root,
1940 index,
1941 stat_cache,
1942 sparse_checkout_active,
1943 )?;
1944
1945 let mut clean_filter = None;
1946 let mut entries = Vec::new();
1947 for precheck in prechecks {
1948 match precheck {
1949 TrackedOnlyPrecheck::Deleted(idx) => {
1950 let entry = &index.entries[idx];
1951 if entry.is_intent_to_add() {
1952 continue;
1953 }
1954 let path = entry.path.as_bytes();
1955 entries.push(ShortStatusEntry {
1956 index: b' ',
1957 worktree: b'D',
1958 path: path.to_vec(),
1959 head_mode: Some(entry.mode),
1960 index_mode: Some(entry.mode),
1961 worktree_mode: None,
1962 head_oid: Some(entry.oid),
1963 index_oid: Some(entry.oid),
1964 submodule: None,
1965 });
1966 }
1967 TrackedOnlyPrecheck::Slow(idx) => {
1968 let entry = &index.entries[idx];
1969 let path = entry.path.as_bytes();
1970 let index_entry = TrackedEntry {
1971 mode: entry.mode,
1972 oid: entry.oid,
1973 };
1974 let worktree_entry = worktree_entry_for_index_entry_with_attributes(
1975 worktree_root,
1976 git_dir,
1977 format,
1978 entry,
1979 stat_cache,
1980 &mut clean_filter,
1981 )?;
1982 let submodule = tracked_only_submodule_status(
1983 worktree_root,
1984 path,
1985 &index_entry,
1986 worktree_entry.as_ref(),
1987 untracked_mode,
1988 )?;
1989 let worktree_code = match worktree_entry.as_ref() {
1990 None if entry.is_intent_to_add() => b' ',
1991 None => b'D',
1992 Some(_) if entry.is_intent_to_add() => b'A',
1993 Some(worktree_entry) if *worktree_entry != index_entry => {
1994 status_change_code(index_entry.mode, worktree_entry.mode)
1995 }
1996 _ if submodule.is_some_and(|sub| sub.any()) => b'M',
1997 _ => b' ',
1998 };
1999 if worktree_code != b' ' {
2000 entries.push(ShortStatusEntry {
2001 index: b' ',
2002 worktree: worktree_code,
2003 path: path.to_vec(),
2004 head_mode: (!entry.is_intent_to_add()).then_some(index_entry.mode),
2005 index_mode: (!entry.is_intent_to_add()).then_some(index_entry.mode),
2006 worktree_mode: worktree_entry.as_ref().map(|entry| entry.mode),
2007 head_oid: (!entry.is_intent_to_add()).then_some(index_entry.oid),
2008 index_oid: (!entry.is_intent_to_add()).then_some(index_entry.oid),
2009 submodule: submodule.filter(|sub| sub.any()),
2010 });
2011 }
2012 }
2013 }
2014 }
2015 entries.sort_by(|left, right| {
2016 status_sort_category(left)
2017 .cmp(&status_sort_category(right))
2018 .then_with(|| left.path.cmp(&right.path))
2019 });
2020 Ok(entries)
2021}
2022
2023pub(crate) fn short_status_borrowed_tracked_only_head_matches_index_parallel(
2024 worktree_root: &Path,
2025 git_dir: &Path,
2026 format: ObjectFormat,
2027 index: &BorrowedIndex<'_>,
2028 stat_cache: &IndexStatCache,
2029 sparse_checkout_active: bool,
2030 untracked_mode: StatusUntrackedMode,
2031) -> Result<Vec<ShortStatusEntry>> {
2032 let prechecks = tracked_only_borrowed_non_clean_prechecks_parallel(
2033 worktree_root,
2034 index,
2035 stat_cache,
2036 sparse_checkout_active,
2037 )?;
2038
2039 let mut clean_filter = None;
2040 let mut entries = Vec::new();
2041 for precheck in prechecks {
2042 match precheck {
2043 TrackedOnlyPrecheck::Deleted(idx) => {
2044 let entry = &index.entries[idx];
2045 if entry.is_intent_to_add() {
2046 continue;
2047 }
2048 entries.push(ShortStatusEntry {
2049 index: b' ',
2050 worktree: b'D',
2051 path: entry.path.to_vec(),
2052 head_mode: Some(entry.mode),
2053 index_mode: Some(entry.mode),
2054 worktree_mode: None,
2055 head_oid: Some(entry.oid),
2056 index_oid: Some(entry.oid),
2057 submodule: None,
2058 });
2059 }
2060 TrackedOnlyPrecheck::Slow(idx) => {
2061 let entry = &index.entries[idx];
2062 let index_entry = TrackedEntry {
2063 mode: entry.mode,
2064 oid: entry.oid,
2065 };
2066 let worktree_entry = worktree_entry_for_index_entry_ref_with_attributes(
2067 worktree_root,
2068 git_dir,
2069 format,
2070 entry,
2071 stat_cache,
2072 &mut clean_filter,
2073 )?;
2074 let submodule = tracked_only_submodule_status(
2075 worktree_root,
2076 entry.path,
2077 &index_entry,
2078 worktree_entry.as_ref(),
2079 untracked_mode,
2080 )?;
2081 let worktree_code = match worktree_entry.as_ref() {
2082 None if entry.is_intent_to_add() => b' ',
2083 None => b'D',
2084 Some(_) if entry.is_intent_to_add() => b'A',
2085 Some(worktree_entry) if *worktree_entry != index_entry => {
2086 status_change_code(index_entry.mode, worktree_entry.mode)
2087 }
2088 _ if submodule.is_some_and(|sub| sub.any()) => b'M',
2089 _ => b' ',
2090 };
2091 if worktree_code != b' ' {
2092 entries.push(ShortStatusEntry {
2093 index: b' ',
2094 worktree: worktree_code,
2095 path: entry.path.to_vec(),
2096 head_mode: (!entry.is_intent_to_add()).then_some(index_entry.mode),
2097 index_mode: (!entry.is_intent_to_add()).then_some(index_entry.mode),
2098 worktree_mode: worktree_entry.as_ref().map(|entry| entry.mode),
2099 head_oid: (!entry.is_intent_to_add()).then_some(index_entry.oid),
2100 index_oid: (!entry.is_intent_to_add()).then_some(index_entry.oid),
2101 submodule: submodule.filter(|sub| sub.any()),
2102 });
2103 }
2104 }
2105 }
2106 }
2107 entries.sort_by(|left, right| {
2108 status_sort_category(left)
2109 .cmp(&status_sort_category(right))
2110 .then_with(|| left.path.cmp(&right.path))
2111 });
2112 Ok(entries)
2113}
2114
2115pub(crate) fn stream_short_status_borrowed_tracked_only_head_matches_index_parallel<F>(
2116 worktree_root: &Path,
2117 git_dir: &Path,
2118 format: ObjectFormat,
2119 index: &BorrowedIndex<'_>,
2120 stat_cache: &IndexStatCache,
2121 sparse_checkout_active: bool,
2122 untracked_mode: StatusUntrackedMode,
2123 emit: &mut F,
2124) -> Result<StreamControl>
2125where
2126 F: for<'a> FnMut(ShortStatusRow<'a>) -> Result<StreamControl>,
2127{
2128 let prechecks = tracked_only_borrowed_non_clean_prechecks_parallel(
2129 worktree_root,
2130 index,
2131 stat_cache,
2132 sparse_checkout_active,
2133 )?;
2134
2135 let mut clean_filter = None;
2136 for precheck in prechecks {
2137 match precheck {
2138 TrackedOnlyPrecheck::Deleted(idx) => {
2139 let entry = &index.entries[idx];
2140 if entry.is_intent_to_add() {
2141 continue;
2142 }
2143 if emit(ShortStatusRow {
2144 index: b' ',
2145 worktree: b'D',
2146 path: entry.path,
2147 head_mode: Some(entry.mode),
2148 index_mode: Some(entry.mode),
2149 worktree_mode: None,
2150 head_oid: Some(entry.oid),
2151 index_oid: Some(entry.oid),
2152 submodule: None,
2153 })?
2154 .is_stop()
2155 {
2156 return Ok(StreamControl::Stop);
2157 }
2158 }
2159 TrackedOnlyPrecheck::Slow(idx) => {
2160 let entry = &index.entries[idx];
2161 let index_entry = TrackedEntry {
2162 mode: entry.mode,
2163 oid: entry.oid,
2164 };
2165 let worktree_entry = worktree_entry_for_index_entry_ref_with_attributes(
2166 worktree_root,
2167 git_dir,
2168 format,
2169 entry,
2170 stat_cache,
2171 &mut clean_filter,
2172 )?;
2173 let submodule = tracked_only_submodule_status(
2174 worktree_root,
2175 entry.path,
2176 &index_entry,
2177 worktree_entry.as_ref(),
2178 untracked_mode,
2179 )?;
2180 let worktree_code = match worktree_entry.as_ref() {
2181 None if entry.is_intent_to_add() => b' ',
2182 None => b'D',
2183 Some(_) if entry.is_intent_to_add() => b'A',
2184 Some(worktree_entry) if *worktree_entry != index_entry => {
2185 status_change_code(index_entry.mode, worktree_entry.mode)
2186 }
2187 _ if submodule.is_some_and(|sub| sub.any()) => b'M',
2188 _ => b' ',
2189 };
2190 if worktree_code != b' ' {
2191 if emit(ShortStatusRow {
2192 index: b' ',
2193 worktree: worktree_code,
2194 path: entry.path,
2195 head_mode: (!entry.is_intent_to_add()).then_some(index_entry.mode),
2196 index_mode: (!entry.is_intent_to_add()).then_some(index_entry.mode),
2197 worktree_mode: worktree_entry.as_ref().map(|entry| entry.mode),
2198 head_oid: (!entry.is_intent_to_add()).then_some(index_entry.oid),
2199 index_oid: (!entry.is_intent_to_add()).then_some(index_entry.oid),
2200 submodule: submodule.filter(|sub| sub.any()),
2201 })?
2202 .is_stop()
2203 {
2204 return Ok(StreamControl::Stop);
2205 }
2206 }
2207 }
2208 }
2209 }
2210 Ok(StreamControl::Continue)
2211}
2212
2213pub(crate) fn short_status_borrowed_tracked_only_head_matches_index_count_parallel(
2214 worktree_root: &Path,
2215 git_dir: &Path,
2216 format: ObjectFormat,
2217 index: &BorrowedIndex<'_>,
2218 stat_cache: &IndexStatCache,
2219 sparse_checkout_active: bool,
2220 untracked_mode: StatusUntrackedMode,
2221) -> Result<usize> {
2222 let prechecks = tracked_only_borrowed_non_clean_prechecks_parallel(
2223 worktree_root,
2224 index,
2225 stat_cache,
2226 sparse_checkout_active,
2227 )?;
2228
2229 let mut clean_filter = None;
2230 let mut count = 0usize;
2231 for precheck in prechecks {
2232 match precheck {
2233 TrackedOnlyPrecheck::Deleted(_) => count += 1,
2234 TrackedOnlyPrecheck::Slow(idx) => {
2235 let entry = &index.entries[idx];
2236 let index_entry = TrackedEntry {
2237 mode: entry.mode,
2238 oid: entry.oid,
2239 };
2240 let worktree_entry = worktree_entry_for_index_entry_ref_with_attributes(
2241 worktree_root,
2242 git_dir,
2243 format,
2244 entry,
2245 stat_cache,
2246 &mut clean_filter,
2247 )?;
2248 let submodule = tracked_only_submodule_status(
2249 worktree_root,
2250 entry.path,
2251 &index_entry,
2252 worktree_entry.as_ref(),
2253 untracked_mode,
2254 )?;
2255 let worktree_code = match worktree_entry.as_ref() {
2256 None => b'D',
2257 Some(worktree_entry) if *worktree_entry != index_entry => {
2258 status_change_code(index_entry.mode, worktree_entry.mode)
2259 }
2260 _ if submodule.is_some_and(|sub| sub.any()) => b'M',
2261 _ => b' ',
2262 };
2263 if worktree_code != b' ' {
2264 count += 1;
2265 }
2266 }
2267 }
2268 }
2269 Ok(count)
2270}
2271
2272pub(crate) fn short_status_tracked_only_with_head_parallel(
2273 worktree_root: &Path,
2274 git_dir: &Path,
2275 format: ObjectFormat,
2276 index: &Index,
2277 stat_cache: &IndexStatCache,
2278 head: &BTreeMap<Vec<u8>, TrackedEntry>,
2279 sparse_checkout_active: bool,
2280 untracked_mode: StatusUntrackedMode,
2281) -> Result<Vec<ShortStatusEntry>> {
2282 let prechecks = tracked_only_non_clean_prechecks_parallel(
2283 worktree_root,
2284 index,
2285 stat_cache,
2286 sparse_checkout_active,
2287 )?;
2288 let mut precheck_cursor = 0usize;
2289 let mut clean_filter = None;
2290 let mut entries = Vec::new();
2291
2292 for (idx, entry) in index.entries.iter().enumerate() {
2293 if entry.stage() != Stage::Normal {
2294 continue;
2295 }
2296 let path = entry.path.as_bytes();
2297 let index_entry = TrackedEntry {
2298 mode: entry.mode,
2299 oid: entry.oid,
2300 };
2301 let head_entry = head.get(path);
2302 let visible_index_entry = (!entry.is_intent_to_add()).then_some(&index_entry);
2303 let index_code = match (head_entry, visible_index_entry) {
2304 (None, Some(_)) => b'A',
2305 (Some(_), None) => b'D',
2306 (Some(head_entry), Some(index_entry)) if *head_entry != *index_entry => {
2307 status_change_code(head_entry.mode, index_entry.mode)
2308 }
2309 _ => b' ',
2310 };
2311 let precheck = prechecks
2312 .get(precheck_cursor)
2313 .copied()
2314 .and_then(|precheck| {
2315 if tracked_only_precheck_index(precheck) == idx {
2316 precheck_cursor += 1;
2317 Some(precheck)
2318 } else {
2319 None
2320 }
2321 });
2322 let (worktree_code, worktree_mode, submodule) = match precheck {
2323 None if entry.is_intent_to_add() => (b' ', None, None),
2324 None => (b' ', Some(index_entry.mode), None),
2325 Some(TrackedOnlyPrecheck::Deleted(_)) if entry.is_intent_to_add() => (b' ', None, None),
2326 Some(TrackedOnlyPrecheck::Deleted(_)) => (b'D', None, None),
2327 Some(TrackedOnlyPrecheck::Slow(_)) => {
2328 let worktree_entry = worktree_entry_for_index_entry_with_attributes(
2329 worktree_root,
2330 git_dir,
2331 format,
2332 entry,
2333 stat_cache,
2334 &mut clean_filter,
2335 )?;
2336 let submodule = tracked_only_submodule_status(
2337 worktree_root,
2338 path,
2339 &index_entry,
2340 worktree_entry.as_ref(),
2341 untracked_mode,
2342 )?;
2343 let worktree_code = match worktree_entry.as_ref() {
2344 None if entry.is_intent_to_add() => b' ',
2345 None => b'D',
2346 Some(_) if entry.is_intent_to_add() => b'A',
2347 Some(worktree_entry) if *worktree_entry != index_entry => {
2348 status_change_code(index_entry.mode, worktree_entry.mode)
2349 }
2350 _ if submodule.is_some_and(|sub| sub.any()) => b'M',
2351 _ => b' ',
2352 };
2353 (
2354 worktree_code,
2355 worktree_entry.as_ref().map(|entry| entry.mode),
2356 submodule.filter(|sub| sub.any()),
2357 )
2358 }
2359 };
2360 if index_code != b' ' || worktree_code != b' ' {
2361 entries.push(ShortStatusEntry {
2362 index: index_code,
2363 worktree: worktree_code,
2364 path: path.to_vec(),
2365 head_mode: head_entry.map(|entry| entry.mode),
2366 index_mode: visible_index_entry.map(|entry| entry.mode),
2367 worktree_mode,
2368 head_oid: head_entry.map(|entry| entry.oid),
2369 index_oid: visible_index_entry.map(|entry| entry.oid),
2370 submodule,
2371 });
2372 }
2373 }
2374
2375 let index_paths = index
2376 .entries
2377 .iter()
2378 .filter(|entry| entry.stage() == Stage::Normal)
2379 .map(|entry| entry.path.as_bytes().to_vec())
2380 .collect::<HashSet<_>>();
2381 for (path, head_entry) in head {
2382 if index_paths.contains(path.as_slice()) {
2383 continue;
2384 }
2385 entries.push(ShortStatusEntry {
2386 index: b'D',
2387 worktree: b' ',
2388 path: path.clone(),
2389 head_mode: Some(head_entry.mode),
2390 index_mode: None,
2391 worktree_mode: None,
2392 head_oid: Some(head_entry.oid),
2393 index_oid: None,
2394 submodule: None,
2395 });
2396 }
2397 entries.sort_by(|left, right| {
2398 status_sort_category(left)
2399 .cmp(&status_sort_category(right))
2400 .then_with(|| left.path.cmp(&right.path))
2401 });
2402 Ok(entries)
2403}
2404
2405pub(crate) fn tracked_only_precheck_index(precheck: TrackedOnlyPrecheck) -> usize {
2406 match precheck {
2407 TrackedOnlyPrecheck::Deleted(idx) | TrackedOnlyPrecheck::Slow(idx) => idx,
2408 }
2409}
2410
2411pub(crate) fn stage0_index_entry_count<E>(
2412 entries: &[E],
2413 mut stage: impl FnMut(&E) -> Stage,
2414) -> usize {
2415 entries
2416 .iter()
2417 .filter(|entry| stage(entry) == Stage::Normal)
2418 .count()
2419}
2420
2421pub(crate) fn stage0_index_chunk_ranges<E>(
2422 entries: &[E],
2423 chunk_size: usize,
2424 mut stage: impl FnMut(&E) -> Stage,
2425) -> Vec<std::ops::Range<usize>> {
2426 debug_assert!(chunk_size > 0);
2427 let mut ranges = Vec::new();
2428 let mut start = None;
2429 let mut end = 0usize;
2430 let mut normals_in_chunk = 0usize;
2431 for (idx, entry) in entries.iter().enumerate() {
2432 if stage(entry) != Stage::Normal {
2433 continue;
2434 }
2435 if start.is_none() {
2436 start = Some(idx);
2437 }
2438 end = idx + 1;
2439 normals_in_chunk += 1;
2440 if normals_in_chunk == chunk_size {
2441 ranges.push(start.expect("chunk start must exist")..end);
2442 start = None;
2443 normals_in_chunk = 0;
2444 }
2445 }
2446 if let Some(start) = start {
2447 ranges.push(start..end);
2448 }
2449 ranges
2450}
2451
2452pub(crate) fn tracked_only_non_clean_prechecks_parallel(
2453 worktree_root: &Path,
2454 index: &Index,
2455 stat_cache: &IndexStatCache,
2456 sparse_checkout_active: bool,
2457) -> Result<Vec<TrackedOnlyPrecheck>> {
2458 let normal_count = stage0_index_entry_count(&index.entries, IndexEntry::stage);
2459 if normal_count == 0 {
2460 return Ok(Vec::new());
2461 }
2462 let executor = StatusExecutor::new();
2463 let worker_count = executor.worker_count_for(normal_count, 512, 4);
2464 if worker_count == 1 {
2465 let mut prechecks = Vec::new();
2466 let mut absolute = PathBuf::new();
2467 for (idx, entry) in index.entries.iter().enumerate() {
2468 if entry.stage() != Stage::Normal {
2469 continue;
2470 }
2471 match tracked_only_stat_precheck(
2472 worktree_root,
2473 entry,
2474 stat_cache,
2475 sparse_checkout_active,
2476 &mut absolute,
2477 )? {
2478 TrackedOnlyPrecheckOutcome::Clean => {}
2479 TrackedOnlyPrecheckOutcome::Deleted => {
2480 prechecks.push(TrackedOnlyPrecheck::Deleted(idx));
2481 }
2482 TrackedOnlyPrecheckOutcome::Slow => {
2483 prechecks.push(TrackedOnlyPrecheck::Slow(idx));
2484 }
2485 }
2486 }
2487 return Ok(prechecks);
2488 }
2489 let chunk_size = normal_count.div_ceil(worker_count);
2490 let chunk_ranges = stage0_index_chunk_ranges(&index.entries, chunk_size, IndexEntry::stage);
2491 let next_chunk = AtomicUsize::new(0);
2492 let mut prechecks = std::thread::scope(|scope| -> Result<Vec<TrackedOnlyPrecheck>> {
2493 let mut handles = Vec::new();
2494 for _ in 0..worker_count {
2495 let chunk_ranges = &chunk_ranges;
2496 let next_chunk = &next_chunk;
2497 handles.push(executor.spawn(
2498 scope,
2499 "status-precheck",
2500 move || -> Result<Vec<TrackedOnlyPrecheck>> {
2501 let mut prechecks = Vec::new();
2502 let mut absolute = PathBuf::new();
2503 loop {
2504 let chunk_idx = next_chunk.fetch_add(1, Ordering::Relaxed);
2505 let Some(range) = chunk_ranges.get(chunk_idx) else {
2506 break;
2507 };
2508 for idx in range.clone() {
2509 let entry = &index.entries[idx];
2510 if entry.stage() != Stage::Normal {
2511 continue;
2512 }
2513 match tracked_only_stat_precheck(
2514 worktree_root,
2515 entry,
2516 stat_cache,
2517 sparse_checkout_active,
2518 &mut absolute,
2519 )? {
2520 TrackedOnlyPrecheckOutcome::Clean => {}
2521 TrackedOnlyPrecheckOutcome::Deleted => {
2522 prechecks.push(TrackedOnlyPrecheck::Deleted(idx));
2523 }
2524 TrackedOnlyPrecheckOutcome::Slow => {
2525 prechecks.push(TrackedOnlyPrecheck::Slow(idx));
2526 }
2527 }
2528 }
2529 }
2530 Ok(prechecks)
2531 },
2532 )?);
2533 }
2534 let mut prechecks = Vec::new();
2535 for handle in handles {
2536 let mut chunk = handle.join()?;
2537 prechecks.append(&mut chunk);
2538 }
2539 Ok(prechecks)
2540 })?;
2541 prechecks.sort_by_key(|precheck| tracked_only_precheck_index(*precheck));
2542 Ok(prechecks)
2543}
2544
2545pub(crate) fn tracked_only_borrowed_non_clean_prechecks_parallel(
2546 worktree_root: &Path,
2547 index: &BorrowedIndex<'_>,
2548 stat_cache: &IndexStatCache,
2549 sparse_checkout_active: bool,
2550) -> Result<Vec<TrackedOnlyPrecheck>> {
2551 let normal_count = stage0_index_entry_count(&index.entries, IndexEntryRef::stage);
2552 if normal_count == 0 {
2553 return Ok(Vec::new());
2554 }
2555 let executor = StatusExecutor::new();
2556 let worker_count = executor.worker_count_for(normal_count, 512, 4);
2557 if worker_count == 1 {
2558 let mut prechecks = Vec::new();
2559 let mut absolute = PathBuf::new();
2560 for (idx, entry) in index.entries.iter().enumerate() {
2561 if entry.stage() != Stage::Normal {
2562 continue;
2563 }
2564 match tracked_only_borrowed_stat_precheck(
2565 worktree_root,
2566 entry,
2567 stat_cache,
2568 sparse_checkout_active,
2569 &mut absolute,
2570 )? {
2571 TrackedOnlyPrecheckOutcome::Clean => {}
2572 TrackedOnlyPrecheckOutcome::Deleted => {
2573 prechecks.push(TrackedOnlyPrecheck::Deleted(idx));
2574 }
2575 TrackedOnlyPrecheckOutcome::Slow => {
2576 prechecks.push(TrackedOnlyPrecheck::Slow(idx));
2577 }
2578 }
2579 }
2580 return Ok(prechecks);
2581 }
2582 let chunk_size = normal_count.div_ceil(worker_count);
2583 let chunk_ranges = stage0_index_chunk_ranges(&index.entries, chunk_size, IndexEntryRef::stage);
2584 let next_chunk = AtomicUsize::new(0);
2585 let mut prechecks = std::thread::scope(|scope| -> Result<Vec<TrackedOnlyPrecheck>> {
2586 let mut handles = Vec::new();
2587 for _ in 0..worker_count {
2588 let chunk_ranges = &chunk_ranges;
2589 let next_chunk = &next_chunk;
2590 handles.push(executor.spawn(
2591 scope,
2592 "status-precheck",
2593 move || -> Result<Vec<TrackedOnlyPrecheck>> {
2594 let mut prechecks = Vec::new();
2595 let mut absolute = PathBuf::new();
2596 loop {
2597 let chunk_idx = next_chunk.fetch_add(1, Ordering::Relaxed);
2598 let Some(range) = chunk_ranges.get(chunk_idx) else {
2599 break;
2600 };
2601 for idx in range.clone() {
2602 let entry = &index.entries[idx];
2603 if entry.stage() != Stage::Normal {
2604 continue;
2605 }
2606 match tracked_only_borrowed_stat_precheck(
2607 worktree_root,
2608 entry,
2609 stat_cache,
2610 sparse_checkout_active,
2611 &mut absolute,
2612 )? {
2613 TrackedOnlyPrecheckOutcome::Clean => {}
2614 TrackedOnlyPrecheckOutcome::Deleted => {
2615 prechecks.push(TrackedOnlyPrecheck::Deleted(idx));
2616 }
2617 TrackedOnlyPrecheckOutcome::Slow => {
2618 prechecks.push(TrackedOnlyPrecheck::Slow(idx));
2619 }
2620 }
2621 }
2622 }
2623 Ok(prechecks)
2624 },
2625 )?);
2626 }
2627 let mut prechecks = Vec::new();
2628 for handle in handles {
2629 let mut chunk = handle.join()?;
2630 prechecks.append(&mut chunk);
2631 }
2632 Ok(prechecks)
2633 })?;
2634 prechecks.sort_by_key(|precheck| tracked_only_precheck_index(*precheck));
2635 Ok(prechecks)
2636}
2637
2638pub(crate) fn tracked_only_stat_precheck(
2639 worktree_root: &Path,
2640 index_entry: &IndexEntry,
2641 stat_cache: &IndexStatCache,
2642 sparse_checkout_active: bool,
2643 absolute: &mut PathBuf,
2644) -> Result<TrackedOnlyPrecheckOutcome> {
2645 if index_entry.is_skip_worktree() && !sparse_checkout_active {
2652 return Ok(TrackedOnlyPrecheckOutcome::Clean);
2653 }
2654 if sley_index::is_gitlink(index_entry.mode) {
2655 return Ok(TrackedOnlyPrecheckOutcome::Slow);
2656 }
2657 let git_path = index_entry.path.as_bytes();
2658 set_worktree_path_from_repo_path(worktree_root, git_path, absolute)?;
2659 let metadata = match fs::symlink_metadata(&absolute) {
2660 Ok(metadata) => metadata,
2661 Err(err)
2662 if matches!(
2663 err.kind(),
2664 std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
2665 ) =>
2666 {
2667 if index_entry.is_skip_worktree() {
2669 return Ok(TrackedOnlyPrecheckOutcome::Clean);
2670 }
2671 return Ok(TrackedOnlyPrecheckOutcome::Deleted);
2672 }
2673 Err(err) => return Err(err.into()),
2674 };
2675 let file_type = metadata.file_type();
2676 if file_type.is_dir() || !(file_type.is_file() || file_type.is_symlink()) {
2677 return Ok(TrackedOnlyPrecheckOutcome::Slow);
2678 }
2679 if stat_cache
2680 .reuse_index_entry(index_entry, &metadata)
2681 .is_some()
2682 {
2683 Ok(TrackedOnlyPrecheckOutcome::Clean)
2684 } else {
2685 Ok(TrackedOnlyPrecheckOutcome::Slow)
2686 }
2687}
2688
2689pub(crate) fn tracked_only_borrowed_stat_precheck(
2690 worktree_root: &Path,
2691 index_entry: &IndexEntryRef<'_>,
2692 stat_cache: &IndexStatCache,
2693 sparse_checkout_active: bool,
2694 absolute: &mut PathBuf,
2695) -> Result<TrackedOnlyPrecheckOutcome> {
2696 if index_entry.is_skip_worktree() && !sparse_checkout_active {
2701 return Ok(TrackedOnlyPrecheckOutcome::Clean);
2702 }
2703 if sley_index::is_gitlink(index_entry.mode) {
2704 return Ok(TrackedOnlyPrecheckOutcome::Slow);
2705 }
2706 set_worktree_path_from_repo_path(worktree_root, index_entry.path, absolute)?;
2707 let metadata = match fs::symlink_metadata(&absolute) {
2708 Ok(metadata) => metadata,
2709 Err(err)
2710 if matches!(
2711 err.kind(),
2712 std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
2713 ) =>
2714 {
2715 if index_entry.is_skip_worktree() {
2717 return Ok(TrackedOnlyPrecheckOutcome::Clean);
2718 }
2719 return Ok(TrackedOnlyPrecheckOutcome::Deleted);
2720 }
2721 Err(err) => return Err(err.into()),
2722 };
2723 let file_type = metadata.file_type();
2724 if file_type.is_dir() || !(file_type.is_file() || file_type.is_symlink()) {
2725 return Ok(TrackedOnlyPrecheckOutcome::Slow);
2726 }
2727 if stat_cache
2728 .reuse_index_entry_ref(index_entry, &metadata)
2729 .is_some()
2730 {
2731 Ok(TrackedOnlyPrecheckOutcome::Clean)
2732 } else {
2733 Ok(TrackedOnlyPrecheckOutcome::Slow)
2734 }
2735}
2736
2737pub(crate) fn set_worktree_path_from_repo_path(
2738 worktree_root: &Path,
2739 git_path: &[u8],
2740 out: &mut PathBuf,
2741) -> Result<()> {
2742 out.clear();
2743 out.push(worktree_root);
2744 push_repo_path(out, git_path)
2745}
2746
2747#[cfg(unix)]
2748pub(crate) fn push_repo_path(out: &mut PathBuf, path: &[u8]) -> Result<()> {
2749 use std::os::unix::ffi::OsStrExt;
2750
2751 out.push(Path::new(std::ffi::OsStr::from_bytes(path)));
2752 Ok(())
2753}
2754
2755#[cfg(not(unix))]
2756pub(crate) fn push_repo_path(out: &mut PathBuf, path: &[u8]) -> Result<()> {
2757 let path = std::str::from_utf8(path)
2758 .map_err(|_| GitError::InvalidPath("index path is not utf8".into()))?;
2759 for component in path.split('/') {
2760 out.push(component);
2761 }
2762 Ok(())
2763}
2764
2765pub(crate) fn tracked_only_submodule_status(
2766 worktree_root: &Path,
2767 path: &[u8],
2768 index_entry: &TrackedEntry,
2769 worktree_entry: Option<&TrackedEntry>,
2770 _untracked_mode: StatusUntrackedMode,
2771) -> Result<Option<SubmoduleStatus>> {
2772 let Some(worktree_entry) = worktree_entry else {
2773 return Ok(None);
2774 };
2775 if !sley_index::is_gitlink(index_entry.mode) || !sley_index::is_gitlink(worktree_entry.mode) {
2776 return Ok(None);
2777 }
2778 let absolute = worktree_root.join(repo_path_to_os_path(path)?);
2779 let dirt = if absolute.is_dir() {
2780 submodule_dirt_checked(&absolute)?
2781 } else {
2782 0
2783 };
2784 Ok(Some(SubmoduleStatus {
2785 new_commits: index_entry.oid != worktree_entry.oid,
2786 modified_content: dirt & DIRTY_SUBMODULE_MODIFIED != 0,
2787 untracked_content: dirt & DIRTY_SUBMODULE_UNTRACKED != 0,
2788 }))
2789}
2790
2791pub(crate) fn status_sort_category(entry: &ShortStatusEntry) -> u8 {
2792 match (entry.index, entry.worktree) {
2793 (b'?', b'?') => 1,
2794 (b'!', b'!') => 2,
2795 _ => 0,
2796 }
2797}