2021-10-10 05:03:05 +02:00
|
|
|
use std::{
|
|
|
|
env,
|
|
|
|
io::{Error, ErrorKind},
|
|
|
|
path::{Path, PathBuf},
|
|
|
|
};
|
2021-10-09 07:45:32 +02:00
|
|
|
|
2021-10-10 05:03:05 +02:00
|
|
|
use tokio::fs::{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-01-15 11:46:06 +01:00
|
|
|
use crate::{paths::PATH_SEPARATOR, project::ProjectType};
|
2021-10-10 05:03:05 +02:00
|
|
|
|
2022-01-15 12:57:29 +01:00
|
|
|
use super::IgnoreFilterer;
|
|
|
|
|
2021-10-10 05:03:05 +02:00
|
|
|
/// An ignore file.
|
|
|
|
///
|
|
|
|
/// This records both the path to the ignore file and some basic metadata about it: which project
|
|
|
|
/// type it applies to if any, and which subtree it applies in if any (`None` = global ignore file).
|
2021-10-09 07:45:32 +02:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
|
|
pub struct IgnoreFile {
|
2021-10-10 05:03:05 +02:00
|
|
|
/// The path to the ignore file.
|
2021-10-09 07:45:32 +02:00
|
|
|
pub path: PathBuf,
|
2021-10-10 05:03:05 +02:00
|
|
|
|
|
|
|
/// The path to the subtree the ignore file applies to, or `None` for global ignores.
|
|
|
|
pub applies_in: Option<PathBuf>,
|
|
|
|
|
|
|
|
/// Which project type the ignore file applies to, or was found through.
|
2021-10-09 07:45:32 +02:00
|
|
|
pub applies_to: Option<ProjectType>,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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
|
2021-10-10 05:03:05 +02:00
|
|
|
/// [`project::origins`](crate::project::origins) function 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**.
|
|
|
|
pub async fn from_origin(path: impl AsRef<Path>) -> (Vec<IgnoreFile>, Vec<Error>) {
|
|
|
|
let base = path.as_ref().to_owned();
|
|
|
|
let mut files = Vec::new();
|
|
|
|
let mut errors = Vec::new();
|
|
|
|
|
|
|
|
match find_file(base.join(".git/config")).await {
|
|
|
|
Err(err) => errors.push(err),
|
|
|
|
Ok(None) => {}
|
|
|
|
Ok(Some(path)) => match git2::Config::open(&path) {
|
|
|
|
Err(err) => errors.push(Error::new(ErrorKind::Other, err)),
|
|
|
|
Ok(config) => {
|
|
|
|
if let Ok(excludes) = config.get_path("core.excludesFile") {
|
|
|
|
discover_file(
|
|
|
|
&mut files,
|
|
|
|
&mut errors,
|
|
|
|
None,
|
|
|
|
Some(ProjectType::Git),
|
|
|
|
excludes,
|
|
|
|
)
|
|
|
|
.await;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
discover_file(
|
|
|
|
&mut files,
|
|
|
|
&mut errors,
|
|
|
|
Some(base.clone()),
|
|
|
|
Some(ProjectType::Bazaar),
|
|
|
|
base.join(".bzrignore"),
|
|
|
|
)
|
|
|
|
.await;
|
|
|
|
|
|
|
|
discover_file(
|
|
|
|
&mut files,
|
|
|
|
&mut errors,
|
|
|
|
Some(base.clone()),
|
|
|
|
Some(ProjectType::Darcs),
|
|
|
|
base.join("_darcs/prefs/boring"),
|
|
|
|
)
|
|
|
|
.await;
|
|
|
|
|
|
|
|
discover_file(
|
|
|
|
&mut files,
|
|
|
|
&mut errors,
|
|
|
|
Some(base.clone()),
|
|
|
|
Some(ProjectType::Fossil),
|
|
|
|
base.join(".fossil-settings/ignore-glob"),
|
|
|
|
)
|
|
|
|
.await;
|
|
|
|
|
|
|
|
discover_file(
|
|
|
|
&mut files,
|
|
|
|
&mut errors,
|
|
|
|
Some(base.clone()),
|
|
|
|
Some(ProjectType::Git),
|
|
|
|
base.join(".git/info/exclude"),
|
|
|
|
)
|
|
|
|
.await;
|
|
|
|
|
2022-01-15 12:57:29 +01:00
|
|
|
trace!("create IgnoreFilterer for visiting directories");
|
|
|
|
let mut search_filter = IgnoreFilterer::new(&base, &files.iter().cloned().collect::<Vec<_>>())
|
|
|
|
.await
|
|
|
|
.map_err(|err| errors.push(Error::new(ErrorKind::Other, err)))
|
|
|
|
.ok();
|
|
|
|
|
|
|
|
trace!("visiting child directories for ignore files");
|
|
|
|
let mut dirs = DirTourist::new(base);
|
|
|
|
loop {
|
|
|
|
match dirs.next().await {
|
|
|
|
Visit::Done => break,
|
|
|
|
Visit::Skip => continue,
|
|
|
|
Visit::Find(dir) => {
|
|
|
|
if let Some(sf) = &search_filter {
|
|
|
|
if sf.check_dir(&dir) {
|
|
|
|
trace!(?dir, "dir is ignored, adding to skiplist");
|
|
|
|
dirs.skip(dir);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if discover_file(
|
2021-10-10 05:03:05 +02:00
|
|
|
&mut files,
|
|
|
|
&mut errors,
|
|
|
|
Some(dir.clone()),
|
|
|
|
None,
|
|
|
|
dir.join(".ignore"),
|
|
|
|
)
|
2022-01-15 12:57:29 +01:00
|
|
|
.await
|
|
|
|
{
|
|
|
|
add_last_file_to_filter(&mut search_filter, &mut files, &mut errors).await;
|
|
|
|
}
|
2021-10-10 05:03:05 +02:00
|
|
|
|
2022-01-15 12:57:29 +01:00
|
|
|
if discover_file(
|
2021-10-10 05:03:05 +02:00
|
|
|
&mut files,
|
|
|
|
&mut errors,
|
|
|
|
Some(dir.clone()),
|
|
|
|
Some(ProjectType::Git),
|
|
|
|
dir.join(".gitignore"),
|
|
|
|
)
|
2022-01-15 12:57:29 +01:00
|
|
|
.await
|
|
|
|
{
|
|
|
|
add_last_file_to_filter(&mut search_filter, &mut files, &mut errors).await;
|
|
|
|
}
|
2021-10-10 05:03:05 +02:00
|
|
|
|
2022-01-15 12:57:29 +01:00
|
|
|
if discover_file(
|
2021-10-10 05:03:05 +02:00
|
|
|
&mut files,
|
|
|
|
&mut errors,
|
|
|
|
Some(dir.clone()),
|
|
|
|
Some(ProjectType::Mercurial),
|
|
|
|
dir.join(".hgignore"),
|
|
|
|
)
|
2022-01-15 12:57:29 +01:00
|
|
|
.await
|
|
|
|
{
|
|
|
|
add_last_file_to_filter(&mut search_filter, &mut files, &mut errors).await;
|
|
|
|
}
|
2021-10-10 05:03:05 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-15 12:57:29 +01:00
|
|
|
errors.extend(dirs.errors);
|
2021-10-10 05:03:05 +02:00
|
|
|
(files, errors)
|
2021-10-09 07:45:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Finds all ignore files that apply to the current runtime.
|
|
|
|
///
|
|
|
|
/// 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)
|
2021-10-10 05:03:05 +02:00
|
|
|
/// - `$XDG_CONFIG_HOME/watchexec/ignore`, as well as other locations (APPDATA on Windows…)
|
|
|
|
/// - Files from the `WATCHEXEC_IGNORE_FILES` environment variable (comma-separated)
|
|
|
|
///
|
|
|
|
/// 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).
|
|
|
|
pub async fn from_environment() -> (Vec<IgnoreFile>, Vec<Error>) {
|
|
|
|
let mut files = Vec::new();
|
|
|
|
let mut errors = Vec::new();
|
|
|
|
|
|
|
|
for path in env::var("WATCHEXEC_IGNORE_FILES")
|
|
|
|
.unwrap_or_default()
|
2022-01-15 03:58:11 +01:00
|
|
|
.split(PATH_SEPARATOR)
|
2021-10-10 05:03:05 +02:00
|
|
|
{
|
|
|
|
discover_file(&mut files, &mut errors, None, None, PathBuf::from(path)).await;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut found_git_global = false;
|
|
|
|
match git2::Config::open_default() {
|
|
|
|
Err(err) => errors.push(Error::new(ErrorKind::Other, err)),
|
|
|
|
Ok(config) => {
|
|
|
|
if let Ok(excludes) = config.get_path("core.excludesFile") {
|
|
|
|
if discover_file(
|
|
|
|
&mut files,
|
|
|
|
&mut errors,
|
|
|
|
None,
|
|
|
|
Some(ProjectType::Git),
|
|
|
|
excludes,
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
{
|
|
|
|
found_git_global = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut wgis = Vec::with_capacity(5);
|
2021-10-10 05:03:05 +02:00
|
|
|
if let Ok(home) = env::var("XDG_CONFIG_HOME") {
|
2021-10-10 05:06:56 +02:00
|
|
|
wgis.push(Path::new(&home).join("watchexec/ignore"));
|
2021-10-10 05:03:05 +02:00
|
|
|
}
|
|
|
|
if let Ok(home) = env::var("APPDATA") {
|
2021-10-10 05:06:56 +02:00
|
|
|
wgis.push(Path::new(&home).join("watchexec/ignore"));
|
2021-10-10 05:03:05 +02:00
|
|
|
}
|
|
|
|
if let Ok(home) = env::var("USERPROFILE") {
|
2021-10-10 05:06:56 +02:00
|
|
|
wgis.push(Path::new(&home).join(".watchexec/ignore"));
|
2021-10-10 05:03:05 +02:00
|
|
|
}
|
|
|
|
if let Ok(home) = env::var("HOME") {
|
2021-10-10 05:06:56 +02:00
|
|
|
wgis.push(Path::new(&home).join(".watchexec/ignore"));
|
2021-10-10 05:03:05 +02:00
|
|
|
}
|
|
|
|
|
2021-10-10 05:06:56 +02:00
|
|
|
for path in wgis {
|
2021-10-10 05:03:05 +02:00
|
|
|
if discover_file(&mut files, &mut errors, None, None, path).await {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
(files, errors)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[inline]
|
2021-12-23 14:20:45 +01:00
|
|
|
pub(crate) 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 {
|
2021-12-31 13:42:39 +01:00
|
|
|
let _span = trace_span!("discover_file", ?path, ?applies_in, ?applies_to).entered();
|
2021-10-10 05:03:05 +02:00
|
|
|
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 {
|
|
|
|
to_visit: Vec<PathBuf>,
|
|
|
|
to_skip: Vec<PathBuf>,
|
|
|
|
pub errors: Vec<std::io::Error>,
|
|
|
|
}
|
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 {
|
|
|
|
fn new(start: PathBuf) -> Self {
|
|
|
|
DirTourist {
|
|
|
|
to_visit: vec![start],
|
|
|
|
to_skip: Vec::new(),
|
|
|
|
errors: Vec::new(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn next(&mut self) -> Visit {
|
|
|
|
if let Some(path) = self.to_visit.pop() {
|
|
|
|
if self.to_skip.contains(&path) {
|
|
|
|
trace!("in skip list");
|
|
|
|
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);
|
|
|
|
return Visit::Skip;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
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);
|
|
|
|
return Visit::Skip;
|
|
|
|
}
|
|
|
|
} {
|
|
|
|
if match entry.file_type().await {
|
|
|
|
Ok(ft) => ft.is_dir(),
|
|
|
|
Err(err) => {
|
|
|
|
trace!(entry=?entry.path(), "failed to read filetype, adding to skip list: {}", err);
|
|
|
|
self.errors.push(err);
|
|
|
|
self.to_skip.push(entry.path());
|
|
|
|
false
|
|
|
|
}
|
|
|
|
} {
|
2021-10-10 05:03:05 +02:00
|
|
|
let path = entry.path();
|
2022-01-15 12:57:29 +01:00
|
|
|
trace!(?path, "found a dir, adding to list");
|
|
|
|
self.to_visit.push(path.clone());
|
2021-10-10 05:03:05 +02:00
|
|
|
}
|
|
|
|
}
|
2022-01-15 12:57:29 +01:00
|
|
|
|
|
|
|
Visit::Find(path)
|
|
|
|
} else {
|
|
|
|
Visit::Done
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn skip(&mut self, path: PathBuf) {
|
|
|
|
self.to_skip.push(path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn add_last_file_to_filter(
|
|
|
|
filter: &mut Option<IgnoreFilterer>,
|
|
|
|
files: &mut Vec<IgnoreFile>,
|
|
|
|
errors: &mut Vec<Error>,
|
|
|
|
) {
|
|
|
|
if let Some(igf) = filter.as_mut() {
|
|
|
|
if let Some(ig) = files.last() {
|
|
|
|
if let Err(err) = igf.add_file(ig).await {
|
|
|
|
errors.push(Error::new(ErrorKind::Other, err));
|
|
|
|
}
|
2021-10-10 05:03:05 +02:00
|
|
|
}
|
|
|
|
}
|
2021-10-09 07:45:32 +02:00
|
|
|
}
|