gix 0.89.0

Interact with git repositories just like git would
Documentation
use std::sync::atomic::Ordering;

use crate::error::{ResultExt, message};
use gix_status::index_as_worktree::{Change, EntryStatus};

use crate::{
    Result,
    bstr::BString,
    config::cache::util::ApplyLeniencyDefault,
    status::{Platform, index_worktree, index_worktree::BuiltinSubmoduleStatus},
    worktree::IndexPersistedOrInMemory,
};

pub(super) mod types;
use types::{ApplyChange, Item, Iter, Outcome};

/// Lifecycle
impl<Progress> Platform<'_, Progress>
where
    Progress: gix_utils::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<Item = BString>) -> Result<Iter> {
        let index = match self.index {
            None => IndexPersistedOrInMemory::Persisted(self.repo.index_or_empty()?),
            Some(index) => index,
        };

        let obtain_tree_id = || -> Result<Option<gix_hash::ObjectId>> {
            Ok(match self.head_tree {
                Some(None) => Some(
                    self.repo
                        .head_tree_id_or_empty()
                        .or_raise(|| message("Could not obtain the tree id pointed to by `HEAD`"))?
                        .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(crate::trace::in_thread({
                        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 || -> std::result::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(action)
                                },
                            )
                        }
                    }))
                    .or_raise(|| message("Failed to spawn producer thread"))?
                    .into()
            } else {
                None
            };
            let mut collect = Collect { tx };
            let join_index_worktree = std::thread::Builder::new()
                .name("gix::status::index_worktree::producer".into())
                .spawn(crate::trace::in_thread({
                    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<_> {
                        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,
                        })
                    }
                }))
                .or_raise(|| message("Failed to spawn producer thread"))?;

            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<BString> = 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(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)
        }
    }
}

impl Iterator for Iter {
    type Item = Result<Item>;

    fn next(&mut self) -> Option<Self::Item> {
        #[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)),
                        }
                    } 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)),
                    };
                }
            }
        }
        #[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<Outcome> {
        self.out.take()
    }
}

impl Iter {
    fn maybe_keep_index_change(&mut self, item: Item) -> Option<Item> {
        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<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.
        let item = Item::IndexWorktree(entry.into());
        #[cfg(feature = "parallel")]
        self.tx.send(item).ok();
        #[cfg(not(feature = "parallel"))]
        self.items.push(item);
    }
}