use std::sync::atomic::AtomicBool; use crate::{ Repository, bstr::{BStr, BString}, config, config::cache::util::ApplyLeniencyDefault, }; use gix_status::index_as_worktree::traits::{CompareBlobs, SubmoduleStatus}; /// The error returned by [Repository::index_worktree_status()]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] pub enum Error { #[error("A working tree is required to perform a directory walk")] MissingWorkDir, #[error(transparent)] AttributesAndExcludes(#[from] crate::repository::attributes::Error), #[error(transparent)] Pathspec(#[from] crate::pathspec::init::Error), #[error(transparent)] Prefix(#[from] gix_path::realpath::Error), #[error(transparent)] FilesystemOptions(#[from] config::boolean::Error), #[error(transparent)] IndexAsWorktreeWithRenames(#[from] gix_status::index_as_worktree_with_renames::Error), #[error(transparent)] StatOptions(#[from] config::stat_options::Error), #[error(transparent)] ResourceCache(#[from] crate::diff::resource_cache::Error), } /// Options for use with [Repository::index_worktree_status()]. #[derive(Default, Debug, Clone, Copy, PartialEq)] pub struct Options { /// The way all output should be sorted. /// /// If `None`, and depending on the `rewrites` field, output will be immediate but the output order /// isn't determined, and may differ between two runs. `rewrites` also depend on the order of entries that /// are presented to it, hence for deterministic results, sorting needs to be enabled. /// /// If `Some(_)`, all entries are collected beforehand, so they can be sorted before outputting any of them /// to the user. /// /// If immediate output of entries in any order is desired, this should be `None`, /// along with `rewrites` being `None` as well. pub sorting: Option, /// If not `None`, the options to configure the directory walk, determining how its results will look like. /// /// If `None`, only modification checks are performed. /// /// Can be instantiated with [Repository::dirwalk_options()]. pub dirwalk_options: Option, /// If `Some(_)`, along with `Some(_)` in `dirwalk_options`, rewrite tracking will be performed between the /// index and the working tree. /// Note that there is no git-configuration specific to index-worktree rename tracking. /// When rewrite tracking is enabled, there will be a delay for some entries as they partake in the rename-analysis. pub rewrites: Option, /// If set, don't use more than this amount of threads for the tracked modification check. /// Otherwise, usually use as many threads as there are logical cores. /// A value of 0 is interpreted as no-limit pub thread_limit: Option, } impl Repository { /// Obtain the status between the index and the worktree, involving modification checks /// for all tracked files along with information about untracked (and posisbly ignored) files (if configured). /// /// * `index` /// - The index to use for modification checks, and to know which files are tacked when applying the dirwalk. /// * `patterns` /// - Optional patterns to use to limit the paths to look at. If empty, all paths are considered. /// * `delegate` /// - The sink for receiving all status data. /// * `compare` /// - The implementations for fine-grained control over what happens if a hash must be recalculated. /// * `submodule` /// - Control what kind of information to retrieve when a submodule is encountered while traversing the index. /// * `progress` /// - A progress indication for index modification checks. /// * `should_interrupt` /// - A flag to stop the whole operation. /// * `options` /// - Additional configuration for all parts of the operation. /// /// ### Note /// /// This is a lower-level method, prefer the [`status`](Repository::status()) method for greater ease of use. #[expect(clippy::too_many_arguments)] pub fn index_worktree_status<'index, T, U, E>( &self, index: &'index gix_index::State, patterns: impl IntoIterator>, delegate: &mut impl gix_status::index_as_worktree_with_renames::VisitEntry< 'index, ContentChange = T, SubmoduleStatus = U, >, compare: impl CompareBlobs + Send + Clone, submodule: impl SubmoduleStatus + Send + Clone, progress: &mut dyn gix_features::progress::Progress, should_interrupt: &AtomicBool, options: Options, ) -> Result where T: Send + Clone, U: Send + Clone, E: std::error::Error + Send + Sync + 'static, { let _span = gix_trace::coarse!("gix::index_worktree_status"); let workdir = self.workdir().ok_or(Error::MissingWorkDir)?; let attrs_and_excludes = self.attributes( index, crate::worktree::stack::state::attributes::Source::WorktreeThenIdMapping, crate::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, None, )?; let pathspec = self.index_worktree_status_pathspec::(patterns, index, options.dirwalk_options.as_ref())?; let cwd = self.current_dir(); let git_dir_realpath = crate::path::realpath_opts(self.git_dir(), cwd, crate::path::realpath::MAX_SYMLINKS)?; let fs_caps = self.filesystem_options()?; let fscache = config::tree::Core::FS_CACHE .enrich_error(self.config.resolved.boolean(config::tree::Core::FS_CACHE)) .with_lenient_default(self.config.lenient_config)? // if unset, default to enabled on Windows. Good for missing Git installations that would turn it on by installation config .unwrap_or(cfg!(windows)); let accelerate_lookup = fs_caps.ignore_case.then(|| index.prepare_icase_backing()); let resource_cache = crate::diff::resource_cache( self, gix_diff::blob::pipeline::Mode::ToGit, attrs_and_excludes.inner, gix_diff::blob::pipeline::WorktreeRoots { old_root: None, new_root: Some(workdir.to_owned()), }, )?; let out = gix_status::index_as_worktree_with_renames( index, workdir, delegate, compare, submodule, self.objects.clone().into_arc().expect("arc conversion always works"), progress, gix_status::index_as_worktree_with_renames::Context { pathspec: pathspec.search, resource_cache, should_interrupt, dirwalk: gix_status::index_as_worktree_with_renames::DirwalkContext { git_dir_realpath: git_dir_realpath.as_path(), current_dir: cwd, ignore_case_index_lookup: accelerate_lookup.as_ref(), }, }, gix_status::index_as_worktree_with_renames::Options { sorting: options.sorting, object_hash: self.object_hash(), tracked_file_modifications: gix_status::index_as_worktree::Options { fs: fs_caps, thread_limit: options.thread_limit, stat: self.stat_options()?, fscache, }, fscache, dirwalk: options.dirwalk_options.map(Into::into), rewrites: options.rewrites, }, )?; Ok(out) } pub(super) fn index_worktree_status_pathspec( &self, patterns: impl IntoIterator>, index: &gix_index::State, options: Option<&crate::dirwalk::Options>, ) -> Result, E> where E: From + From, { let empty_patterns_match_prefix = options.is_some_and(|opts| opts.empty_patterns_match_prefix); let attrs_and_excludes = self.attributes( index, crate::worktree::stack::state::attributes::Source::WorktreeThenIdMapping, crate::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, None, )?; Ok(crate::Pathspec::new( self, empty_patterns_match_prefix, patterns, true, /* inherit ignore case */ move || Ok(attrs_and_excludes.inner), )?) } } /// An implementation of a trait to use with [`Repository::index_worktree_status()`] to compute the submodule status /// using [Submodule::status()](crate::Submodule::status()). #[derive(Clone)] pub struct BuiltinSubmoduleStatus { mode: crate::status::Submodule, #[cfg(feature = "parallel")] repo: crate::ThreadSafeRepository, #[cfg(not(feature = "parallel"))] git_dir: std::path::PathBuf, submodule_paths: Vec, } /// mod submodule_status { use crate::config::cache::util::ApplyLeniency; use crate::{ bstr, bstr::BStr, config, status::{Submodule, index_worktree::BuiltinSubmoduleStatus}, }; impl BuiltinSubmoduleStatus { /// Create a new instance from a `repo` and a `mode` to control how the submodule status will be obtained. pub fn new( repo: crate::ThreadSafeRepository, mode: Submodule, ) -> Result { let local_repo = repo.to_thread_local(); let submodule_paths = match local_repo.submodules() { Ok(Some(sm)) => { let mut v: Vec<_> = sm.filter_map(|sm| sm.path().ok()).collect(); v.sort(); v } Ok(None) => Vec::new(), Err(err) => return Err(err), }; Ok(Self { mode, #[cfg(feature = "parallel")] repo, #[cfg(not(feature = "parallel"))] git_dir: local_repo.git_dir().to_owned(), submodule_paths, }) } } /// The error returned submodule status checks. #[derive(Debug, thiserror::Error)] pub enum Error { #[error(transparent)] SubmoduleStatus(#[from] crate::submodule::status::Error), #[error(transparent)] IgnoreConfig(#[from] crate::submodule::config::Error), #[error(transparent)] DiffSubmoduleIgnoreConfig(#[from] config::key::GenericErrorWithValue), } impl gix_status::index_as_worktree::traits::SubmoduleStatus for BuiltinSubmoduleStatus { type Output = crate::submodule::Status; type Error = Error; fn status(&mut self, _entry: &gix_index::Entry, rela_path: &BStr) -> Result, Self::Error> { use bstr::ByteSlice; if self .submodule_paths .binary_search_by(|path| path.as_bstr().cmp(rela_path)) .is_err() { return Ok(None); } #[cfg(feature = "parallel")] let repo = self.repo.to_thread_local(); #[cfg(not(feature = "parallel"))] let Ok(repo) = crate::open(&self.git_dir) else { return Ok(None); }; let Ok(Some(mut submodules)) = repo.submodules() else { return Ok(None); }; let Some(sm) = submodules.find(|sm| sm.path().is_ok_and(|path| path == rela_path)) else { return Ok(None); }; let (ignore, check_dirty) = match self.mode { Submodule::AsConfigured { check_dirty } => { // diff.ignoreSubmodules is the global setting, and if it exists, it overrides the submodule's own ignore setting. let global_ignore = repo .config_snapshot() .string(config::tree::Diff::IGNORE_SUBMODULES) .map(|value| config::tree::Diff::IGNORE_SUBMODULES.try_into_ignore(value)) .transpose() .with_leniency(repo.config.lenient_config)?; if let Some(ignore) = global_ignore { (ignore, check_dirty) } else { // If no global ignore is set, use the submodule's ignore setting. let ignore = sm.ignore()?.unwrap_or_default(); (ignore, check_dirty) } } Submodule::Given { ignore, check_dirty } => (ignore, check_dirty), }; let status = sm.status(ignore, check_dirty)?; Ok(status.is_dirty().and_then(|dirty| dirty.then_some(status))) } } } /// An iterator for changes between the index and the worktree. /// /// Note that depending on the underlying configuration, there might be a significant delay until the first /// item is received due to the buffering necessary to perform rename tracking and/or sorting. /// /// ### Submodules /// /// Note that submodules can be set to 'inactive', which will not exclude them from the status operation, similar to /// how `git status` includes them. /// /// ### Index Changes /// /// Changes to the index are collected and it's possible to write the index back using [Outcome::write_changes()](crate::status::Outcome). /// Note that these changes are not observable, they will always be kept. /// /// ### Parallel Operation /// /// Note that without the `parallel` feature, the iterator becomes 'serial', which means all status will be computed in advance /// and it's non-interruptible, yielding worse performance for is-dirty checks for instance as interruptions won't happen. /// It's a crutch that is just there to make single-threaded applications possible at all, as it's not really an iterator /// anymore. If this matters, better run [Repository::index_worktree_status()] by hand as it provides all control one would need, /// just not as an iterator. /// /// Also, even with `parallel` set, the first call to `next()` will block until there is an item available, without a chance /// to interrupt unless [`status::Platform::should_interrupt_*()`](crate::status::Platform::should_interrupt_shared()) was /// configured. pub struct Iter { inner: crate::status::Iter, } /// The item produced by the iterator #[derive(Clone, PartialEq, Debug)] pub enum Item { /// A tracked file was modified, and index-specific information is passed. Modification { /// The entry with modifications. entry: gix_index::Entry, /// The index of the `entry` for lookup in [`gix_index::State::entries()`] - useful to look at neighbors. entry_index: usize, /// The repository-relative path of the entry. rela_path: BString, /// The computed status of the entry. status: gix_status::index_as_worktree::EntryStatus<(), crate::submodule::Status>, }, /// An entry returned by the directory walk, without any relation to the index. /// /// This can happen if ignored files are returned as well, or if rename-tracking is disabled. DirectoryContents { /// The entry found during the disk traversal. entry: gix_dir::Entry, /// `collapsed_directory_status` is `Some(dir_status)` if this `entry` was part of a directory with the given /// `dir_status` that wasn't the same as the one of `entry` and if [gix_dir::walk::Options::emit_collapsed] was /// [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch). /// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All). collapsed_directory_status: Option, }, /// The rewrite tracking discovered a match between a deleted and added file, and considers them equal enough, /// depending on the tracker settings. /// /// Note that the source of the rewrite is always the index as it detects the absence of entries, something that /// can't be done during a directory walk. Rewrite { /// The source of the rewrite operation. source: RewriteSource, /// The untracked entry found during the disk traversal, the destination of the rewrite. /// /// Note that its [`rela_path`](gix_dir::EntryRef::rela_path) is the destination of the rewrite, and the current /// location of the entry. dirwalk_entry: gix_dir::Entry, /// `collapsed_directory_status` is `Some(dir_status)` if this `dirwalk_entry` was part of a directory with the given /// `dir_status` that wasn't the same as the one of `dirwalk_entry` and if [gix_dir::walk::Options::emit_collapsed] was /// [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch). /// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All). dirwalk_entry_collapsed_directory_status: Option, /// The object id as it would appear if the entry was written to the object database, specifically hashed in order to determine equality. /// Note that it doesn't (necessarily) exist in the object database, and may be [null](gix_hash::ObjectId::null) if no hashing /// was performed. dirwalk_entry_id: gix_hash::ObjectId, /// It's `None` if the 'source.id' is equal to `dirwalk_entry_id`, as identity made an actual diff computation unnecessary. /// Otherwise, and if enabled, it's `Some(stats)` to indicate how similar both entries were. diff: Option, /// If true, this rewrite is created by copy, and 'source.id' is pointing to its source. /// Otherwise, it's a rename, and 'source.id' points to a deleted object, /// as renames are tracked as deletions and additions of the same or similar content. copy: bool, }, } /// Either an index entry for renames or another directory entry in case of copies. #[derive(Clone, PartialEq, Debug)] pub enum RewriteSource { /// The source originates in the index and is detected as missing in the working tree. /// This can also happen for copies. RewriteFromIndex { /// The entry that is the source of the rewrite, which means it was removed on disk, /// equivalent to [Change::Removed](gix_status::index_as_worktree::Change::Removed). /// /// Note that the [entry-id](gix_index::Entry::id) is the content-id of the source of the rewrite. source_entry: gix_index::Entry, /// The index of the `source_entry` for lookup in [`gix_index::State::entries()`] - useful to look at neighbors. source_entry_index: usize, /// The repository-relative path of the `source_entry`. source_rela_path: BString, /// The computed status of the `source_entry`. source_status: gix_status::index_as_worktree::EntryStatus<(), crate::submodule::Status>, }, /// This source originates in the directory tree and is always the source of copies. CopyFromDirectoryEntry { /// The source of the copy operation, which is also an entry of the directory walk. /// /// Note that its [`rela_path`](gix_dir::EntryRef::rela_path) is the source of the rewrite. source_dirwalk_entry: gix_dir::Entry, /// `collapsed_directory_status` is `Some(dir_status)` if this `source_dirwalk_entry` was part of a directory with the given /// `dir_status` that wasn't the same as the one of `source_dirwalk_entry` and /// if [gix_dir::walk::Options::emit_collapsed] was [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch). /// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All). source_dirwalk_entry_collapsed_directory_status: Option, /// The object id as it would appear if the entry was written to the object database. /// It's the same as [`dirwalk_entry_id`](Item::Rewrite), or `diff` is `Some(_)` to indicate that the copy /// was determined by similarity, not by content equality. source_dirwalk_entry_id: gix_hash::ObjectId, }, } /// pub mod iter { use gix_status::index_as_worktree::{Change, EntryStatus}; pub use gix_status::index_as_worktree_with_renames::Summary; use super::{Item, RewriteSource}; use crate::{ bstr::{BStr, BString}, status::{Platform, index_worktree}, }; /// Access impl RewriteSource { /// The repository-relative path of this source. pub fn rela_path(&self) -> &BStr { match self { RewriteSource::RewriteFromIndex { source_rela_path, .. } => source_rela_path.as_ref(), RewriteSource::CopyFromDirectoryEntry { source_dirwalk_entry, .. } => source_dirwalk_entry.rela_path.as_ref(), } } } impl<'index> From> for RewriteSource { fn from(value: gix_status::index_as_worktree_with_renames::RewriteSource<'index, (), SubmoduleStatus>) -> Self { match value { gix_status::index_as_worktree_with_renames::RewriteSource::RewriteFromIndex { index_entries: _, source_entry, source_entry_index, source_rela_path, source_status, } => RewriteSource::RewriteFromIndex { source_entry: source_entry.clone(), source_entry_index, source_rela_path: source_rela_path.to_owned(), source_status, }, gix_status::index_as_worktree_with_renames::RewriteSource::CopyFromDirectoryEntry { source_dirwalk_entry, source_dirwalk_entry_collapsed_directory_status, source_dirwalk_entry_id, } => RewriteSource::CopyFromDirectoryEntry { source_dirwalk_entry, source_dirwalk_entry_collapsed_directory_status, source_dirwalk_entry_id, }, } } } impl Item { /// Return a simplified summary of the item as digest of its status, or `None` if this item is /// created from the directory walk and is *not untracked*, or if it is merely to communicate /// a needed update to the index entry. pub fn summary(&self) -> Option { use gix_status::index_as_worktree_with_renames::Summary::*; Some(match self { Item::Modification { status, .. } => match status { EntryStatus::Conflict { .. } => Conflict, EntryStatus::Change(change) => match change { Change::Removed => Removed, Change::Type { .. } => TypeChange, Change::Modification { .. } | Change::SubmoduleModification(_) => Modified, }, EntryStatus::NeedsUpdate(_) => return None, EntryStatus::IntentToAdd => IntentToAdd, }, Item::DirectoryContents { entry, .. } => { if matches!(entry.status, gix_dir::entry::Status::Untracked) { Added } else { return None; } } Item::Rewrite { copy, .. } => { if *copy { Copied } else { Renamed } } }) } /// The repository-relative path of the entry contained in this item. pub fn rela_path(&self) -> &BStr { match self { Item::Modification { rela_path, .. } => rela_path.as_ref(), Item::DirectoryContents { entry, .. } => entry.rela_path.as_ref(), Item::Rewrite { dirwalk_entry, .. } => dirwalk_entry.rela_path.as_ref(), } } } impl<'index> From> for Item { fn from(value: gix_status::index_as_worktree_with_renames::Entry<'index, (), SubmoduleStatus>) -> Self { match value { gix_status::index_as_worktree_with_renames::Entry::Modification { entries: _, entry, entry_index, rela_path, status, } => Item::Modification { entry: entry.clone(), entry_index, rela_path: rela_path.to_owned(), status, }, gix_status::index_as_worktree_with_renames::Entry::DirectoryContents { entry, collapsed_directory_status, } => Item::DirectoryContents { entry, collapsed_directory_status, }, gix_status::index_as_worktree_with_renames::Entry::Rewrite { source, dirwalk_entry, dirwalk_entry_collapsed_directory_status, dirwalk_entry_id, diff, copy, } => Item::Rewrite { source: source.into(), dirwalk_entry, dirwalk_entry_collapsed_directory_status, dirwalk_entry_id, diff, copy, }, } } } type SubmoduleStatus = crate::submodule::Status; /// Lifecycle impl Platform<'_, Progress> where Progress: gix_features::progress::Progress, { /// Turn the platform into an iterator for changes between the index and the working tree. /// /// * `patterns` /// - Optional patterns to use to limit the paths to look at. If empty, all paths are considered. #[doc(alias = "diff_index_to_workdir", alias = "git2")] pub fn into_index_worktree_iter( mut self, patterns: impl IntoIterator, ) -> Result { // deactivate the tree-iteration self.head_tree = None; Ok(index_worktree::Iter { inner: self.into_iter(patterns)?, }) } } impl Iterator for super::Iter { type Item = Result; fn next(&mut self) -> Option { self.inner.next().map(|res| { res.map(|item| match item { crate::status::Item::IndexWorktree(item) => item, crate::status::Item::TreeIndex(_) => unreachable!("BUG: we deactivated this kind of traversal"), }) .map_err(|err| match err { crate::status::iter::Error::IndexWorktree(err) => err, crate::status::iter::Error::TreeIndex(_) => { unreachable!("BUG: we deactivated this kind of traversal") } }) }) } } /// Access impl super::Iter { /// Return the outcome of the iteration, or `None` if the iterator isn't fully consumed. pub fn outcome_mut(&mut self) -> Option<&mut crate::status::Outcome> { self.inner.out.as_mut() } /// Turn the iterator into the iteration outcome, which is `None` on error or if the iteration /// isn't complete. pub fn into_outcome(mut self) -> Option { self.inner.out.take() } } }