use crate::hook::SkipReason;
use crate::settings::Settings;
use crate::{Result, glob};
use dashmap::DashMap;
use indexmap::IndexSet;
use itertools::Itertools;
use std::collections::HashSet;
use std::io::Read;
use std::path::PathBuf;
use std::sync::LazyLock;
use super::types::Step;
pub fn is_binary_file(path: &PathBuf) -> Option<bool> {
static CACHE: LazyLock<DashMap<PathBuf, bool>> = LazyLock::new(DashMap::new);
if let Some(result) = CACHE.get(path) {
return Some(*result);
}
let mut file = std::fs::File::open(path).ok()?;
let mut buffer = [0u8; 8192];
let bytes_read = file.read(&mut buffer).ok()?;
let is_binary = buffer[..bytes_read].contains(&0);
CACHE.insert(path.clone(), is_binary);
Some(is_binary)
}
pub fn is_symlink_file(path: &PathBuf) -> Option<bool> {
static CACHE: LazyLock<DashMap<PathBuf, bool>> = LazyLock::new(DashMap::new);
if let Some(result) = CACHE.get(path) {
return Some(*result);
}
let metadata = std::fs::symlink_metadata(path).ok()?;
let is_symlink = metadata.file_type().is_symlink();
CACHE.insert(path.clone(), is_symlink);
Some(is_symlink)
}
impl Step {
pub fn enabled_profiles(&self) -> Option<IndexSet<String>> {
self.profiles.as_ref().map(|profiles| {
profiles
.iter()
.filter(|s| !s.starts_with('!'))
.map(|s| s.to_string())
.collect()
})
}
pub fn disabled_profiles(&self) -> Option<IndexSet<String>> {
self.profiles.as_ref().map(|profiles| {
profiles
.iter()
.filter(|s| s.starts_with('!'))
.map(|s| s.strip_prefix('!').unwrap().to_string())
.collect()
})
}
pub fn profile_skip_reason(&self) -> Option<SkipReason> {
let settings = Settings::get();
if let Some(enabled) = self.enabled_profiles() {
let enabled_profiles = settings.enabled_profiles();
let missing_profiles = enabled.difference(&enabled_profiles).collect::<Vec<_>>();
if !missing_profiles.is_empty() {
let profiles = missing_profiles
.into_iter()
.map(|s| s.to_string())
.collect();
return Some(SkipReason::ProfileNotEnabled(profiles));
}
let disabled_profiles_set = settings.disabled_profiles();
let disabled_profiles = disabled_profiles_set.intersection(&enabled).collect_vec();
if !disabled_profiles.is_empty() {
return Some(SkipReason::ProfileExplicitlyDisabled);
}
}
if let Some(disabled) = self.disabled_profiles() {
let enabled_profiles = settings.enabled_profiles();
let disabled_profiles = disabled.intersection(&enabled_profiles).collect::<Vec<_>>();
if !disabled_profiles.is_empty() {
return Some(SkipReason::ProfileExplicitlyDisabled);
}
}
None
}
pub fn filter_files(&self, files: &[PathBuf]) -> Result<Vec<PathBuf>> {
let mut files = files.to_vec();
if let Some(dir) = &self.dir {
files.retain(|f| f.starts_with(dir));
if files.is_empty() {
debug!("{self}: no files in {dir}");
}
}
if let Some(pattern) = &self.glob {
files = glob::get_pattern_matches(pattern, &files, self.dir.as_deref())?;
}
if let Some(pattern) = self.exclude.as_ref().filter(|pattern| !pattern.is_empty()) {
let excluded: HashSet<_> =
glob::get_pattern_matches(pattern, &files, self.dir.as_deref())?
.into_iter()
.collect();
files.retain(|f| !excluded.contains(f));
}
if !self.allow_binary {
files.retain(|f| {
is_binary_file(f).map(|is_bin| !is_bin).unwrap_or(true)
});
}
if !self.allow_symlinks {
files.retain(|f| {
is_symlink_file(f)
.map(|is_symlink| !is_symlink)
.unwrap_or(true)
});
}
if let Some(types) = &self.types {
files.retain(|f| crate::file_type::matches_types(f, types));
}
Ok(files)
}
pub fn workspaces_for_files(&self, files: &[PathBuf]) -> Result<Option<IndexSet<PathBuf>>> {
let Some(workspace_indicator) = &self.workspace_indicator else {
return Ok(None);
};
let mut dirs = files.iter().filter_map(|f| f.parent()).collect_vec();
let mut workspaces: IndexSet<PathBuf> = Default::default();
while let Some(dir) = dirs.pop() {
if let Some(workspace) = xx::file::find_up(dir, &[workspace_indicator]) {
workspaces.insert(workspace);
}
}
Ok(Some(workspaces))
}
}