2021-10-10 05:03:05 +02:00
|
|
|
use std::{
|
2022-01-15 13:44:53 +01:00
|
|
|
collections::HashSet,
|
2021-10-10 05:03:05 +02:00
|
|
|
env,
|
|
|
|
io::{Error, ErrorKind},
|
|
|
|
path::{Path, PathBuf},
|
|
|
|
};
|
2021-10-09 07:45:32 +02:00
|
|
|
|
2024-01-04 10:32:47 +01:00
|
|
|
use futures::future::try_join_all;
|
2023-03-02 04:17:19 +01:00
|
|
|
use gix_config::{path::interpolate::Context as InterpolateContext, File, Path as GitPath};
|
2024-01-01 06:01:14 +01:00
|
|
|
use miette::{bail, Result};
|
2024-01-04 10:32:47 +01:00
|
|
|
use normalize_path::NormalizePath;
|
2022-06-15 05:25:05 +02:00
|
|
|
use project_origins::ProjectType;
|
2023-01-06 14:53:49 +01:00
|
|
|
use tokio::fs::{canonicalize, metadata, read_dir};
|
2021-12-31 13:42:39 +01:00
|
|
|
use tracing::{trace, trace_span};
|
2021-10-09 07:45:32 +02:00
|
|
|
|
2022-06-15 05:25:05 +02:00
|
|
|
use crate::{IgnoreFile, IgnoreFilter};
|
2021-10-10 05:03:05 +02:00
|
|
|
|
2024-01-01 06:01:14 +01:00
|
|
|
/// Arguments for finding ignored files in a given directory and subdirectories
|
2024-01-04 10:32:47 +01:00
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
|
|
#[non_exhaustive]
|
2024-01-01 06:01:14 +01:00
|
|
|
pub struct IgnoreFilesFromOriginArgs {
|
2024-01-04 10:32:47 +01:00
|
|
|
/// Origin from which finding ignored files will start.
|
|
|
|
pub origin: PathBuf,
|
2024-01-01 06:01:14 +01:00
|
|
|
|
|
|
|
/// Paths that have been explicitly selected to be watched.
|
|
|
|
///
|
2024-01-04 10:32:47 +01:00
|
|
|
/// If this list is non-empty, all paths not on this list will be ignored.
|
2024-01-01 06:01:14 +01:00
|
|
|
///
|
2024-01-04 10:32:47 +01:00
|
|
|
/// These paths *must* be absolute and normalised (no `.` and `..` components).
|
|
|
|
pub explicit_watches: Vec<PathBuf>,
|
2024-01-01 06:01:14 +01:00
|
|
|
|
2024-01-04 10:32:47 +01:00
|
|
|
/// Paths that have been explicitly ignored.
|
2024-01-01 06:01:14 +01:00
|
|
|
///
|
2024-01-04 10:32:47 +01:00
|
|
|
/// If this list is non-empty, all paths on this list will be ignored.
|
2024-01-01 06:01:14 +01:00
|
|
|
///
|
2024-01-04 10:32:47 +01:00
|
|
|
/// These paths *must* be absolute and normalised (no `.` and `..` components).
|
|
|
|
pub explicit_ignores: Vec<PathBuf>,
|
2024-01-01 06:01:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
impl IgnoreFilesFromOriginArgs {
|
2024-01-04 10:32:47 +01:00
|
|
|
/// Check that this struct is correctly-formed.
|
|
|
|
pub fn check(&self) -> Result<()> {
|
|
|
|
if self.explicit_watches.iter().any(|p| !p.is_absolute()) {
|
|
|
|
bail!("explicit_watches contains non-absolute paths");
|
|
|
|
}
|
|
|
|
if self.explicit_watches.iter().any(|p| !p.is_normalized()) {
|
|
|
|
bail!("explicit_watches contains non-normalised paths");
|
|
|
|
}
|
|
|
|
if self.explicit_ignores.iter().any(|p| !p.is_absolute()) {
|
|
|
|
bail!("explicit_ignores contains non-absolute paths");
|
|
|
|
}
|
|
|
|
if self.explicit_ignores.iter().any(|p| !p.is_normalized()) {
|
|
|
|
bail!("explicit_ignores contains non-normalised paths");
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Canonicalise all paths.
|
|
|
|
///
|
|
|
|
/// The result is always well-formed.
|
|
|
|
pub async fn canonicalise(self) -> std::io::Result<Self> {
|
|
|
|
Ok(Self {
|
|
|
|
origin: canonicalize(&self.origin).await?,
|
|
|
|
explicit_watches: try_join_all(self.explicit_watches.into_iter().map(canonicalize))
|
|
|
|
.await?,
|
|
|
|
explicit_ignores: try_join_all(self.explicit_ignores.into_iter().map(canonicalize))
|
|
|
|
.await?,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Create args with all fields set and check that they are correctly-formed.
|
2024-01-01 06:01:14 +01:00
|
|
|
pub fn new(
|
|
|
|
origin: impl AsRef<Path>,
|
|
|
|
explicit_watches: Vec<PathBuf>,
|
|
|
|
explicit_ignores: Vec<PathBuf>,
|
|
|
|
) -> Result<Self> {
|
2024-01-04 10:32:47 +01:00
|
|
|
let this = Self {
|
2024-01-01 06:01:14 +01:00
|
|
|
origin: PathBuf::from(origin.as_ref()),
|
|
|
|
explicit_watches,
|
|
|
|
explicit_ignores,
|
2024-01-04 10:32:47 +01:00
|
|
|
};
|
|
|
|
this.check()?;
|
|
|
|
Ok(this)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Create args without checking well-formed-ness.
|
|
|
|
///
|
|
|
|
/// Use this only if you know that the args are well-formed, or if you are about to call
|
|
|
|
/// [`canonicalise()`][IgnoreFilesFromOriginArgs::canonicalise()] on them.
|
|
|
|
pub fn new_unchecked(
|
|
|
|
origin: impl AsRef<Path>,
|
|
|
|
explicit_watches: impl IntoIterator<Item = impl Into<PathBuf>>,
|
|
|
|
explicit_ignores: impl IntoIterator<Item = impl Into<PathBuf>>,
|
|
|
|
) -> Self {
|
|
|
|
Self {
|
|
|
|
origin: origin.as_ref().into(),
|
|
|
|
explicit_watches: explicit_watches.into_iter().map(Into::into).collect(),
|
|
|
|
explicit_ignores: explicit_ignores.into_iter().map(Into::into).collect(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<&Path> for IgnoreFilesFromOriginArgs {
|
|
|
|
fn from(path: &Path) -> Self {
|
|
|
|
Self {
|
|
|
|
origin: path.into(),
|
|
|
|
..Default::default()
|
|
|
|
}
|
2024-01-01 06:01:14 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-09 07:45:32 +02:00
|
|
|
/// Finds all ignore files in the given directory and subdirectories.
|
|
|
|
///
|
|
|
|
/// This considers:
|
|
|
|
/// - Git ignore files (`.gitignore`)
|
|
|
|
/// - Mercurial ignore files (`.hgignore`)
|
|
|
|
/// - Tool-generic `.ignore` files
|
|
|
|
/// - `.git/info/exclude` files in the `path` directory only
|
|
|
|
/// - Git configurable project ignore files (with `core.excludesFile` in `.git/config`)
|
|
|
|
///
|
|
|
|
/// Importantly, this should be called from the origin of the project, not a subfolder. This
|
|
|
|
/// function will not discover the project origin, and will not traverse parent directories. Use the
|
2023-03-18 11:23:46 +01:00
|
|
|
/// `project-origins` crate for that.
|
2021-10-09 07:45:32 +02:00
|
|
|
///
|
|
|
|
/// This function also does not distinguish between project folder types, and collects all files for
|
|
|
|
/// all supported VCSs and other project types. Use the `applies_to` field to filter the results.
|
2021-10-10 05:03:05 +02:00
|
|
|
///
|
|
|
|
/// All errors (permissions, etc) are collected and returned alongside the ignore files: you may
|
|
|
|
/// want to show them to the user while still using whatever ignores were successfully found. Errors
|
|
|
|
/// from files not being found are silently ignored (the files are just not returned).
|
|
|
|
///
|
|
|
|
/// ## Special case: project-local git config specifying `core.excludesFile`
|
|
|
|
///
|
|
|
|
/// If the project's `.git/config` specifies a value for `core.excludesFile`, this function will
|
|
|
|
/// return an `IgnoreFile { path: path/to/that/file, applies_in: None, applies_to: Some(ProjectType::Git) }`.
|
|
|
|
/// This is the only case in which the `applies_in` field is None from this function. When such is
|
|
|
|
/// received the global Git ignore files found by [`from_environment()`] **should be ignored**.
|
2023-01-06 14:53:49 +01:00
|
|
|
///
|
|
|
|
/// ## Async
|
|
|
|
///
|
2023-03-02 04:17:19 +01:00
|
|
|
/// This future is not `Send` due to [`gix_config`] internals.
|
2024-01-04 10:32:47 +01:00
|
|
|
///
|
|
|
|
/// ## Panics
|
|
|
|
///
|
|
|
|
/// This function panics if the `args` are not correctly-formed; this can be checked beforehand
|
|
|
|
/// without panicking with [`IgnoreFilesFromOriginArgs::check()`].
|
2023-01-06 14:53:49 +01:00
|
|
|
#[allow(clippy::future_not_send)]
|
2024-01-01 06:01:14 +01:00
|
|
|
pub async fn from_origin(
|
|
|
|
args: impl Into<IgnoreFilesFromOriginArgs>,
|
|
|
|
) -> (Vec<IgnoreFile>, Vec<Error>) {
|
|
|
|
let args = args.into();
|
2024-01-04 10:32:47 +01:00
|
|
|
args.check()
|
|
|
|
.expect("checking well-formedness of IgnoreFilesFromOriginArgs");
|
|
|
|
|
2024-01-01 06:01:14 +01:00
|
|
|
let origin = &args.origin;
|
|
|
|
let mut ignore_files = args
|
|
|
|
.explicit_ignores
|
|
|
|
.iter()
|
|
|
|
.map(|p| IgnoreFile {
|
|
|
|
path: p.clone(),
|
|
|
|
applies_in: Some(origin.clone()),
|
|
|
|
applies_to: None,
|
|
|
|
})
|
|
|
|
.collect();
|
2021-10-10 05:03:05 +02:00
|
|
|
let mut errors = Vec::new();
|
|
|
|
|
2024-01-01 06:01:14 +01:00
|
|
|
match find_file(origin.join(".git/config")).await {
|
2021-10-10 05:03:05 +02:00
|
|
|
Err(err) => errors.push(err),
|
|
|
|
Ok(None) => {}
|
2023-11-25 21:33:44 +01:00
|
|
|
Ok(Some(path)) => match path.parent().map(|path| File::from_git_dir(path.into())) {
|
2022-09-07 04:52:53 +02:00
|
|
|
None => errors.push(Error::new(
|
|
|
|
ErrorKind::Other,
|
|
|
|
"unreachable: .git/config must have a parent",
|
|
|
|
)),
|
|
|
|
Some(Err(err)) => errors.push(Error::new(ErrorKind::Other, err)),
|
|
|
|
Some(Ok(config)) => {
|
2023-01-06 14:53:49 +01:00
|
|
|
let config_excludes = config.value::<GitPath<'_>>("core", None, "excludesFile");
|
|
|
|
if let Ok(excludes) = config_excludes {
|
2022-09-07 04:52:53 +02:00
|
|
|
match excludes.interpolate(InterpolateContext {
|
|
|
|
home_dir: env::var("HOME").ok().map(PathBuf::from).as_deref(),
|
|
|
|
..Default::default()
|
|
|
|
}) {
|
2022-04-04 01:43:30 +02:00
|
|
|
Ok(e) => {
|
|
|
|
discover_file(
|
2024-01-01 06:01:14 +01:00
|
|
|
&mut ignore_files,
|
2022-04-04 01:43:30 +02:00
|
|
|
&mut errors,
|
|
|
|
None,
|
|
|
|
Some(ProjectType::Git),
|
|
|
|
e.into(),
|
|
|
|
)
|
|
|
|
.await;
|
|
|
|
}
|
|
|
|
Err(err) => {
|
|
|
|
errors.push(Error::new(ErrorKind::Other, err));
|
|
|
|
}
|
|
|
|
}
|
2021-10-10 05:03:05 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
discover_file(
|
2024-01-01 06:01:14 +01:00
|
|
|
&mut ignore_files,
|
2021-10-10 05:03:05 +02:00
|
|
|
&mut errors,
|
2024-01-01 06:01:14 +01:00
|
|
|
Some(origin.clone()),
|
2021-10-10 05:03:05 +02:00
|
|
|
Some(ProjectType::Bazaar),
|
2024-01-01 06:01:14 +01:00
|
|
|
origin.join(".bzrignore"),
|
2021-10-10 05:03:05 +02:00
|
|
|
)
|
|
|
|
.await;
|
|
|
|
|
|
|
|
discover_file(
|
2024-01-01 06:01:14 +01:00
|
|
|
&mut ignore_files,
|
2021-10-10 05:03:05 +02:00
|
|
|
&mut errors,
|
2024-01-01 06:01:14 +01:00
|
|
|
Some(origin.clone()),
|
2021-10-10 05:03:05 +02:00
|
|
|
Some(ProjectType::Darcs),
|
2024-01-01 06:01:14 +01:00
|
|
|
origin.join("_darcs/prefs/boring"),
|
2021-10-10 05:03:05 +02:00
|
|
|
)
|
|
|
|
.await;
|
|
|
|
|
|
|
|
discover_file(
|
2024-01-01 06:01:14 +01:00
|
|
|
&mut ignore_files,
|
2021-10-10 05:03:05 +02:00
|
|
|
&mut errors,
|
2024-01-01 06:01:14 +01:00
|
|
|
Some(origin.clone()),
|
2021-10-10 05:03:05 +02:00
|
|
|
Some(ProjectType::Fossil),
|
2024-01-01 06:01:14 +01:00
|
|
|
origin.join(".fossil-settings/ignore-glob"),
|
2021-10-10 05:03:05 +02:00
|
|
|
)
|
|
|
|
.await;
|
|
|
|
|
|
|
|
discover_file(
|
2024-01-01 06:01:14 +01:00
|
|
|
&mut ignore_files,
|
2021-10-10 05:03:05 +02:00
|
|
|
&mut errors,
|
2024-01-01 06:01:14 +01:00
|
|
|
Some(origin.clone()),
|
2021-10-10 05:03:05 +02:00
|
|
|
Some(ProjectType::Git),
|
2024-01-01 06:01:14 +01:00
|
|
|
origin.join(".git/info/exclude"),
|
2021-10-10 05:03:05 +02:00
|
|
|
)
|
|
|
|
.await;
|
|
|
|
|
2022-01-15 12:57:29 +01:00
|
|
|
trace!("visiting child directories for ignore files");
|
2024-01-01 06:01:14 +01:00
|
|
|
match DirTourist::new(origin, &ignore_files, &args.explicit_watches).await {
|
2022-01-15 14:36:22 +01:00
|
|
|
Ok(mut dirs) => {
|
2022-01-15 13:44:53 +01:00
|
|
|
loop {
|
|
|
|
match dirs.next().await {
|
|
|
|
Visit::Done => break,
|
|
|
|
Visit::Skip => continue,
|
|
|
|
Visit::Find(dir) => {
|
2024-01-01 06:01:14 +01:00
|
|
|
// Attempt to find a .ignore file in the directory
|
2022-01-15 13:44:53 +01:00
|
|
|
if discover_file(
|
2024-01-01 06:01:14 +01:00
|
|
|
&mut ignore_files,
|
2022-01-15 13:44:53 +01:00
|
|
|
&mut errors,
|
|
|
|
Some(dir.clone()),
|
|
|
|
None,
|
|
|
|
dir.join(".ignore"),
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
{
|
2024-01-01 06:01:14 +01:00
|
|
|
dirs.add_last_file_to_filter(&ignore_files, &mut errors)
|
|
|
|
.await;
|
2022-01-15 13:44:53 +01:00
|
|
|
}
|
|
|
|
|
2024-01-01 06:01:14 +01:00
|
|
|
// Attempt to find a .gitignore file in the directory
|
2022-01-15 13:44:53 +01:00
|
|
|
if discover_file(
|
2024-01-01 06:01:14 +01:00
|
|
|
&mut ignore_files,
|
2022-01-15 13:44:53 +01:00
|
|
|
&mut errors,
|
|
|
|
Some(dir.clone()),
|
|
|
|
Some(ProjectType::Git),
|
|
|
|
dir.join(".gitignore"),
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
{
|
2024-01-01 06:01:14 +01:00
|
|
|
dirs.add_last_file_to_filter(&ignore_files, &mut errors)
|
|
|
|
.await;
|
2022-01-15 13:44:53 +01:00
|
|
|
}
|
|
|
|
|
2024-01-01 06:01:14 +01:00
|
|
|
// Attempt to find a .hgignore file in the directory
|
2022-01-15 13:44:53 +01:00
|
|
|
if discover_file(
|
2024-01-01 06:01:14 +01:00
|
|
|
&mut ignore_files,
|
2022-01-15 13:44:53 +01:00
|
|
|
&mut errors,
|
|
|
|
Some(dir.clone()),
|
|
|
|
Some(ProjectType::Mercurial),
|
|
|
|
dir.join(".hgignore"),
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
{
|
2024-01-01 06:01:14 +01:00
|
|
|
dirs.add_last_file_to_filter(&ignore_files, &mut errors)
|
|
|
|
.await;
|
2022-01-15 13:44:53 +01:00
|
|
|
}
|
2022-01-15 12:57:29 +01:00
|
|
|
}
|
|
|
|
}
|
2021-10-10 05:03:05 +02:00
|
|
|
}
|
2022-01-15 13:44:53 +01:00
|
|
|
errors.extend(dirs.errors);
|
|
|
|
}
|
|
|
|
Err(err) => {
|
|
|
|
errors.push(err);
|
2021-10-10 05:03:05 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-01 06:01:14 +01:00
|
|
|
(ignore_files, errors)
|
2021-10-09 07:45:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Finds all ignore files that apply to the current runtime.
|
|
|
|
///
|
2023-03-05 02:57:34 +01:00
|
|
|
/// Takes an optional `appname` for the calling application for application-specific config files.
|
2022-06-15 05:25:05 +02:00
|
|
|
///
|
2021-10-09 07:45:32 +02:00
|
|
|
/// This considers:
|
2021-10-10 05:03:05 +02:00
|
|
|
/// - User-specific git ignore files (e.g. `~/.gitignore`)
|
2021-10-09 07:45:32 +02:00
|
|
|
/// - Git configurable ignore files (e.g. with `core.excludesFile` in system or user config)
|
2022-06-15 05:25:05 +02:00
|
|
|
/// - `$XDG_CONFIG_HOME/{appname}/ignore`, as well as other locations (APPDATA on Windows…)
|
2021-10-10 05:03:05 +02:00
|
|
|
///
|
|
|
|
/// All errors (permissions, etc) are collected and returned alongside the ignore files: you may
|
|
|
|
/// want to show them to the user while still using whatever ignores were successfully found. Errors
|
|
|
|
/// from files not being found are silently ignored (the files are just not returned).
|
2023-01-06 14:53:49 +01:00
|
|
|
///
|
|
|
|
/// ## Async
|
|
|
|
///
|
2023-03-02 04:17:19 +01:00
|
|
|
/// This future is not `Send` due to [`gix_config`] internals.
|
2023-01-06 14:53:49 +01:00
|
|
|
#[allow(clippy::future_not_send)]
|
2022-06-15 05:25:05 +02:00
|
|
|
pub async fn from_environment(appname: Option<&str>) -> (Vec<IgnoreFile>, Vec<Error>) {
|
2021-10-10 05:03:05 +02:00
|
|
|
let mut files = Vec::new();
|
|
|
|
let mut errors = Vec::new();
|
|
|
|
|
|
|
|
let mut found_git_global = false;
|
2022-09-07 04:52:53 +02:00
|
|
|
match File::from_environment_overrides().map(|mut env| {
|
|
|
|
File::from_globals().map(move |glo| {
|
|
|
|
env.append(glo);
|
|
|
|
env
|
|
|
|
})
|
|
|
|
}) {
|
2021-10-10 05:03:05 +02:00
|
|
|
Err(err) => errors.push(Error::new(ErrorKind::Other, err)),
|
2022-09-07 04:52:53 +02:00
|
|
|
Ok(Err(err)) => errors.push(Error::new(ErrorKind::Other, err)),
|
|
|
|
Ok(Ok(config)) => {
|
2023-01-06 14:53:49 +01:00
|
|
|
let config_excludes = config.value::<GitPath<'_>>("core", None, "excludesFile");
|
|
|
|
if let Ok(excludes) = config_excludes {
|
2022-09-07 04:52:53 +02:00
|
|
|
match excludes.interpolate(InterpolateContext {
|
|
|
|
home_dir: env::var("HOME").ok().map(PathBuf::from).as_deref(),
|
|
|
|
..Default::default()
|
|
|
|
}) {
|
2022-04-04 01:43:30 +02:00
|
|
|
Ok(e) => {
|
|
|
|
if discover_file(
|
|
|
|
&mut files,
|
|
|
|
&mut errors,
|
|
|
|
None,
|
|
|
|
Some(ProjectType::Git),
|
|
|
|
e.into(),
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
{
|
|
|
|
found_git_global = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(err) => {
|
|
|
|
errors.push(Error::new(ErrorKind::Other, err));
|
|
|
|
}
|
2021-10-10 05:03:05 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !found_git_global {
|
|
|
|
let mut tries = Vec::with_capacity(5);
|
|
|
|
if let Ok(home) = env::var("XDG_CONFIG_HOME") {
|
|
|
|
tries.push(Path::new(&home).join("git/ignore"));
|
|
|
|
}
|
|
|
|
if let Ok(home) = env::var("APPDATA") {
|
|
|
|
tries.push(Path::new(&home).join(".gitignore"));
|
|
|
|
}
|
|
|
|
if let Ok(home) = env::var("USERPROFILE") {
|
|
|
|
tries.push(Path::new(&home).join(".gitignore"));
|
|
|
|
}
|
|
|
|
if let Ok(home) = env::var("HOME") {
|
|
|
|
tries.push(Path::new(&home).join(".config/git/ignore"));
|
|
|
|
tries.push(Path::new(&home).join(".gitignore"));
|
|
|
|
}
|
|
|
|
|
|
|
|
for path in tries {
|
|
|
|
if discover_file(&mut files, &mut errors, None, Some(ProjectType::Git), path).await {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-10 05:06:56 +02:00
|
|
|
let mut bzrs = Vec::with_capacity(5);
|
|
|
|
if let Ok(home) = env::var("APPDATA") {
|
|
|
|
bzrs.push(Path::new(&home).join("Bazzar/2.0/ignore"));
|
|
|
|
}
|
|
|
|
if let Ok(home) = env::var("HOME") {
|
|
|
|
bzrs.push(Path::new(&home).join(".bazarr/ignore"));
|
|
|
|
}
|
|
|
|
|
|
|
|
for path in bzrs {
|
|
|
|
if discover_file(
|
|
|
|
&mut files,
|
|
|
|
&mut errors,
|
|
|
|
None,
|
|
|
|
Some(ProjectType::Bazaar),
|
|
|
|
path,
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
{
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-15 05:25:05 +02:00
|
|
|
if let Some(name) = appname {
|
|
|
|
let mut wgis = Vec::with_capacity(4);
|
|
|
|
if let Ok(home) = env::var("XDG_CONFIG_HOME") {
|
|
|
|
wgis.push(Path::new(&home).join(format!("{name}/ignore")));
|
|
|
|
}
|
|
|
|
if let Ok(home) = env::var("APPDATA") {
|
|
|
|
wgis.push(Path::new(&home).join(format!("{name}/ignore")));
|
|
|
|
}
|
|
|
|
if let Ok(home) = env::var("USERPROFILE") {
|
|
|
|
wgis.push(Path::new(&home).join(format!(".{name}/ignore")));
|
|
|
|
}
|
|
|
|
if let Ok(home) = env::var("HOME") {
|
|
|
|
wgis.push(Path::new(&home).join(format!(".{name}/ignore")));
|
|
|
|
}
|
2021-10-10 05:03:05 +02:00
|
|
|
|
2022-06-15 05:25:05 +02:00
|
|
|
for path in wgis {
|
|
|
|
if discover_file(&mut files, &mut errors, None, None, path).await {
|
|
|
|
break;
|
|
|
|
}
|
2021-10-10 05:03:05 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
(files, errors)
|
|
|
|
}
|
|
|
|
|
2022-01-16 07:46:52 +01:00
|
|
|
// TODO: add context to these errors
|
|
|
|
|
2022-06-15 05:25:05 +02:00
|
|
|
/// Utility function to handle looking for an ignore file and adding it to a list if found.
|
|
|
|
///
|
|
|
|
/// This is mostly an internal function, but it is exposed for other filterers to use.
|
2023-01-06 14:53:49 +01:00
|
|
|
#[allow(clippy::future_not_send)]
|
|
|
|
#[tracing::instrument(skip(files, errors), level = "trace")]
|
2021-10-10 05:03:05 +02:00
|
|
|
#[inline]
|
2022-06-15 05:25:05 +02:00
|
|
|
pub async fn discover_file(
|
2021-10-10 05:03:05 +02:00
|
|
|
files: &mut Vec<IgnoreFile>,
|
|
|
|
errors: &mut Vec<Error>,
|
|
|
|
applies_in: Option<PathBuf>,
|
|
|
|
applies_to: Option<ProjectType>,
|
|
|
|
path: PathBuf,
|
|
|
|
) -> bool {
|
|
|
|
match find_file(path).await {
|
|
|
|
Err(err) => {
|
2021-12-31 13:42:39 +01:00
|
|
|
trace!(?err, "found an error");
|
2021-10-10 05:03:05 +02:00
|
|
|
errors.push(err);
|
|
|
|
false
|
|
|
|
}
|
2021-12-31 13:42:39 +01:00
|
|
|
Ok(None) => {
|
|
|
|
trace!("found nothing");
|
|
|
|
false
|
2022-01-10 08:47:06 +01:00
|
|
|
}
|
2021-10-10 05:03:05 +02:00
|
|
|
Ok(Some(path)) => {
|
2021-12-31 13:42:39 +01:00
|
|
|
trace!(?path, "found a file");
|
2021-10-10 05:03:05 +02:00
|
|
|
files.push(IgnoreFile {
|
|
|
|
path,
|
|
|
|
applies_in,
|
|
|
|
applies_to,
|
|
|
|
});
|
|
|
|
true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn find_file(path: PathBuf) -> Result<Option<PathBuf>, Error> {
|
|
|
|
match metadata(&path).await {
|
|
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
|
|
|
Err(err) => Err(err),
|
|
|
|
Ok(meta) if meta.is_file() && meta.len() > 0 => Ok(Some(path)),
|
|
|
|
Ok(_) => Ok(None),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-15 12:57:29 +01:00
|
|
|
#[derive(Debug)]
|
|
|
|
struct DirTourist {
|
2022-01-15 13:44:53 +01:00
|
|
|
base: PathBuf,
|
2022-01-15 12:57:29 +01:00
|
|
|
to_visit: Vec<PathBuf>,
|
2022-01-15 13:44:53 +01:00
|
|
|
to_skip: HashSet<PathBuf>,
|
2024-01-01 06:01:14 +01:00
|
|
|
to_explicitly_watch: HashSet<PathBuf>,
|
2022-01-15 12:57:29 +01:00
|
|
|
pub errors: Vec<std::io::Error>,
|
2022-06-15 05:25:05 +02:00
|
|
|
filter: IgnoreFilter,
|
2022-01-15 12:57:29 +01:00
|
|
|
}
|
2021-10-10 05:03:05 +02:00
|
|
|
|
2022-01-15 12:57:29 +01:00
|
|
|
#[derive(Debug)]
|
|
|
|
enum Visit {
|
|
|
|
Find(PathBuf),
|
|
|
|
Skip,
|
|
|
|
Done,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl DirTourist {
|
2024-01-01 06:01:14 +01:00
|
|
|
pub async fn new(
|
|
|
|
base: &Path,
|
|
|
|
ignore_files: &[IgnoreFile],
|
|
|
|
watch_files: &[PathBuf],
|
|
|
|
) -> Result<Self, Error> {
|
2023-01-06 14:53:49 +01:00
|
|
|
let base = canonicalize(base).await?;
|
2022-01-15 14:36:22 +01:00
|
|
|
trace!("create IgnoreFilterer for visiting directories");
|
2024-01-01 06:01:14 +01:00
|
|
|
let mut filter = IgnoreFilter::new(&base, ignore_files)
|
2022-01-15 14:36:22 +01:00
|
|
|
.await
|
|
|
|
.map_err(|err| Error::new(ErrorKind::Other, err))?;
|
|
|
|
|
2022-01-16 02:49:14 +01:00
|
|
|
filter
|
|
|
|
.add_globs(
|
2022-01-16 04:13:05 +01:00
|
|
|
&[
|
|
|
|
"/.git",
|
|
|
|
"/.hg",
|
|
|
|
"/.bzr",
|
|
|
|
"/_darcs",
|
|
|
|
"/.fossil-settings",
|
|
|
|
"/.svn",
|
|
|
|
"/.pijul",
|
|
|
|
],
|
2023-01-06 14:53:49 +01:00
|
|
|
Some(&base),
|
2022-01-16 02:49:14 +01:00
|
|
|
)
|
|
|
|
.map_err(|err| Error::new(ErrorKind::Other, err))?;
|
2022-01-15 14:36:22 +01:00
|
|
|
|
|
|
|
Ok(Self {
|
2022-01-15 13:44:53 +01:00
|
|
|
to_visit: vec![base.clone()],
|
|
|
|
base,
|
|
|
|
to_skip: HashSet::new(),
|
2024-01-04 10:32:47 +01:00
|
|
|
to_explicitly_watch: watch_files.iter().cloned().collect(),
|
2022-01-15 12:57:29 +01:00
|
|
|
errors: Vec::new(),
|
2022-01-15 14:36:22 +01:00
|
|
|
filter,
|
|
|
|
})
|
2022-01-15 12:57:29 +01:00
|
|
|
}
|
|
|
|
|
2023-01-06 14:53:49 +01:00
|
|
|
#[allow(clippy::future_not_send)]
|
2022-01-15 13:44:53 +01:00
|
|
|
pub async fn next(&mut self) -> Visit {
|
2022-01-15 12:57:29 +01:00
|
|
|
if let Some(path) = self.to_visit.pop() {
|
2023-01-06 14:53:49 +01:00
|
|
|
self.visit_path(path).await
|
|
|
|
} else {
|
|
|
|
Visit::Done
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(clippy::future_not_send)]
|
|
|
|
#[tracing::instrument(skip(self), level = "trace")]
|
|
|
|
async fn visit_path(&mut self, path: PathBuf) -> Visit {
|
|
|
|
if self.must_skip(&path) {
|
|
|
|
trace!("in skip list");
|
|
|
|
return Visit::Skip;
|
|
|
|
}
|
|
|
|
|
|
|
|
if !self.filter.check_dir(&path) {
|
2024-01-01 06:01:14 +01:00
|
|
|
trace!(?path, "path is ignored, adding to skip list");
|
|
|
|
self.skip(path);
|
|
|
|
return Visit::Skip;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If explicitly watched paths were not specified, we can include any path
|
|
|
|
//
|
|
|
|
// If explicitly watched paths *were* specified, then to include the path, either:
|
|
|
|
// - the path in question starts with an explicitly included path (/a/b starting with /a)
|
|
|
|
// - the path in question is *above* the explicitly included path (/a is above /a/b)
|
|
|
|
if self.to_explicitly_watch.is_empty()
|
|
|
|
|| self
|
|
|
|
.to_explicitly_watch
|
|
|
|
.iter()
|
|
|
|
.any(|p| path.starts_with(p) || p.starts_with(&path))
|
|
|
|
{
|
|
|
|
trace!(?path, ?self.to_explicitly_watch, "including path; it starts with one of the explicitly watched paths");
|
|
|
|
} else {
|
|
|
|
trace!(?path, ?self.to_explicitly_watch, "excluding path; it did not start with any of explicitly watched paths");
|
2023-01-06 14:53:49 +01:00
|
|
|
self.skip(path);
|
|
|
|
return Visit::Skip;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut dir = match read_dir(&path).await {
|
|
|
|
Ok(dir) => dir,
|
|
|
|
Err(err) => {
|
|
|
|
trace!("failed to read dir: {}", err);
|
|
|
|
self.errors.push(err);
|
2022-01-15 12:57:29 +01:00
|
|
|
return Visit::Skip;
|
|
|
|
}
|
2023-01-06 14:53:49 +01:00
|
|
|
};
|
2022-01-15 12:57:29 +01:00
|
|
|
|
2023-01-06 14:53:49 +01:00
|
|
|
while let Some(entry) = match dir.next_entry().await {
|
|
|
|
Ok(entry) => entry,
|
|
|
|
Err(err) => {
|
|
|
|
trace!("failed to read dir entries: {}", err);
|
|
|
|
self.errors.push(err);
|
2022-01-15 14:36:22 +01:00
|
|
|
return Visit::Skip;
|
|
|
|
}
|
2023-01-06 14:53:49 +01:00
|
|
|
} {
|
|
|
|
let path = entry.path();
|
|
|
|
let _span = trace_span!("dir_entry", ?path).entered();
|
2022-01-15 14:36:22 +01:00
|
|
|
|
2023-01-06 14:53:49 +01:00
|
|
|
if self.must_skip(&path) {
|
|
|
|
trace!("in skip list");
|
|
|
|
continue;
|
|
|
|
}
|
2022-01-15 13:44:53 +01:00
|
|
|
|
2023-01-06 14:53:49 +01:00
|
|
|
match entry.file_type().await {
|
|
|
|
Ok(ft) => {
|
|
|
|
if ft.is_dir() {
|
|
|
|
if !self.filter.check_dir(&path) {
|
|
|
|
trace!("path is ignored, adding to skip list");
|
|
|
|
self.skip(path);
|
|
|
|
continue;
|
2022-01-15 13:44:53 +01:00
|
|
|
}
|
2023-01-06 14:53:49 +01:00
|
|
|
|
|
|
|
trace!("found a dir, adding to list");
|
|
|
|
self.to_visit.push(path);
|
|
|
|
} else {
|
|
|
|
trace!("not a dir");
|
2022-01-15 13:44:53 +01:00
|
|
|
}
|
2023-01-06 14:53:49 +01:00
|
|
|
}
|
|
|
|
Err(err) => {
|
|
|
|
trace!("failed to read filetype, adding to skip list: {}", err);
|
|
|
|
self.errors.push(err);
|
|
|
|
self.skip(path);
|
2021-10-10 05:03:05 +02:00
|
|
|
}
|
|
|
|
}
|
2022-01-15 12:57:29 +01:00
|
|
|
}
|
2023-01-06 14:53:49 +01:00
|
|
|
|
|
|
|
Visit::Find(path)
|
2022-01-15 12:57:29 +01:00
|
|
|
}
|
|
|
|
|
2022-01-15 13:44:53 +01:00
|
|
|
pub fn skip(&mut self, path: PathBuf) {
|
|
|
|
let check_path = path.as_path();
|
2022-09-07 04:15:38 +02:00
|
|
|
self.to_visit.retain(|p| !p.starts_with(check_path));
|
2022-01-15 13:44:53 +01:00
|
|
|
self.to_skip.insert(path);
|
|
|
|
}
|
|
|
|
|
2022-01-15 14:36:22 +01:00
|
|
|
pub(crate) async fn add_last_file_to_filter(
|
|
|
|
&mut self,
|
2023-08-30 05:43:57 +02:00
|
|
|
files: &[IgnoreFile],
|
2022-01-16 02:49:14 +01:00
|
|
|
errors: &mut Vec<Error>,
|
|
|
|
) {
|
2022-01-15 14:36:22 +01:00
|
|
|
if let Some(ig) = files.last() {
|
|
|
|
if let Err(err) = self.filter.add_file(ig).await {
|
|
|
|
errors.push(Error::new(ErrorKind::Other, err));
|
|
|
|
}
|
|
|
|
}
|
2022-01-16 02:49:14 +01:00
|
|
|
}
|
2022-01-15 14:36:22 +01:00
|
|
|
|
2022-01-15 13:44:53 +01:00
|
|
|
fn must_skip(&self, mut path: &Path) -> bool {
|
|
|
|
if self.to_skip.contains(path) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
while let Some(parent) = path.parent() {
|
|
|
|
if parent == self.base {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if self.to_skip.contains(parent) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
path = parent;
|
|
|
|
}
|
|
|
|
|
|
|
|
false
|
2022-01-15 12:57:29 +01:00
|
|
|
}
|
|
|
|
}
|