use std::collections::BTreeSet;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::time::Instant;
use ignore::WalkBuilder;
use ignore::overrides::Override;
use ignore::overrides::OverrideBuilder;
use crate::Engine;
use crate::Error;
use crate::ErrorKind;
use crate::Scope;
use crate::config::FeatureMode;
use crate::engine::git::Repository;
impl Engine {
pub(super) fn discover_files(
&self,
repo: Option<&Repository>,
scope: Scope<'_>,
) -> Result<Vec<PathBuf>, Error> {
let started = Instant::now();
let (files, source) = match scope {
Scope::All => (self.discover_directory(&self.root, repo)?, "files.root"),
Scope::Paths(paths) => {
let (directories, direct_files) = self.resolve_requested_paths(paths)?;
let mut files = direct_files;
for directory in directories {
files.extend(self.discover_directory(&directory, repo)?);
}
(files, "the requested paths")
}
};
let files = if let Some(header_path) = &self.header_path {
let mut selected = Vec::with_capacity(files.len());
for path in files {
let absolute_path = self.root.join(&path);
let is_header =
same_file::is_same_file(&absolute_path, header_path).map_err(|err| {
Error::new(
ErrorKind::Unexpected,
format!(
"cannot compare {} with header template {}",
absolute_path.display(),
header_path.display()
),
)
.with_source(err)
})?;
if !is_header {
selected.push(path);
}
}
selected
} else {
files.into_iter().collect()
};
log::debug!(
"discovered {} candidate files from {source} in {:?}",
files.len(),
started.elapsed()
);
Ok(files)
}
fn discover_directory(
&self,
scan_root: &Path,
repo: Option<&Repository>,
) -> Result<BTreeSet<PathBuf>, Error> {
let started = Instant::now();
let (files, backend) = if self.git.ignore != FeatureMode::Disable
&& let Some(repo) = repo
{
let prefix = scan_root
.strip_prefix(&self.root)
.expect("requested directories are inside files.root");
let mut files = repo
.list_files(scan_root)?
.into_iter()
.map(|path| prefix.join(path))
.collect::<BTreeSet<_>>();
files.retain(|path| self.file_filter.matched(path, false).is_whitelist());
(files, "Git worktree")
} else {
(self.walk_directory(scan_root)?, "filesystem walk")
};
log::debug!(
"discovered {} files under {} via {backend} in {:?}",
files.len(),
scan_root.display(),
started.elapsed()
);
Ok(files)
}
fn resolve_requested_paths(
&self,
paths: &[PathBuf],
) -> Result<(BTreeSet<PathBuf>, BTreeSet<PathBuf>), Error> {
let mut directories = BTreeSet::new();
let mut files = BTreeSet::new();
for requested in paths {
let path = if requested.is_absolute() {
requested.clone()
} else {
self.root.join(requested)
};
let metadata = match fs::symlink_metadata(&path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
log::warn!("skipping path that does not exist: {}", requested.display());
continue;
}
Err(err) => {
return Err(Error::new(
ErrorKind::Unexpected,
format!("cannot inspect requested path {}", requested.display()),
)
.with_source(err));
}
};
let resolved = if metadata.file_type().is_symlink() && path.is_file() {
let parent = path
.parent()
.unwrap_or(Path::new("."))
.canonicalize()
.map_err(|err| {
Error::new(
ErrorKind::Unexpected,
format!("cannot resolve requested path {}", requested.display()),
)
.with_source(err)
})?;
parent.join(
path.file_name()
.expect("a file symlink must have a filename"),
)
} else {
path.canonicalize().map_err(|err| {
Error::new(
ErrorKind::Unexpected,
format!("cannot resolve requested path {}", requested.display()),
)
.with_source(err)
})?
};
let relative = match resolved.strip_prefix(&self.root) {
Ok(relative) => relative.to_path_buf(),
Err(_) => {
log::debug!(
"skipping path outside files.root {}: {}",
self.root.display(),
requested.display()
);
continue;
}
};
if metadata.is_dir() {
directories.insert(resolved);
} else if metadata.is_file()
|| (metadata.file_type().is_symlink() && resolved.is_file())
{
if self.matches_file_filter(&relative) {
files.insert(relative);
}
} else {
log::warn!(
"skipping path that is not a regular file or directory: {}",
requested.display()
);
}
}
Ok((directories, files))
}
fn matches_file_filter(&self, path: &Path) -> bool {
for parent in path.ancestors().skip(1) {
if !parent.as_os_str().is_empty() && self.file_filter.matched(parent, true).is_ignore()
{
return false;
}
}
self.file_filter.matched(path, false).is_whitelist()
}
fn walk_directory(&self, scan_root: &Path) -> Result<BTreeSet<PathBuf>, Error> {
let use_git_ignore = self.git.ignore != FeatureMode::Disable;
let mut files = BTreeSet::new();
let walker = WalkBuilder::new(scan_root)
.hidden(false)
.ignore(false)
.git_ignore(use_git_ignore)
.git_global(use_git_ignore)
.git_exclude(use_git_ignore)
.parents(use_git_ignore)
.follow_links(false)
.overrides(self.walk_filter.clone())
.build();
for entry in walker {
let entry = entry.map_err(|err| {
Error::new(ErrorKind::Unexpected, "cannot discover files").with_source(err)
})?;
let path = entry.path();
let Some(file_type) = entry.file_type() else {
continue;
};
if !file_type.is_file() && !(file_type.is_symlink() && path.is_file()) {
continue;
}
let relative = path.strip_prefix(&self.root).map_err(|_| {
Error::new(
ErrorKind::Unexpected,
format!(
"file walker returned path outside files.root: {}",
path.display()
),
)
})?;
if self.file_filter.matched(relative, false).is_whitelist() {
files.insert(relative.to_path_buf());
}
}
Ok(files)
}
}
pub fn compile_file_filters(
root: &Path,
includes: &[String],
excludes: &[String],
) -> Result<(Override, Override), Error> {
let mut builder = OverrideBuilder::new(root);
if includes.is_empty() {
builder.add("**").map_err(file_filter_error)?;
} else {
for pattern in includes {
builder.add(pattern).map_err(file_filter_error)?;
}
}
builder.add("!.git").map_err(file_filter_error)?;
builder.add("!.git/**").map_err(file_filter_error)?;
for pattern in excludes {
builder
.add(&format!("!{pattern}"))
.map_err(file_filter_error)?;
}
let file_filter = builder.build().map_err(file_filter_error)?;
let mut builder = OverrideBuilder::new(root);
builder.add("!.git").map_err(file_filter_error)?;
builder.add("!.git/**").map_err(file_filter_error)?;
for pattern in excludes {
builder
.add(&format!("!{pattern}"))
.map_err(file_filter_error)?;
}
let walk_filter = builder.build().map_err(file_filter_error)?;
Ok((file_filter, walk_filter))
}
fn file_filter_error(err: ignore::Error) -> Error {
Error::new(
ErrorKind::ConfigInvalid,
"invalid files.includes or files.excludes pattern",
)
.with_source(err)
}