use std::sync::atomic::AtomicBool;
use super::{Baseline, Error, Limits, Report, UntrackedFiles};
use crate::ignore::WorktreeRules;
use crate::pathspec::Pathspec;
use crate::{Repository, filter, fsmonitor};
#[cfg(any(target_os = "linux", target_os = "macos"))]
mod refresh;
pub struct Scan<'a> {
repo: &'a Repository,
baseline: Baseline,
untracked: UntrackedFiles,
ignore: &'a WorktreeRules,
paths: Option<&'a Pathspec>,
filters: Option<&'a mut filter::Pipeline>,
monitor: Option<&'a fsmonitor::Monitor>,
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(super) capture: Option<std::sync::Arc<super::untracked::Capture>>,
}
impl<'a> Scan<'a> {
pub fn new(
repo: &'a Repository,
baseline: Baseline,
untracked: UntrackedFiles,
ignore: &'a WorktreeRules,
) -> Self {
Self {
repo,
baseline,
untracked,
ignore,
paths: None,
filters: None,
monitor: None,
#[cfg(any(target_os = "linux", target_os = "macos"))]
capture: None,
}
}
#[must_use]
pub fn with_pathspec(mut self, paths: &'a Pathspec) -> Self {
self.paths = Some(paths);
self
}
#[must_use]
pub fn with_filters(mut self, filters: &'a mut filter::Pipeline) -> Self {
self.filters = Some(filters);
self
}
#[must_use]
pub fn with_monitor(mut self, monitor: &'a fsmonitor::Monitor) -> Self {
self.monitor = Some(monitor);
self
}
pub fn refresh_caches(self) -> Result<Report, Error> {
self.refresh_caches_with(Limits::default(), &AtomicBool::new(false))
}
pub fn refresh_caches_with(self, limits: Limits, cancel: &AtomicBool) -> Result<Report, Error> {
super::types::check(cancel)?;
#[cfg(any(target_os = "linux", target_os = "macos"))]
return refresh::run(self, limits, cancel);
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let _ = (self, limits);
Err(Error::Unsupported {
path: Vec::new(),
reason: "status cache refresh requires macOS/Linux",
})
}
}
pub fn run(self) -> Result<Report, Error> {
self.run_with(Limits::default(), &AtomicBool::new(false))
}
pub fn run_with(self, limits: Limits, cancel: &AtomicBool) -> Result<Report, Error> {
super::types::check(cancel)?;
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let all = Pathspec::all();
super::workflow::supported::run_with_checkpoint(
self.repo,
self.baseline,
super::worktree::Listing::Ignoring {
rules: Box::new(self.ignore.base().clone()),
files: self.untracked,
names: super::workflow::supported::git_names(self.repo)?,
environment: Box::new(self.ignore.environment().clone()),
monitor: self.monitor.cloned().map(Box::new),
capture: self.capture,
excludes: Some(self.ignore.untracked_excludes(self.repo.object_format())),
},
self.paths.unwrap_or(&all),
self.filters,
limits,
cancel,
|| {},
)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let _ = (
self.repo,
self.baseline,
self.untracked,
self.ignore,
self.paths,
self.filters,
self.monitor,
limits,
);
Err(Error::Unsupported {
path: Vec::new(),
reason: "descriptor-relative worktree traversal requires macOS/Linux",
})
}
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(super) fn monitor_clean(
repo: &Repository,
index: Option<&crate::index::Index>,
monitor: &fsmonitor::Monitor,
limits: crate::index::Limits,
cancel: &AtomicBool,
) -> Result<Vec<bool>, Error> {
use std::collections::HashSet;
if repo.worktree() != Some(monitor.worktree()) {
return Err(Error::Fsmonitor(Box::new(fsmonitor::Error::Unsupported(
"monitor belongs to another working tree",
))));
}
let exact =
super::workflow::supported::git_names(repo)? == super::worktree::GitNames::default();
let standalone = !index.is_some_and(|index| {
index
.extensions()
.iter()
.any(|extension| extension.signature() == *b"link")
});
let cache = index
.filter(|_| exact && standalone)
.and_then(|index| index.fsmonitor_cache(limits).ok().flatten());
let changes = match monitor.query_with(
cache.as_ref().map(crate::index::FsmonitorCache::token),
Default::default(),
cancel,
) {
Ok(changes) => changes,
Err(fsmonitor::Error::Cancelled) => return Err(Error::Cancelled),
Err(
error @ (fsmonitor::Error::Refused
| fsmonitor::Error::Unsupported(_)
| fsmonitor::Error::Config(_)),
) => return Err(Error::Fsmonitor(Box::new(error))),
Err(
fsmonitor::Error::Io(_)
| fsmonitor::Error::Failed
| fsmonitor::Error::Response(_)
| fsmonitor::Error::Limit(_),
) => return Ok(Vec::new()),
};
let (Some(index), Some(cache)) = (index, cache) else {
return Ok(Vec::new());
};
if changes.full_scan() {
return Ok(Vec::new());
}
let paths: HashSet<&[u8]> = changes.paths().iter().map(Vec::as_slice).collect();
let mut clean = vec![true; index.entries().len()];
for (position, entry) in index.entries().iter().enumerate() {
super::types::check(cancel)?;
let changed = paths.contains(entry.path.as_slice())
|| entry.path.iter().enumerate().any(|(offset, byte)| {
*byte == b'/'
&& (paths.contains(&entry.path[..offset])
|| paths.contains(&entry.path[..=offset]))
});
if changed || cache.dirty().binary_search(&position).is_ok() {
clean[position] = false;
}
}
Ok(clean)
}