use std::hash::{Hash, Hasher};
use camino::Utf8PathBuf;
use indexmap::IndexSet;
use crate::stdlib::which::{env::EnvSnapshot, resolve_error::ResolveError};
#[cfg(not(windows))]
mod posix;
#[cfg(windows)]
mod windows;
#[cfg(not(windows))]
use posix::search_workspace as platform_search_workspace;
#[cfg(windows)]
use windows::search_workspace as platform_search_workspace;
pub(super) const WORKSPACE_MAX_DEPTH: usize = 6;
pub(crate) const WORKSPACE_SKIP_DIRS: &[&str] =
&[".git", "target", "node_modules", "dist", "build"];
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct WorkspaceSkipList {
dirs: IndexSet<String>,
}
impl WorkspaceSkipList {
fn from_defaults() -> Self {
let mut dirs = IndexSet::new();
for dir in WORKSPACE_SKIP_DIRS {
dirs.insert((*dir).to_owned());
}
Self { dirs }
}
fn contains(&self, name: &str) -> bool {
self.dirs.contains(name)
}
pub(crate) fn from_names(names: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
let mut dirs = IndexSet::new();
for name in names {
let trimmed = name.as_ref().trim();
if trimmed.is_empty() {
continue;
}
dirs.insert(normalise_name(trimmed));
}
Self { dirs }
}
}
impl Hash for WorkspaceSkipList {
fn hash<H: Hasher>(&self, state: &mut H) {
let mut dirs: Vec<&String> = self.dirs.iter().collect();
dirs.sort_unstable();
for dir in dirs {
dir.hash(state);
}
}
}
impl Default for WorkspaceSkipList {
fn default() -> Self {
Self::from_defaults()
}
}
pub(super) fn search_workspace(
env: &EnvSnapshot,
command: &str,
collect_all: bool,
skip_dirs: &WorkspaceSkipList,
) -> Result<Vec<Utf8PathBuf>, ResolveError> {
if !env.workspace_fallback_enabled() {
tracing::debug!(
env = crate::stdlib::which::workspace_switch::WORKSPACE_FALLBACK_ENV,
"workspace which fallback disabled via env override",
);
return Ok(Vec::new());
}
tracing::debug!(
max_depth = WORKSPACE_MAX_DEPTH,
skip = ?skip_dirs,
"using workspace which fallback",
);
platform_search_workspace(env, command, collect_all, skip_dirs)
}
pub(super) fn should_visit_entry(entry: &walkdir::DirEntry, skip_dirs: &WorkspaceSkipList) -> bool {
if !entry.file_type().is_dir() {
return true;
}
let name = entry.file_name().to_string_lossy();
!skip_dirs.contains(&name)
}
pub(super) fn unwrap_or_log_error(
walk_entry: Result<walkdir::DirEntry, walkdir::Error>,
) -> Result<walkdir::DirEntry, ResolveError> {
walk_entry.map_err(|err| {
tracing::debug!(
error = %err,
"unreadable workspace entry during which fallback",
);
ResolveError::WalkDir { source: err }
})
}
pub(super) fn log_if_no_matches(matches: &[Utf8PathBuf], skip_dirs: &WorkspaceSkipList) {
if matches.is_empty() {
tracing::debug!(
max_depth = WORKSPACE_MAX_DEPTH,
skip = ?skip_dirs,
"workspace which fallback found no matches",
);
}
}
#[cfg(windows)]
fn normalise_name(name: &str) -> String {
name.to_ascii_lowercase()
}
#[cfg(not(windows))]
fn normalise_name(name: &str) -> String {
name.to_owned()
}