use std::sync::atomic::AtomicBool;
use gix_status::index_as_worktree::traits::{CompareBlobs, SubmoduleStatus};
use crate::{
bstr::{BStr, BString},
config, Repository,
};
#[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),
}
#[derive(Default, Debug, Clone, Copy, PartialEq)]
pub struct Options {
pub sorting: Option<gix_status::index_as_worktree_with_renames::Sorting>,
pub dirwalk_options: Option<crate::dirwalk::Options>,
pub rewrites: Option<gix_diff::Rewrites>,
pub thread_limit: Option<usize>,
}
impl Repository {
#[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.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::<Error>(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 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)
}
pub(super) fn index_worktree_status_pathspec<E>(
&self,
patterns: impl IntoIterator<Item = impl AsRef<BStr>>,
index: &gix_index::State,
options: Option<&crate::dirwalk::Options>,
) -> Result<crate::Pathspec<'_>, E>
where
E: From<crate::repository::attributes::Error> + From<crate::pathspec::init::Error>,
{
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,
move || Ok(attrs_and_excludes.inner),
)?)
}
}
#[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>,
}
mod submodule_status {
use std::borrow::Cow;
use crate::config::cache::util::ApplyLeniency;
use crate::{
bstr,
bstr::BStr,
config,
status::{index_worktree::BuiltinSubmoduleStatus, Submodule},
};
impl BuiltinSubmoduleStatus {
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() {
Ok(Some(sm)) => {
let mut v: Vec<_> = sm.filter_map(|sm| sm.path().ok().map(Cow::into_owned)).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,
})
}
}
#[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),
#[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<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().is_ok_and(|path| path == rela_path)) else {
return Ok(None);
};
let (ignore, check_dirty) = match self.mode {
Submodule::AsConfigured { check_dirty } => {
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 {
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)))
}
}
}
pub struct Iter {
inner: crate::status::Iter,
}
#[derive(Clone, PartialEq, Debug)]
pub enum Item {
Modification {
entry: gix_index::Entry,
entry_index: usize,
rela_path: BString,
status: gix_status::index_as_worktree::EntryStatus<(), crate::submodule::Status>,
},
DirectoryContents {
entry: gix_dir::Entry,
collapsed_directory_status: Option<gix_dir::entry::Status>,
},
Rewrite {
source: RewriteSource,
dirwalk_entry: gix_dir::Entry,
dirwalk_entry_collapsed_directory_status: Option<gix_dir::entry::Status>,
dirwalk_entry_id: gix_hash::ObjectId,
diff: Option<gix_diff::blob::DiffLineStats>,
copy: bool,
},
}
#[derive(Clone, PartialEq, Debug)]
pub enum RewriteSource {
RewriteFromIndex {
source_entry: gix_index::Entry,
source_entry_index: usize,
source_rela_path: BString,
source_status: gix_status::index_as_worktree::EntryStatus<(), crate::submodule::Status>,
},
CopyFromDirectoryEntry {
source_dirwalk_entry: gix_dir::Entry,
source_dirwalk_entry_collapsed_directory_status: Option<gix_dir::entry::Status>,
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::{index_worktree, Platform},
};
impl RewriteSource {
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,
},
}
}
}
impl Item {
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
}
}
})
}
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<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;
impl<Progress> Platform<'_, Progress>
where
Progress: gix_features::progress::Progress,
{
#[doc(alias = "diff_index_to_workdir", alias = "git2")]
pub fn into_index_worktree_iter(
mut self,
patterns: impl IntoIterator<Item = BString>,
) -> Result<index_worktree::Iter, crate::status::into_iter::Error> {
self.head_tree = None;
Ok(index_worktree::Iter {
inner: self.into_iter(patterns)?,
})
}
}
impl Iterator for super::Iter {
type Item = Result<Item, index_worktree::Error>;
fn next(&mut self) -> Option<Self::Item> {
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")
}
})
})
}
}
impl super::Iter {
pub fn outcome_mut(&mut self) -> Option<&mut crate::status::Outcome> {
self.inner.out.as_mut()
}
pub fn into_outcome(mut self) -> Option<crate::status::Outcome> {
self.inner.out.take()
}
}
}