use std::path::{Path, PathBuf};
use ignore::WalkBuilder;
use crate::WorkspaceInfo;
#[must_use]
pub fn source_walk_builder(root: &Path) -> WalkBuilder {
let mut builder = WalkBuilder::new(root);
builder
.hidden(false)
.git_ignore(true)
.git_global(true)
.git_exclude(true);
builder
}
#[must_use]
pub fn source_walked_workspaces(root: &Path, workspaces: &[WorkspaceInfo]) -> Vec<WorkspaceInfo> {
let canonical_root = dunce::canonicalize(root).unwrap_or_else(|_| root.to_path_buf());
let canonical: Vec<Option<PathBuf>> = workspaces
.iter()
.map(|workspace| {
dunce::canonicalize(&workspace.root)
.ok()
.filter(|dir| dir.starts_with(&canonical_root))
})
.collect();
let candidates: Vec<PathBuf> = canonical.iter().flatten().cloned().collect();
let reached = dirs_reached_by_source_walk(&canonical_root, &candidates);
workspaces
.iter()
.zip(&canonical)
.filter(|(_, dir)| dir.as_ref().is_none_or(|dir| reached.contains(dir)))
.map(|(workspace, _)| workspace.clone())
.collect()
}
pub fn dirs_reached_by_source_walk(
canonical_root: &Path,
targets: &[PathBuf],
) -> rustc_hash::FxHashSet<PathBuf> {
let mut reached = rustc_hash::FxHashSet::default();
if targets.is_empty() {
return reached;
}
let mut on_path: rustc_hash::FxHashSet<PathBuf> = rustc_hash::FxHashSet::default();
for target in targets {
for ancestor in target.ancestors() {
if ancestor == canonical_root || !on_path.insert(ancestor.to_path_buf()) {
break;
}
}
}
let max_depth = targets
.iter()
.filter_map(|target| target.strip_prefix(canonical_root).ok())
.map(|relative| relative.components().count())
.max();
let mut builder = source_walk_builder(canonical_root);
builder
.max_depth(max_depth)
.filter_entry(move |entry| entry.depth() == 0 || on_path.contains(entry.path()));
let targets: rustc_hash::FxHashSet<&Path> = targets.iter().map(PathBuf::as_path).collect();
for entry in builder.build().flatten() {
if targets.contains(entry.path()) {
reached.insert(entry.into_path());
}
}
reached
}
#[cfg(test)]
mod tests {
use super::*;
fn workspace(root: PathBuf) -> WorkspaceInfo {
WorkspaceInfo {
name: root
.file_name()
.map_or_else(String::new, |name| name.to_string_lossy().into_owned()),
root,
is_internal_dependency: false,
}
}
fn walked_names(root: &Path, workspaces: &[WorkspaceInfo]) -> Vec<String> {
source_walked_workspaces(root, workspaces)
.into_iter()
.map(|workspace| workspace.name)
.collect()
}
#[test]
fn source_walked_workspaces_drops_gitignored_members_only_in_a_git_repository() {
let directory = tempfile::tempdir().expect("temporary directory");
let root = directory.path();
for dir in ["packages/app", "clone/packages/lib"] {
std::fs::create_dir_all(root.join(dir)).expect("member directory");
}
std::fs::write(root.join(".gitignore"), "clone\n").expect("gitignore");
let outside = tempfile::tempdir().expect("outside directory");
let workspaces = [
workspace(root.to_path_buf()),
workspace(root.join("packages/app")),
workspace(root.join("clone")),
workspace(root.join("clone/packages/lib")),
workspace(root.join("missing")),
workspace(outside.path().to_path_buf()),
];
let all: Vec<String> = workspaces.iter().map(|ws| ws.name.clone()).collect();
assert_eq!(walked_names(root, &workspaces), all, "no git repository");
std::fs::create_dir_all(root.join(".git")).expect("git directory");
let walked = walked_names(root, &workspaces);
assert!(!walked.contains(&"clone".to_string()), "{walked:?}");
assert!(!walked.contains(&"lib".to_string()), "{walked:?}");
assert_eq!(walked.len(), all.len() - 2, "{walked:?}");
}
}