blob: d95ab5a243846f8672281a62b481d2fc18eee82f [file] [log] [blame]
use crate::bstr::{BStr, BString};
use crate::{config, Repository};
use gix_status::index_as_worktree::traits::{CompareBlobs, SubmoduleStatus};
use std::sync::atomic::AtomicBool;
/// The error returned by [Repository::index_worktree_status()].
#[derive(Debug, thiserror::Error)]
#[allow(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<gix_status::index_as_worktree_with_renames::Sorting>,
/// 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<crate::dirwalk::Options>,
/// 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<gix_diff::Rewrites>,
/// 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<usize>,
}
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.
#[allow(clippy::too_many_arguments)]
pub fn index_worktree_status<'index, T, U, E>(
&self,
index: &'index gix_index::State,
patterns: impl IntoIterator<Item = impl AsRef<BStr>>,
delegate: &mut impl gix_status::index_as_worktree_with_renames::VisitEntry<
'index,
ContentChange = T,
SubmoduleStatus = U,
>,
compare: impl CompareBlobs<Output = T> + Send + Clone,
submodule: impl SubmoduleStatus<Output = U, Error = E> + Send + Clone,
progress: &mut dyn gix_features::progress::Progress,
should_interrupt: &AtomicBool,
options: Options,
) -> Result<gix_status::index_as_worktree_with_renames::Outcome, Error>
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.work_dir().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 = crate::Pathspec::new(
self,
options
.dirwalk_options
.as_ref()
.map_or(false, |opts| opts.empty_patterns_match_prefix),
patterns,
true, /* inherit ignore case */
|| Ok(attrs_and_excludes.clone()),
)?;
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 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()?,
},
dirwalk: options.dirwalk_options.map(Into::into),
rewrites: options.rewrites,
},
)?;
Ok(out)
}
}
/// 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<BString>,
}
///
#[allow(clippy::empty_docs)]
mod submodule_status {
use crate::bstr;
use crate::bstr::BStr;
use crate::status::index_worktree::BuiltinSubmoduleStatus;
use crate::status::Submodule;
use std::borrow::Cow;
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<Self, crate::submodule::modules::Error> {
let local_repo = repo.to_thread_local();
let submodule_paths = match local_repo.submodules()? {
Some(sm) => {
let mut v: Vec<_> = sm
.filter(|sm| sm.is_active().unwrap_or_default())
.filter_map(|sm| sm.path().ok().map(Cow::into_owned))
.collect();
v.sort();
v
}
None => Vec::new(),
};
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)]
#[allow(missing_docs)]
pub enum Error {
#[error(transparent)]
SubmoduleStatus(#[from] crate::submodule::status::Error),
#[error(transparent)]
IgnoreConfig(#[from] crate::submodule::config::Error),
}
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<Option<Self::Output>, 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().map_or(false, |path| path == rela_path)) else {
return Ok(None);
};
let (ignore, check_dirty) = match self.mode {
Submodule::AsConfigured { check_dirty } => (sm.ignore()?.unwrap_or_default(), 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 automatically excludes them from the status operation.
///
/// ### Index Changes
///
/// Changes to the index are collected and it's possible to write the index back using [iter::Outcome::write_changes()].
/// 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-interruptable, 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 {
#[cfg(feature = "parallel")]
#[allow(clippy::type_complexity)]
rx_and_join: Option<(
std::sync::mpsc::Receiver<iter::Item>,
std::thread::JoinHandle<Result<iter::Outcome, crate::status::index_worktree::Error>>,
)>,
#[cfg(feature = "parallel")]
should_interrupt: crate::status::OwnedOrStaticAtomicBool,
/// Without parallelization, the iterator has to buffer all changes in advance.
#[cfg(not(feature = "parallel"))]
items: std::vec::IntoIter<iter::Item>,
/// The outcome of the operation, only available once the operation has ended.
out: Option<iter::Outcome>,
/// The set of `(entry_index, change)` we extracted in order to potentially write back the index with the changes applied.
changes: Vec<(usize, iter::ApplyChange)>,
}
///
#[allow(clippy::empty_docs)]
pub mod iter {
use crate::bstr::{BStr, BString};
use crate::config::cache::util::ApplyLeniencyDefault;
use crate::status::index_worktree::{iter, BuiltinSubmoduleStatus};
use crate::status::{index_worktree, Platform};
use crate::worktree::IndexPersistedOrInMemory;
use gix_status::index_as_worktree::{Change, EntryStatus};
pub use gix_status::index_as_worktree_with_renames::Summary;
pub(super) enum ApplyChange {
SetSizeToZero,
NewStat(crate::index::entry::Stat),
}
/// The data the thread sends over to the receiving iterator.
pub struct Outcome {
/// The outcome of the index-to-worktree comparison operation.
pub index_worktree: gix_status::index_as_worktree_with_renames::Outcome,
/// The index that was used for the operation.
pub index: crate::worktree::IndexPersistedOrInMemory,
skip_hash: bool,
changes: Option<Vec<(usize, iter::ApplyChange)>>,
}
impl Outcome {
/// Returns `true` if the index has received currently unapplied changes that *should* be written back.
///
/// If they are not written back, subsequent `status` operations will take longer to complete, whereas the
/// additional work can be prevented by writing the changes back to the index.
pub fn has_changes(&self) -> bool {
self.changes.as_ref().map_or(false, |changes| !changes.is_empty())
}
/// Write the changes if there are any back to the index file.
/// This can only be done once as the changes are consumed in the process, if there were any.
pub fn write_changes(&mut self) -> Option<Result<(), gix_index::file::write::Error>> {
let _span = gix_features::trace::coarse!("gix::status::index_worktree::iter::Outcome::write_changes()");
let changes = self.changes.take()?;
let mut index = match &self.index {
IndexPersistedOrInMemory::Persisted(persisted) => (***persisted).clone(),
IndexPersistedOrInMemory::InMemory(index) => index.clone(),
};
let entries = index.entries_mut();
for (entry_index, change) in changes {
let entry = &mut entries[entry_index];
match change {
ApplyChange::SetSizeToZero => {
entry.stat.size = 0;
}
ApplyChange::NewStat(new_stat) => {
entry.stat = new_stat;
}
}
}
Some(index.write(crate::index::write::Options {
extensions: Default::default(),
skip_hash: self.skip_hash,
}))
}
}
/// 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].
///
/// 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<gix_dir::entry::Status>,
/// 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,
},
}
/// 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<gix_status::index_as_worktree_with_renames::RewriteSource<'index, (), SubmoduleStatus>>
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,
},
}
}
}
/// 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<(), SubmoduleStatus>,
},
/// 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<gix_dir::entry::Status>,
},
/// 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<gix_dir::entry::Status>,
/// The object id after the rename, specifically hashed in order to determine equality.
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<gix_diff::blob::DiffLineStats>,
/// 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,
},
}
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<Summary> {
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
}
}
})
}
}
impl<'index> From<gix_status::index_as_worktree_with_renames::Entry<'index, (), SubmoduleStatus>> 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;
/// The error returned by [Platform::into_index_worktree_iter()](crate::status::Platform::into_index_worktree_iter()).
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error(transparent)]
Index(#[from] crate::worktree::open_index::Error),
#[error("Failed to spawn producer thread")]
#[cfg(feature = "parallel")]
SpawnThread(#[source] std::io::Error),
#[error(transparent)]
#[cfg(not(feature = "parallel"))]
IndexWorktreeStatus(#[from] crate::status::index_worktree::Error),
#[error(transparent)]
ConfigSkipHash(#[from] crate::config::boolean::Error),
#[error(transparent)]
PrepareSubmodules(#[from] crate::submodule::modules::Error),
}
/// Lifecycle
impl<'repo, Progress> Platform<'repo, 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.
pub fn into_index_worktree_iter(self, patterns: Vec<BString>) -> Result<index_worktree::Iter, Error> {
let index = match self.index {
None => IndexPersistedOrInMemory::Persisted(self.repo.index_or_empty()?),
Some(index) => index,
};
let skip_hash = self
.repo
.config
.resolved
.boolean("index", None, "skipHash")
.map(|res| crate::config::tree::Index::SKIP_HASH.enrich_error(res))
.transpose()
.with_lenient_default(self.repo.config.lenient_config)?
.unwrap_or_default();
let should_interrupt = self.should_interrupt.clone().unwrap_or_default();
let submodule = BuiltinSubmoduleStatus::new(self.repo.clone().into_sync(), self.submodules)?;
#[cfg(feature = "parallel")]
{
let (tx, rx) = std::sync::mpsc::channel();
let mut collect = Collect { tx };
let join = std::thread::Builder::new()
.name("gix::status::index_worktree::iter::producer".into())
.spawn({
let repo = self.repo.clone().into_sync();
let options = self.index_worktree_options;
let should_interrupt = should_interrupt.clone();
let mut progress = self.progress;
move || -> Result<_, crate::status::index_worktree::Error> {
let repo = repo.to_thread_local();
let out = repo.index_worktree_status(
&index,
patterns,
&mut collect,
gix_status::index_as_worktree::traits::FastEq,
submodule,
&mut progress,
&should_interrupt,
options,
)?;
Ok(Outcome {
index_worktree: out,
index,
changes: None,
skip_hash,
})
}
})
.map_err(Error::SpawnThread)?;
Ok(super::Iter {
rx_and_join: Some((rx, join)),
should_interrupt,
changes: Vec::new(),
out: None,
})
}
#[cfg(not(feature = "parallel"))]
{
let mut collect = Collect { items: Vec::new() };
let repo = self.repo.clone().into_sync();
let options = self.index_worktree_options;
let mut progress = self.progress;
let repo = repo.to_thread_local();
let out = repo.index_worktree_status(
&index,
patterns,
&mut collect,
gix_status::index_as_worktree::traits::FastEq,
submodule,
&mut progress,
&should_interrupt,
options,
)?;
let mut out = Outcome {
index_worktree: out,
index,
changes: None,
skip_hash,
};
let mut iter = super::Iter {
items: Vec::new().into_iter(),
changes: Vec::new(),
out: None,
};
let items = collect
.items
.into_iter()
.filter_map(|item| iter.maybe_keep_index_change(item))
.collect::<Vec<_>>();
out.changes = (!iter.changes.is_empty()).then(|| std::mem::take(&mut iter.changes));
iter.items = items.into_iter();
iter.out = Some(out);
Ok(iter)
}
}
}
impl Iterator for super::Iter {
type Item = Result<Item, index_worktree::Error>;
fn next(&mut self) -> Option<Self::Item> {
#[cfg(feature = "parallel")]
loop {
let (rx, _join) = self.rx_and_join.as_ref()?;
match rx.recv().ok() {
Some(item) => {
if let Some(item) = self.maybe_keep_index_change(item) {
break Some(Ok(item));
}
continue;
}
None => {
let (_rx, handle) = self.rx_and_join.take()?;
break match handle.join().expect("no panic") {
Ok(mut out) => {
out.changes = Some(std::mem::take(&mut self.changes));
self.out = Some(out);
None
}
Err(err) => Some(Err(err)),
};
}
}
}
#[cfg(not(feature = "parallel"))]
self.items.next().map(Ok)
}
}
/// 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 Outcome> {
self.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<Outcome> {
self.out.take()
}
}
impl super::Iter {
fn maybe_keep_index_change(&mut self, item: Item) -> Option<Item> {
let change = match item {
Item::Modification {
status: gix_status::index_as_worktree::EntryStatus::NeedsUpdate(stat),
entry_index,
..
} => (entry_index, ApplyChange::NewStat(stat)),
Item::Modification {
status:
gix_status::index_as_worktree::EntryStatus::Change(
gix_status::index_as_worktree::Change::Modification {
set_entry_stat_size_zero,
..
},
),
entry_index,
..
} if set_entry_stat_size_zero => (entry_index, ApplyChange::SetSizeToZero),
_ => return Some(item),
};
self.changes.push(change);
None
}
}
#[cfg(feature = "parallel")]
impl Drop for super::Iter {
fn drop(&mut self) {
crate::util::parallel_iter_drop(self.rx_and_join.take(), &self.should_interrupt);
}
}
struct Collect {
#[cfg(feature = "parallel")]
tx: std::sync::mpsc::Sender<Item>,
#[cfg(not(feature = "parallel"))]
items: Vec<Item>,
}
impl<'index> gix_status::index_as_worktree_with_renames::VisitEntry<'index> for Collect {
type ContentChange = <gix_status::index_as_worktree::traits::FastEq as gix_status::index_as_worktree::traits::CompareBlobs>::Output;
type SubmoduleStatus =
<BuiltinSubmoduleStatus as gix_status::index_as_worktree::traits::SubmoduleStatus>::Output;
fn visit_entry(
&mut self,
entry: gix_status::index_as_worktree_with_renames::Entry<
'index,
Self::ContentChange,
Self::SubmoduleStatus,
>,
) {
// NOTE: we assume that the receiver triggers interruption so the operation will stop if the receiver is down.
#[cfg(feature = "parallel")]
self.tx.send(entry.into()).ok();
#[cfg(not(feature = "parallel"))]
self.items.push(entry.into());
}
}
}