use std::sync::atomic::Ordering; use gix_status::index_as_worktree::{Change, EntryStatus}; use crate::{ bstr::BString, config::cache::util::ApplyLeniencyDefault, status::{Platform, index_worktree, index_worktree::BuiltinSubmoduleStatus, tree_index}, worktree::IndexPersistedOrInMemory, }; pub(super) mod types; use types::{ApplyChange, Item, Iter, Outcome}; /// Lifecycle impl Platform<'_, Progress> where Progress: gix_features::progress::Progress, { /// Turn the platform into an iterator for changes between the head-tree and the index, and the index and the working tree, /// while optionally listing untracked and/or ignored files. /// /// * `patterns` /// - Optional patterns to use to limit the paths to look at. If empty, all paths are considered. /// /// ### Important: Undefined ordering /// /// When compiled with the `parallel` feature, three operations are run at once: /// /// * a dirwalk to find untracked and possibly ignored files /// * an entry-by-entry check to see which of the tracked files changed, and how /// * a tree-index comparison /// /// All of these generate distinct events which may now happen in any order, so consumers /// that are ordering dependent have to restore their desired order. /// /// This isn't feasible to do here as it would mean that returned items would have to be delayed, /// degrading performance for everyone who isn't order-dependent. #[doc(alias = "diff_index_to_workdir", alias = "git2")] pub fn into_iter( self, patterns: impl IntoIterator, ) -> Result { let index = match self.index { None => IndexPersistedOrInMemory::Persisted(self.repo.index_or_empty()?), Some(index) => index, }; let obtain_tree_id = || -> Result, crate::status::into_iter::Error> { Ok(match self.head_tree { Some(None) => Some(self.repo.head_tree_id_or_empty()?.into()), Some(Some(tree_id)) => Some(tree_id), None => None, }) }; let skip_hash = crate::config::tree::Index::SKIP_HASH .enrich_error(self.repo.config.resolved.boolean(crate::config::tree::Index::SKIP_HASH)) .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 patterns: Vec<_> = patterns.into_iter().collect(); let join_tree_index = if let Some(tree_id) = obtain_tree_id()? { std::thread::Builder::new() .name("gix::status::tree_index::producer".into()) .spawn({ let repo = self.repo.clone().into_sync(); let should_interrupt = should_interrupt.clone(); let tx = tx.clone(); let tree_index_renames = self.tree_index_renames; let index = index.clone(); let crate::Pathspec { repo: _, stack, search } = self .repo .index_worktree_status_pathspec::( &patterns, &index, self.index_worktree_options.dirwalk_options.as_ref(), )?; move || -> Result<_, _> { let repo = repo.to_thread_local(); let mut pathspec = crate::Pathspec { repo: &repo, stack, search, }; repo.tree_index_status( &tree_id, &index, Some(&mut pathspec), tree_index_renames, |change, _, _| { let action = if tx.send(change.into_owned().into()).is_err() || should_interrupt.load(Ordering::Acquire) { std::ops::ControlFlow::Break(()) } else { std::ops::ControlFlow::Continue(()) }; Ok::<_, std::convert::Infallible>(action) }, ) } }) .map_err(crate::status::into_iter::Error::SpawnThread)? .into() } else { None }; let mut collect = Collect { tx }; let join_index_worktree = std::thread::Builder::new() .name("gix::status::index_worktree::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<_, 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, tree_index: None, worktree_index: index, changes: None, skip_hash, }) } }) .map_err(crate::status::into_iter::Error::SpawnThread)?; Ok(Iter { rx_and_join: Some((rx, join_index_worktree, join_tree_index)), should_interrupt, index_changes: Vec::new(), out: None, }) } #[cfg(not(feature = "parallel"))] { let mut collect = Collect { items: Vec::new() }; let repo = self.repo; let options = self.index_worktree_options; let mut progress = self.progress; let patterns: Vec = patterns.into_iter().collect(); let (mut items, tree_index) = match obtain_tree_id()? { Some(tree_id) => { let mut pathspec = repo.index_worktree_status_pathspec::( &patterns, &index, self.index_worktree_options.dirwalk_options.as_ref(), )?; let mut items = Vec::new(); let tree_index = self.repo.tree_index_status( &tree_id, &index, Some(&mut pathspec), self.tree_index_renames, |change, _, _| { items.push(change.into_owned().into()); let action = if should_interrupt.load(Ordering::Acquire) { std::ops::ControlFlow::Break(()) } else { std::ops::ControlFlow::Continue(()) }; Ok::<_, std::convert::Infallible>(action) }, )?; (items, Some(tree_index)) } None => (Vec::new(), None), }; 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 iter = Iter { items: Vec::new().into_iter(), index_changes: Vec::new(), out: None, }; let mut out = Outcome { index_worktree: out, worktree_index: index, tree_index, changes: None, skip_hash, }; items.extend( collect .items .into_iter() .filter_map(|item| iter.maybe_keep_index_change(item)), ); out.changes = (!iter.index_changes.is_empty()).then(|| std::mem::take(&mut iter.index_changes)); iter.items = items.into_iter(); iter.out = Some(out); Ok(iter) } } } /// The error returned for each item returned by [`Iter`]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] pub enum Error { #[error(transparent)] IndexWorktree(#[from] index_worktree::Error), #[error(transparent)] TreeIndex(#[from] tree_index::Error), } impl Iterator for Iter { type Item = Result; fn next(&mut self) -> Option { #[cfg(feature = "parallel")] loop { let (rx, _join_worktree, _join_tree) = self.rx_and_join.as_ref()?; match rx.recv_timeout(std::time::Duration::from_millis(25)) { Ok(item) => { if let Some(item) = self.maybe_keep_index_change(item) { break Some(Ok(item)); } continue; } // NOTE: this isn't necessary when index::from-tree also supports interrupts. As it stands, // on big repositories it can go up to 500ms which aren't interruptible, so this is another // way to not wait for this. Once it can be interrupted, this won't be needed anymore. Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { if self.should_interrupt.load(Ordering::SeqCst) { return None; } } Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => { let (_rx, worktree_handle, tree_handle) = self.rx_and_join.take()?; let tree_index = if let Some(handle) = tree_handle { match handle.join().expect("no panic") { Ok(out) => Some(out), Err(err) => break Some(Err(err.into())), } } else { None }; break match worktree_handle.join().expect("no panic") { Ok(mut out) => { out.changes = Some(std::mem::take(&mut self.index_changes)); out.tree_index = tree_index; self.out = Some(out); None } Err(err) => Some(Err(err.into())), }; } } } #[cfg(not(feature = "parallel"))] self.items.next().map(Ok) } } /// Access impl 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 { self.out.take() } } impl Iter { fn maybe_keep_index_change(&mut self, item: Item) -> Option { match item { Item::IndexWorktree(index_worktree::Item::Modification { status: EntryStatus::NeedsUpdate(stat), entry_index, .. }) => { self.index_changes.push((entry_index, ApplyChange::NewStat(stat))); return None; } Item::IndexWorktree(index_worktree::Item::Modification { status: EntryStatus::Change(Change::Modification { set_entry_stat_size_zero, .. }), entry_index, .. }) if set_entry_stat_size_zero => { self.index_changes.push((entry_index, ApplyChange::SetSizeToZero)); } _ => {} } Some(item) } } #[cfg(feature = "parallel")] impl Drop for 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, #[cfg(not(feature = "parallel"))] items: Vec, } impl<'index> gix_status::index_as_worktree_with_renames::VisitEntry<'index> for Collect { type ContentChange = ::Output; type 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. let item = Item::IndexWorktree(entry.into()); #[cfg(feature = "parallel")] self.tx.send(item).ok(); #[cfg(not(feature = "parallel"))] self.items.push(item); } }