use std::collections::HashSet;
use std::path::{Path, PathBuf};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(unix)]
type Identity = (u64, u64);
#[cfg(not(unix))]
type Identity = PathBuf;
#[cfg(unix)]
fn identity(p: &Path) -> Option<Identity> {
std::fs::metadata(p).ok().map(|md| (md.dev(), md.ino()))
}
#[cfg(not(unix))]
fn identity(p: &Path) -> Option<Identity> {
std::fs::canonicalize(p).ok()
}
fn push_unique(items: &mut Vec<PathBuf>, seen: &mut HashSet<Identity>, p: PathBuf) {
match identity(&p) {
Some(id) if !seen.insert(id) => {} _ => items.push(p),
}
}
pub fn resolve(paths: &[PathBuf]) -> (Vec<PathBuf>, Vec<String>) {
resolve_with(paths, None)
}
pub fn resolve_with(
paths: &[PathBuf],
archive_globs: Option<&ArchiveGlobs>,
) -> (Vec<PathBuf>, Vec<String>) {
let mut items = Vec::new();
let mut errors = Vec::new();
let mut seen = HashSet::new();
let mut walked_any = false;
let mut kept_any = false;
for p in paths {
match std::fs::metadata(p) {
Err(e) => errors.push(format!("{}: {e}", p.display())),
Ok(md) if md.is_dir() => {
walked_any = true;
let mut found = walk(p, &mut errors);
if let Some(g) = archive_globs {
found.retain(|f| g.is_match(p, f));
}
kept_any |= !found.is_empty();
found.sort();
for f in found {
push_unique(&mut items, &mut seen, f);
}
}
Ok(_) => push_unique(&mut items, &mut seen, p.clone()),
}
}
if archive_globs.is_some() && walked_any && !kept_any {
errors.push("--archive-glob matched no files in the directories searched".to_string());
}
(items, errors)
}
pub struct ArchiveGlobs {
names: globset::GlobSet,
paths: globset::GlobSet,
}
impl ArchiveGlobs {
fn is_match(&self, root: &Path, file: &Path) -> bool {
file.file_name().is_some_and(|n| self.names.is_match(n))
|| file.strip_prefix(root).is_ok_and(|rel| self.paths.is_match(rel))
}
}
pub fn build_archive_globs(pats: &[String]) -> anyhow::Result<Option<ArchiveGlobs>> {
if pats.is_empty() {
return Ok(None);
}
let mut names = globset::GlobSetBuilder::new();
let mut paths = globset::GlobSetBuilder::new();
for p in pats {
let g = globset::Glob::new(p)?;
if p.contains('/') { paths.add(g) } else { names.add(g) };
}
Ok(Some(ArchiveGlobs { names: names.build()?, paths: paths.build()? }))
}
fn walk(root: &Path, errors: &mut Vec<String>) -> Vec<PathBuf> {
let mut out = Vec::new();
for res in ignore::WalkBuilder::new(root)
.standard_filters(false)
.follow_links(true)
.build()
{
match res {
Ok(e) => {
if e.file_type().is_some_and(|t| t.is_file()) {
out.push(e.into_path());
}
}
Err(e) => errors.push(format!("{}: {e}", root.display())),
}
}
out
}
pub fn build_globs(pats: &[String]) -> anyhow::Result<Option<globset::GlobSet>> {
if pats.is_empty() {
return Ok(None);
}
let mut b = globset::GlobSetBuilder::new();
for p in pats {
b.add(globset::Glob::new(p)?);
}
Ok(Some(b.build()?))
}