watchexec/crates/cli/src/filterer/common.rs

200 lines
5.0 KiB
Rust

use std::{
collections::HashSet,
env,
path::{Path, PathBuf},
};
use clap::ArgMatches;
use ignore_files::IgnoreFile;
use miette::{miette, IntoDiagnostic, Result};
use project_origins::ProjectType;
use tokio::fs::canonicalize;
use tracing::{debug, info, warn};
use watchexec::paths::common_prefix;
pub async fn dirs(args: &ArgMatches) -> Result<(PathBuf, PathBuf)> {
let curdir = env::current_dir().into_diagnostic()?;
let curdir = canonicalize(curdir).await.into_diagnostic()?;
debug!(?curdir, "current directory");
let project_origin = if let Some(origin) = args.value_of_os("project-origin") {
debug!(?origin, "project origin override");
canonicalize(origin).await.into_diagnostic()?
} else {
let homedir = match dirs::home_dir() {
None => None,
Some(dir) => Some(canonicalize(dir).await.into_diagnostic()?),
};
debug!(?homedir, "home directory");
let mut paths = HashSet::new();
for path in args.values_of_os("paths").unwrap_or_default() {
paths.insert(canonicalize(path).await.into_diagnostic()?);
}
let homedir_requested = homedir.as_ref().map_or(false, |home| paths.contains(home));
debug!(
?homedir_requested,
"resolved whether the homedir is explicitly requested"
);
if paths.is_empty() {
debug!("no paths, using current directory");
paths.insert(curdir.clone());
}
debug!(?paths, "resolved all watched paths");
let mut origins = HashSet::new();
for path in paths {
origins.extend(project_origins::origins(&path).await);
}
match (homedir, homedir_requested) {
(Some(ref dir), false) if origins.contains(dir) => {
debug!("removing homedir from origins");
origins.remove(dir);
}
_ => {}
}
if origins.is_empty() {
debug!("no origins, using current directory");
origins.insert(curdir.clone());
}
debug!(?origins, "resolved all project origins");
// This canonicalize is probably redundant
canonicalize(
common_prefix(&origins)
.ok_or_else(|| miette!("no common prefix, but this should never fail"))?,
)
.await
.into_diagnostic()?
};
info!(?project_origin, "resolved common/project origin");
let workdir = curdir;
info!(?workdir, "resolved working directory");
Ok((project_origin, workdir))
}
pub async fn vcs_types(origin: &Path) -> Vec<ProjectType> {
let vcs_types = project_origins::types(origin)
.await
.into_iter()
.filter(|pt| pt.is_vcs())
.collect::<Vec<_>>();
info!(?vcs_types, "resolved vcs types");
vcs_types
}
pub async fn ignores(
args: &ArgMatches,
vcs_types: &[ProjectType],
origin: &Path,
) -> Vec<IgnoreFile> {
let (mut ignores, errors) = ignore_files::from_origin(origin).await;
for err in errors {
warn!("while discovering project-local ignore files: {}", err);
}
debug!(?ignores, "discovered ignore files from project origin");
// TODO: use drain_ignore instead for x = x.filter()... when that stabilises
let mut skip_git_global_excludes = false;
if !vcs_types.is_empty() {
ignores = ignores
.into_iter()
.filter(|ig| match ig.applies_to {
Some(pt) if pt.is_vcs() => vcs_types.contains(&pt),
_ => true,
})
.inspect(|ig| {
if let IgnoreFile {
applies_to: Some(ProjectType::Git),
applies_in: None,
..
} = ig
{
warn!("project git config overrides the global excludes");
skip_git_global_excludes = true;
}
})
.collect::<Vec<_>>();
debug!(?ignores, "filtered ignores to only those for project vcs");
}
let (mut global_ignores, errors) = ignore_files::from_environment(Some("watchexec")).await;
for err in errors {
warn!("while discovering global ignore files: {}", err);
}
debug!(?global_ignores, "discovered ignore files from environment");
if skip_git_global_excludes {
global_ignores = global_ignores
.into_iter()
.filter(|gig| {
!matches!(
gig,
IgnoreFile {
applies_to: Some(ProjectType::Git),
applies_in: None,
..
}
)
})
.collect::<Vec<_>>();
debug!(
?global_ignores,
"filtered global ignores to exclude global git ignores"
);
}
ignores.extend(global_ignores.into_iter().filter(|ig| match ig.applies_to {
Some(pt) if pt.is_vcs() => vcs_types.contains(&pt),
_ => true,
}));
debug!(
?ignores,
?vcs_types,
"combined and applied overall vcs filter over ignores"
);
if args.is_present("no-project-ignore") {
ignores = ignores
.into_iter()
.filter(|ig| {
!ig.applies_in
.as_ref()
.map_or(false, |p| p.starts_with(origin))
})
.collect::<Vec<_>>();
debug!(
?ignores,
"filtered ignores to exclude project-local ignores"
);
}
if args.is_present("no-global-ignore") {
ignores = ignores
.into_iter()
.filter(|ig| !matches!(ig.applies_in, None))
.collect::<Vec<_>>();
debug!(?ignores, "filtered ignores to exclude global ignores");
}
if args.is_present("no-vcs-ignore") {
ignores = ignores
.into_iter()
.filter(|ig| matches!(ig.applies_to, None))
.collect::<Vec<_>>();
debug!(?ignores, "filtered ignores to exclude VCS-specific ignores");
}
info!(files=?ignores.iter().map(|ig| ig.path.as_path()).collect::<Vec<_>>(), "found some ignores");
ignores
}