Skip to main content

sley_worktree/
status.rs

1//! The `git status` engine: short-status streaming/collection, tracked-only fast paths, and untracked-status rows.
2//!
3//! Split out of `lib.rs` in the wave-47 mechanical refactor: a pure code move
4//! (no function body changed); all items are re-exported from `lib.rs`.
5use 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
332/// Compare one expected tracked entry to the worktree path named by `path`.
333///
334/// `path` is repository-relative and uses the platform path representation. For
335/// callers that already carry git's byte path form, use
336/// [`worktree_entry_state_by_git_path`].
337pub 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
365/// Compare one expected tracked entry to the worktree path named by a
366/// repository-relative git path (`/` separators, raw bytes).
367///
368/// The comparison uses the same clean-filter, symlink-target, gitlink, and
369/// racy-clean stat shortcut rules as [`stream_short_status_with_options`].
370pub 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    // Parse the index once: the stat cache lets the worktree walk skip
456    // re-hashing files whose stat proves they are unchanged since staging
457    // (git's racy-git shortcut). When HEAD matches the index, the status
458    // comparison can stream directly from the parsed index and avoid building a
459    // second path-sorted copy of every tracked entry.
460    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        // Honor the skip-worktree bit literally unless sparse-checkout is active
759        // and the file is present (git clears the bit then -> changes reported).
760        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        // Honor the skip-worktree bit literally unless sparse-checkout is active
858        // and the file is present (git clears the bit then -> changes reported).
859        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        // Honor the skip-worktree bit literally unless sparse-checkout is active
1031        // and the file is present (git clears the bit then -> changes reported).
1032        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    // Skip-worktree handling mirrors git's clear_skip_worktree_from_present_files:
2634    // when core.sparseCheckout is DISABLED the bit is honored literally, so the
2635    // worktree side is always clean regardless of the on-disk file (git's
2636    // run_diff_files skips ce_skip_worktree entries). When it is ENABLED, git
2637    // clears the bit for entries whose file is present, so a present file's
2638    // worktree changes ARE reported; only an absent file stays clean.
2639    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            // sparse-active + absent skip-worktree: bit stays set -> clean.
2659            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    // See tracked_only_stat_precheck: when core.sparseCheckout is disabled the
2688    // skip-worktree bit is honored literally (worktree side always clean); when
2689    // enabled, git clears the bit for present files so their changes ARE
2690    // reported, and only absent files stay clean.
2691    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            // sparse-active + absent skip-worktree: bit stays set -> clean.
2710            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}