gix/status/
index_worktree.rs

1use std::sync::atomic::AtomicBool;
2
3use gix_status::index_as_worktree::traits::{CompareBlobs, SubmoduleStatus};
4
5use crate::{
6    bstr::{BStr, BString},
7    config, Repository,
8};
9
10/// The error returned by [Repository::index_worktree_status()].
11#[derive(Debug, thiserror::Error)]
12#[allow(missing_docs)]
13pub enum Error {
14    #[error("A working tree is required to perform a directory walk")]
15    MissingWorkDir,
16    #[error(transparent)]
17    AttributesAndExcludes(#[from] crate::repository::attributes::Error),
18    #[error(transparent)]
19    Pathspec(#[from] crate::pathspec::init::Error),
20    #[error(transparent)]
21    Prefix(#[from] gix_path::realpath::Error),
22    #[error(transparent)]
23    FilesystemOptions(#[from] config::boolean::Error),
24    #[error(transparent)]
25    IndexAsWorktreeWithRenames(#[from] gix_status::index_as_worktree_with_renames::Error),
26    #[error(transparent)]
27    StatOptions(#[from] config::stat_options::Error),
28    #[error(transparent)]
29    ResourceCache(#[from] crate::diff::resource_cache::Error),
30}
31
32/// Options for use with [Repository::index_worktree_status()].
33#[derive(Default, Debug, Clone, Copy, PartialEq)]
34pub struct Options {
35    /// The way all output should be sorted.
36    ///
37    /// If `None`, and depending on the `rewrites` field, output will be immediate but the output order
38    /// isn't determined, and may differ between two runs. `rewrites` also depend on the order of entries that
39    /// are presented to it, hence for deterministic results, sorting needs to be enabled.
40    ///
41    /// If `Some(_)`, all entries are collected beforehand, so they can be sorted before outputting any of them
42    /// to the user.
43    ///
44    /// If immediate output of entries in any order is desired, this should be `None`,
45    /// along with `rewrites` being `None` as well.
46    pub sorting: Option<gix_status::index_as_worktree_with_renames::Sorting>,
47    /// If not `None`, the options to configure the directory walk, determining how its results will look like.
48    ///
49    /// If `None`, only modification checks are performed.
50    ///
51    /// Can be instantiated with [Repository::dirwalk_options()].
52    pub dirwalk_options: Option<crate::dirwalk::Options>,
53    /// If `Some(_)`, along with `Some(_)` in `dirwalk_options`, rewrite tracking will be performed between the
54    /// index and the working tree.
55    /// Note that there is no git-configuration specific to index-worktree rename tracking.
56    /// When rewrite tracking is enabled, there will be a delay for some entries as they partake in the rename-analysis.
57    pub rewrites: Option<gix_diff::Rewrites>,
58    /// If set, don't use more than this amount of threads for the tracked modification check.
59    /// Otherwise, usually use as many threads as there are logical cores.
60    /// A value of 0 is interpreted as no-limit
61    pub thread_limit: Option<usize>,
62}
63
64impl Repository {
65    /// Obtain the status between the index and the worktree, involving modification checks
66    /// for all tracked files along with information about untracked (and posisbly ignored) files (if configured).
67    ///
68    /// * `index`
69    ///     - The index to use for modification checks, and to know which files are tacked when applying the dirwalk.
70    /// * `patterns`
71    ///     - Optional patterns to use to limit the paths to look at. If empty, all paths are considered.
72    /// * `delegate`
73    ///     - The sink for receiving all status data.
74    /// * `compare`
75    ///     - The implementations for fine-grained control over what happens if a hash must be recalculated.
76    /// * `submodule`
77    ///      - Control what kind of information to retrieve when a submodule is encountered while traversing the index.
78    /// * `progress`
79    ///     - A progress indication for index modification checks.
80    /// * `should_interrupt`
81    ///     - A flag to stop the whole operation.
82    /// * `options`
83    ///     - Additional configuration for all parts of the operation.
84    ///
85    /// ### Note
86    ///
87    /// This is a lower-level method, prefer the [`status`](Repository::status()) method for greater ease of use.
88    #[allow(clippy::too_many_arguments)]
89    pub fn index_worktree_status<'index, T, U, E>(
90        &self,
91        index: &'index gix_index::State,
92        patterns: impl IntoIterator<Item = impl AsRef<BStr>>,
93        delegate: &mut impl gix_status::index_as_worktree_with_renames::VisitEntry<
94            'index,
95            ContentChange = T,
96            SubmoduleStatus = U,
97        >,
98        compare: impl CompareBlobs<Output = T> + Send + Clone,
99        submodule: impl SubmoduleStatus<Output = U, Error = E> + Send + Clone,
100        progress: &mut dyn gix_features::progress::Progress,
101        should_interrupt: &AtomicBool,
102        options: Options,
103    ) -> Result<gix_status::index_as_worktree_with_renames::Outcome, Error>
104    where
105        T: Send + Clone,
106        U: Send + Clone,
107        E: std::error::Error + Send + Sync + 'static,
108    {
109        let _span = gix_trace::coarse!("gix::index_worktree_status");
110        let workdir = self.workdir().ok_or(Error::MissingWorkDir)?;
111        let attrs_and_excludes = self.attributes(
112            index,
113            crate::worktree::stack::state::attributes::Source::WorktreeThenIdMapping,
114            crate::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped,
115            None,
116        )?;
117        let pathspec =
118            self.index_worktree_status_pathspec::<Error>(patterns, index, options.dirwalk_options.as_ref())?;
119
120        let cwd = self.current_dir();
121        let git_dir_realpath = crate::path::realpath_opts(self.git_dir(), cwd, crate::path::realpath::MAX_SYMLINKS)?;
122        let fs_caps = self.filesystem_options()?;
123        let accelerate_lookup = fs_caps.ignore_case.then(|| index.prepare_icase_backing());
124        let resource_cache = crate::diff::resource_cache(
125            self,
126            gix_diff::blob::pipeline::Mode::ToGit,
127            attrs_and_excludes.inner,
128            gix_diff::blob::pipeline::WorktreeRoots {
129                old_root: None,
130                new_root: Some(workdir.to_owned()),
131            },
132        )?;
133
134        let out = gix_status::index_as_worktree_with_renames(
135            index,
136            workdir,
137            delegate,
138            compare,
139            submodule,
140            self.objects.clone().into_arc().expect("arc conversion always works"),
141            progress,
142            gix_status::index_as_worktree_with_renames::Context {
143                pathspec: pathspec.search,
144                resource_cache,
145                should_interrupt,
146                dirwalk: gix_status::index_as_worktree_with_renames::DirwalkContext {
147                    git_dir_realpath: git_dir_realpath.as_path(),
148                    current_dir: cwd,
149                    ignore_case_index_lookup: accelerate_lookup.as_ref(),
150                },
151            },
152            gix_status::index_as_worktree_with_renames::Options {
153                sorting: options.sorting,
154                object_hash: self.object_hash(),
155                tracked_file_modifications: gix_status::index_as_worktree::Options {
156                    fs: fs_caps,
157                    thread_limit: options.thread_limit,
158                    stat: self.stat_options()?,
159                },
160                dirwalk: options.dirwalk_options.map(Into::into),
161                rewrites: options.rewrites,
162            },
163        )?;
164        Ok(out)
165    }
166
167    pub(super) fn index_worktree_status_pathspec<E>(
168        &self,
169        patterns: impl IntoIterator<Item = impl AsRef<BStr>>,
170        index: &gix_index::State,
171        options: Option<&crate::dirwalk::Options>,
172    ) -> Result<crate::Pathspec<'_>, E>
173    where
174        E: From<crate::repository::attributes::Error> + From<crate::pathspec::init::Error>,
175    {
176        let empty_patterns_match_prefix = options.is_some_and(|opts| opts.empty_patterns_match_prefix);
177        let attrs_and_excludes = self.attributes(
178            index,
179            crate::worktree::stack::state::attributes::Source::WorktreeThenIdMapping,
180            crate::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped,
181            None,
182        )?;
183        Ok(crate::Pathspec::new(
184            self,
185            empty_patterns_match_prefix,
186            patterns,
187            true, /* inherit ignore case */
188            move || Ok(attrs_and_excludes.inner),
189        )?)
190    }
191}
192
193/// An implementation of a trait to use with [`Repository::index_worktree_status()`] to compute the submodule status
194/// using [Submodule::status()](crate::Submodule::status()).
195#[derive(Clone)]
196pub struct BuiltinSubmoduleStatus {
197    mode: crate::status::Submodule,
198    #[cfg(feature = "parallel")]
199    repo: crate::ThreadSafeRepository,
200    #[cfg(not(feature = "parallel"))]
201    git_dir: std::path::PathBuf,
202    submodule_paths: Vec<BString>,
203}
204
205///
206mod submodule_status {
207    use std::borrow::Cow;
208
209    use crate::config::cache::util::ApplyLeniency;
210    use crate::{
211        bstr,
212        bstr::BStr,
213        config,
214        status::{index_worktree::BuiltinSubmoduleStatus, Submodule},
215    };
216
217    impl BuiltinSubmoduleStatus {
218        /// Create a new instance from a `repo` and a `mode` to control how the submodule status will be obtained.
219        pub fn new(
220            repo: crate::ThreadSafeRepository,
221            mode: Submodule,
222        ) -> Result<Self, crate::submodule::modules::Error> {
223            let local_repo = repo.to_thread_local();
224            let submodule_paths = match local_repo.submodules() {
225                Ok(Some(sm)) => {
226                    let mut v: Vec<_> = sm.filter_map(|sm| sm.path().ok().map(Cow::into_owned)).collect();
227                    v.sort();
228                    v
229                }
230                Ok(None) => Vec::new(),
231                Err(err) => return Err(err),
232            };
233            Ok(Self {
234                mode,
235                #[cfg(feature = "parallel")]
236                repo,
237                #[cfg(not(feature = "parallel"))]
238                git_dir: local_repo.git_dir().to_owned(),
239                submodule_paths,
240            })
241        }
242    }
243
244    /// The error returned submodule status checks.
245    #[derive(Debug, thiserror::Error)]
246    #[allow(missing_docs)]
247    pub enum Error {
248        #[error(transparent)]
249        SubmoduleStatus(#[from] crate::submodule::status::Error),
250        #[error(transparent)]
251        IgnoreConfig(#[from] crate::submodule::config::Error),
252        #[error(transparent)]
253        DiffSubmoduleIgnoreConfig(#[from] config::key::GenericErrorWithValue),
254    }
255
256    impl gix_status::index_as_worktree::traits::SubmoduleStatus for BuiltinSubmoduleStatus {
257        type Output = crate::submodule::Status;
258        type Error = Error;
259
260        fn status(&mut self, _entry: &gix_index::Entry, rela_path: &BStr) -> Result<Option<Self::Output>, Self::Error> {
261            use bstr::ByteSlice;
262            if self
263                .submodule_paths
264                .binary_search_by(|path| path.as_bstr().cmp(rela_path))
265                .is_err()
266            {
267                return Ok(None);
268            }
269            #[cfg(feature = "parallel")]
270            let repo = self.repo.to_thread_local();
271            #[cfg(not(feature = "parallel"))]
272            let Ok(repo) = crate::open(&self.git_dir) else {
273                return Ok(None);
274            };
275            let Ok(Some(mut submodules)) = repo.submodules() else {
276                return Ok(None);
277            };
278            let Some(sm) = submodules.find(|sm| sm.path().is_ok_and(|path| path == rela_path)) else {
279                return Ok(None);
280            };
281            let (ignore, check_dirty) = match self.mode {
282                Submodule::AsConfigured { check_dirty } => {
283                    // diff.ignoreSubmodules is the global setting, and if it exists, it overrides the submodule's own ignore setting.
284                    let global_ignore = repo
285                        .config_snapshot()
286                        .string(&config::tree::Diff::IGNORE_SUBMODULES)
287                        .map(|value| config::tree::Diff::IGNORE_SUBMODULES.try_into_ignore(value))
288                        .transpose()
289                        .with_leniency(repo.config.lenient_config)?;
290                    if let Some(ignore) = global_ignore {
291                        (ignore, check_dirty)
292                    } else {
293                        // If no global ignore is set, use the submodule's ignore setting.
294                        let ignore = sm.ignore()?.unwrap_or_default();
295                        (ignore, check_dirty)
296                    }
297                }
298                Submodule::Given { ignore, check_dirty } => (ignore, check_dirty),
299            };
300            let status = sm.status(ignore, check_dirty)?;
301            Ok(status.is_dirty().and_then(|dirty| dirty.then_some(status)))
302        }
303    }
304}
305
306/// An iterator for changes between the index and the worktree.
307///
308/// Note that depending on the underlying configuration, there might be a significant delay until the first
309/// item is received due to the buffering necessary to perform rename tracking and/or sorting.
310///
311/// ### Submodules
312///
313/// Note that submodules can be set to 'inactive', which will not exclude them from the status operation, similar to
314/// how `git status` includes them.
315///
316/// ### Index Changes
317///
318/// Changes to the index are collected and it's possible to write the index back using [Outcome::write_changes()](crate::status::Outcome).
319/// Note that these changes are not observable, they will always be kept.
320///
321/// ### Parallel Operation
322///
323/// Note that without the `parallel` feature, the iterator becomes 'serial', which means all status will be computed in advance
324/// and it's non-interruptible, yielding worse performance for is-dirty checks for instance as interruptions won't happen.
325/// It's a crutch that is just there to make single-threaded applications possible at all, as it's not really an iterator
326/// anymore. If this matters, better run [Repository::index_worktree_status()] by hand as it provides all control one would need,
327/// just not as an iterator.
328///
329/// Also, even with `parallel` set, the first call to `next()` will block until there is an item available, without a chance
330/// to interrupt unless [`status::Platform::should_interrupt_*()`](crate::status::Platform::should_interrupt_shared()) was
331/// configured.
332pub struct Iter {
333    inner: crate::status::Iter,
334}
335
336/// The item produced by the iterator
337#[derive(Clone, PartialEq, Debug)]
338pub enum Item {
339    /// A tracked file was modified, and index-specific information is passed.
340    Modification {
341        /// The entry with modifications.
342        entry: gix_index::Entry,
343        /// The index of the `entry` for lookup in [`gix_index::State::entries()`] - useful to look at neighbors.
344        entry_index: usize,
345        /// The repository-relative path of the entry.
346        rela_path: BString,
347        /// The computed status of the entry.
348        status: gix_status::index_as_worktree::EntryStatus<(), crate::submodule::Status>,
349    },
350    /// An entry returned by the directory walk, without any relation to the index.
351    ///
352    /// This can happen if ignored files are returned as well, or if rename-tracking is disabled.
353    DirectoryContents {
354        /// The entry found during the disk traversal.
355        entry: gix_dir::Entry,
356        /// `collapsed_directory_status` is `Some(dir_status)` if this `entry` was part of a directory with the given
357        /// `dir_status` that wasn't the same as the one of `entry` and if [gix_dir::walk::Options::emit_collapsed] was
358        /// [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch).
359        /// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All).
360        collapsed_directory_status: Option<gix_dir::entry::Status>,
361    },
362    /// The rewrite tracking discovered a match between a deleted and added file, and considers them equal enough,
363    /// depending on the tracker settings.
364    ///
365    /// Note that the source of the rewrite is always the index as it detects the absence of entries, something that
366    /// can't be done during a directory walk.
367    Rewrite {
368        /// The source of the rewrite operation.
369        source: RewriteSource,
370        /// The untracked entry found during the disk traversal, the destination of the rewrite.
371        ///
372        /// Note that its [`rela_path`](gix_dir::EntryRef::rela_path) is the destination of the rewrite, and the current
373        /// location of the entry.
374        dirwalk_entry: gix_dir::Entry,
375        /// `collapsed_directory_status` is `Some(dir_status)` if this `dirwalk_entry` was part of a directory with the given
376        /// `dir_status` that wasn't the same as the one of `dirwalk_entry` and if [gix_dir::walk::Options::emit_collapsed] was
377        /// [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch).
378        /// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All).
379        dirwalk_entry_collapsed_directory_status: Option<gix_dir::entry::Status>,
380        /// The object id as it would appear if the entry was written to the object database, specifically hashed in order to determine equality.
381        /// Note that it doesn't (necessarily) exist in the object database, and may be [null](gix_hash::ObjectId::null) if no hashing
382        /// was performed.
383        dirwalk_entry_id: gix_hash::ObjectId,
384        /// It's `None` if the 'source.id' is equal to `dirwalk_entry_id`, as identity made an actual diff computation unnecessary.
385        /// Otherwise, and if enabled, it's `Some(stats)` to indicate how similar both entries were.
386        diff: Option<gix_diff::blob::DiffLineStats>,
387        /// If true, this rewrite is created by copy, and 'source.id' is pointing to its source.
388        /// Otherwise, it's a rename, and 'source.id' points to a deleted object,
389        /// as renames are tracked as deletions and additions of the same or similar content.
390        copy: bool,
391    },
392}
393
394/// Either an index entry for renames or another directory entry in case of copies.
395#[derive(Clone, PartialEq, Debug)]
396pub enum RewriteSource {
397    /// The source originates in the index and is detected as missing in the working tree.
398    /// This can also happen for copies.
399    RewriteFromIndex {
400        /// The entry that is the source of the rewrite, which means it was removed on disk,
401        /// equivalent to [Change::Removed](gix_status::index_as_worktree::Change::Removed).
402        ///
403        /// Note that the [entry-id](gix_index::Entry::id) is the content-id of the source of the rewrite.
404        source_entry: gix_index::Entry,
405        /// The index of the `source_entry` for lookup in [`gix_index::State::entries()`] - useful to look at neighbors.
406        source_entry_index: usize,
407        /// The repository-relative path of the `source_entry`.
408        source_rela_path: BString,
409        /// The computed status of the `source_entry`.
410        source_status: gix_status::index_as_worktree::EntryStatus<(), crate::submodule::Status>,
411    },
412    /// This source originates in the directory tree and is always the source of copies.
413    CopyFromDirectoryEntry {
414        /// The source of the copy operation, which is also an entry of the directory walk.
415        ///
416        /// Note that its [`rela_path`](gix_dir::EntryRef::rela_path) is the source of the rewrite.
417        source_dirwalk_entry: gix_dir::Entry,
418        /// `collapsed_directory_status` is `Some(dir_status)` if this `source_dirwalk_entry` was part of a directory with the given
419        /// `dir_status` that wasn't the same as the one of `source_dirwalk_entry` and
420        /// if [gix_dir::walk::Options::emit_collapsed] was [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch).
421        /// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All).
422        source_dirwalk_entry_collapsed_directory_status: Option<gix_dir::entry::Status>,
423        /// The object id as it would appear if the entry was written to the object database.
424        /// It's the same as [`dirwalk_entry_id`](Item::Rewrite), or `diff` is `Some(_)` to indicate that the copy
425        /// was determined by similarity, not by content equality.
426        source_dirwalk_entry_id: gix_hash::ObjectId,
427    },
428}
429
430///
431pub mod iter {
432    use gix_status::index_as_worktree::{Change, EntryStatus};
433    pub use gix_status::index_as_worktree_with_renames::Summary;
434
435    use super::{Item, RewriteSource};
436    use crate::{
437        bstr::{BStr, BString},
438        status::{index_worktree, Platform},
439    };
440
441    /// Access
442    impl RewriteSource {
443        /// The repository-relative path of this source.
444        pub fn rela_path(&self) -> &BStr {
445            match self {
446                RewriteSource::RewriteFromIndex { source_rela_path, .. } => source_rela_path.as_ref(),
447                RewriteSource::CopyFromDirectoryEntry {
448                    source_dirwalk_entry, ..
449                } => source_dirwalk_entry.rela_path.as_ref(),
450            }
451        }
452    }
453
454    impl<'index> From<gix_status::index_as_worktree_with_renames::RewriteSource<'index, (), SubmoduleStatus>>
455        for RewriteSource
456    {
457        fn from(value: gix_status::index_as_worktree_with_renames::RewriteSource<'index, (), SubmoduleStatus>) -> Self {
458            match value {
459                gix_status::index_as_worktree_with_renames::RewriteSource::RewriteFromIndex {
460                    index_entries: _,
461                    source_entry,
462                    source_entry_index,
463                    source_rela_path,
464                    source_status,
465                } => RewriteSource::RewriteFromIndex {
466                    source_entry: source_entry.clone(),
467                    source_entry_index,
468                    source_rela_path: source_rela_path.to_owned(),
469                    source_status,
470                },
471                gix_status::index_as_worktree_with_renames::RewriteSource::CopyFromDirectoryEntry {
472                    source_dirwalk_entry,
473                    source_dirwalk_entry_collapsed_directory_status,
474                    source_dirwalk_entry_id,
475                } => RewriteSource::CopyFromDirectoryEntry {
476                    source_dirwalk_entry,
477                    source_dirwalk_entry_collapsed_directory_status,
478                    source_dirwalk_entry_id,
479                },
480            }
481        }
482    }
483
484    impl Item {
485        /// Return a simplified summary of the item as digest of its status, or `None` if this item is
486        /// created from the directory walk and is *not untracked*, or if it is merely to communicate
487        /// a needed update to the index entry.
488        pub fn summary(&self) -> Option<Summary> {
489            use gix_status::index_as_worktree_with_renames::Summary::*;
490            Some(match self {
491                Item::Modification { status, .. } => match status {
492                    EntryStatus::Conflict { .. } => Conflict,
493                    EntryStatus::Change(change) => match change {
494                        Change::Removed => Removed,
495                        Change::Type { .. } => TypeChange,
496                        Change::Modification { .. } | Change::SubmoduleModification(_) => Modified,
497                    },
498                    EntryStatus::NeedsUpdate(_) => return None,
499                    EntryStatus::IntentToAdd => IntentToAdd,
500                },
501                Item::DirectoryContents { entry, .. } => {
502                    if matches!(entry.status, gix_dir::entry::Status::Untracked) {
503                        Added
504                    } else {
505                        return None;
506                    }
507                }
508                Item::Rewrite { copy, .. } => {
509                    if *copy {
510                        Copied
511                    } else {
512                        Renamed
513                    }
514                }
515            })
516        }
517
518        /// The repository-relative path of the entry contained in this item.
519        pub fn rela_path(&self) -> &BStr {
520            match self {
521                Item::Modification { rela_path, .. } => rela_path.as_ref(),
522                Item::DirectoryContents { entry, .. } => entry.rela_path.as_ref(),
523                Item::Rewrite { dirwalk_entry, .. } => dirwalk_entry.rela_path.as_ref(),
524            }
525        }
526    }
527
528    impl<'index> From<gix_status::index_as_worktree_with_renames::Entry<'index, (), SubmoduleStatus>> for Item {
529        fn from(value: gix_status::index_as_worktree_with_renames::Entry<'index, (), SubmoduleStatus>) -> Self {
530            match value {
531                gix_status::index_as_worktree_with_renames::Entry::Modification {
532                    entries: _,
533                    entry,
534                    entry_index,
535                    rela_path,
536                    status,
537                } => Item::Modification {
538                    entry: entry.clone(),
539                    entry_index,
540                    rela_path: rela_path.to_owned(),
541                    status,
542                },
543                gix_status::index_as_worktree_with_renames::Entry::DirectoryContents {
544                    entry,
545                    collapsed_directory_status,
546                } => Item::DirectoryContents {
547                    entry,
548                    collapsed_directory_status,
549                },
550                gix_status::index_as_worktree_with_renames::Entry::Rewrite {
551                    source,
552                    dirwalk_entry,
553                    dirwalk_entry_collapsed_directory_status,
554                    dirwalk_entry_id,
555                    diff,
556                    copy,
557                } => Item::Rewrite {
558                    source: source.into(),
559                    dirwalk_entry,
560                    dirwalk_entry_collapsed_directory_status,
561                    dirwalk_entry_id,
562                    diff,
563                    copy,
564                },
565            }
566        }
567    }
568
569    type SubmoduleStatus = crate::submodule::Status;
570
571    /// Lifecycle
572    impl<Progress> Platform<'_, Progress>
573    where
574        Progress: gix_features::progress::Progress,
575    {
576        /// Turn the platform into an iterator for changes between the index and the working tree.
577        ///
578        /// * `patterns`
579        ///     - Optional patterns to use to limit the paths to look at. If empty, all paths are considered.
580        #[doc(alias = "diff_index_to_workdir", alias = "git2")]
581        pub fn into_index_worktree_iter(
582            mut self,
583            patterns: impl IntoIterator<Item = BString>,
584        ) -> Result<index_worktree::Iter, crate::status::into_iter::Error> {
585            // deactivate the tree-iteration
586            self.head_tree = None;
587            Ok(index_worktree::Iter {
588                inner: self.into_iter(patterns)?,
589            })
590        }
591    }
592
593    impl Iterator for super::Iter {
594        type Item = Result<Item, index_worktree::Error>;
595
596        fn next(&mut self) -> Option<Self::Item> {
597            self.inner.next().map(|res| {
598                res.map(|item| match item {
599                    crate::status::Item::IndexWorktree(item) => item,
600                    crate::status::Item::TreeIndex(_) => unreachable!("BUG: we deactivated this kind of traversal"),
601                })
602                .map_err(|err| match err {
603                    crate::status::iter::Error::IndexWorktree(err) => err,
604                    crate::status::iter::Error::TreeIndex(_) => {
605                        unreachable!("BUG: we deactivated this kind of traversal")
606                    }
607                })
608            })
609        }
610    }
611
612    /// Access
613    impl super::Iter {
614        /// Return the outcome of the iteration, or `None` if the iterator isn't fully consumed.
615        pub fn outcome_mut(&mut self) -> Option<&mut crate::status::Outcome> {
616            self.inner.out.as_mut()
617        }
618
619        /// Turn the iterator into the iteration outcome, which is `None` on error or if the iteration
620        /// isn't complete.
621        pub fn into_outcome(mut self) -> Option<crate::status::Outcome> {
622            self.inner.out.take()
623        }
624    }
625}