use std::{ borrow::Cow, collections::HashSet, env, path::{Path, PathBuf}, }; use ignore_files::{IgnoreFile, IgnoreFilesFromOriginArgs}; use miette::{miette, IntoDiagnostic, Result}; use project_origins::ProjectType; use tokio::fs::canonicalize; use tracing::{debug, info, warn}; use watchexec::paths::common_prefix; use crate::args::Args; type ProjectOriginPath = PathBuf; type WorkDirPath = PathBuf; /// Extract relevant directories (in particular the project origin and work directory) /// given the command line arguments that were provided pub async fn dirs(args: &Args) -> Result<(ProjectOriginPath, WorkDirPath)> { 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.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.paths { 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 { let vcs_types = project_origins::types(origin) .await .into_iter() .filter(|pt| pt.is_vcs()) .collect::>(); info!(?vcs_types, "resolved vcs types"); vcs_types } pub async fn ignores( args: &Args, vcs_types: &[ProjectType], origin: &Path, ) -> Result> { fn higher_make_absolute_if_needed<'a>( origin: &'a Path, ) -> impl 'a + Fn(&'a PathBuf) -> Cow<'a, Path> { |path| { if path.is_absolute() { Cow::Borrowed(path) } else { Cow::Owned(origin.join(path)) } } } let mut skip_git_global_excludes = false; let mut ignores = if args.no_project_ignore { Vec::new() } else { let make_absolute_if_needed = higher_make_absolute_if_needed(origin); let include_paths = args.paths.iter().map(&make_absolute_if_needed); let ignore_files = args.ignore_files.iter().map(&make_absolute_if_needed); let (mut ignores, errors) = ignore_files::from_origin( IgnoreFilesFromOriginArgs::new_unchecked(origin, include_paths, ignore_files) .canonicalise() .await .into_diagnostic()?, ) .await; for err in errors { warn!("while discovering project-local ignore files: {}", err); } debug!(?ignores, "discovered ignore files from project origin"); 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::>(); debug!(?ignores, "filtered ignores to only those for project vcs"); } ignores }; let global_ignores = if args.no_global_ignore { Vec::new() } else { 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::>(); debug!( ?global_ignores, "filtered global ignores to exclude global git ignores" ); } global_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" ); ignores.extend(args.ignore_files.iter().map(|ig| IgnoreFile { applies_to: None, applies_in: None, path: ig.clone(), })); debug!( ?ignores, ?args.ignore_files, "combined with ignore files from command line / env" ); if args.no_project_ignore { ignores = ignores .into_iter() .filter(|ig| { !ig.applies_in .as_ref() .map_or(false, |p| p.starts_with(origin)) }) .collect::>(); debug!( ?ignores, "filtered ignores to exclude project-local ignores" ); } if args.no_global_ignore { ignores = ignores .into_iter() .filter(|ig| ig.applies_in.is_some()) .collect::>(); debug!(?ignores, "filtered ignores to exclude global ignores"); } if args.no_vcs_ignore { ignores = ignores .into_iter() .filter(|ig| ig.applies_to.is_none()) .collect::>(); debug!(?ignores, "filtered ignores to exclude VCS-specific ignores"); } info!(files=?ignores.iter().map(|ig| ig.path.as_path()).collect::>(), "found some ignores"); Ok(ignores) }