Discover ignore files for path and for user/env

This commit is contained in:
Félix Saparelli 2021-10-10 16:03:05 +13:00
parent 8bc58ba6b5
commit 65b042ec8f
No known key found for this signature in database
GPG Key ID: B948C4BAE44FC474
8 changed files with 474 additions and 68 deletions

169
Cargo.lock generated
View File

@ -248,6 +248,9 @@ name = "cc"
version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
dependencies = [
"jobserver",
]
[[package]]
name = "cfg-if"
@ -593,6 +596,16 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
dependencies = [
"matches",
"percent-encoding",
]
[[package]]
name = "fsevent-sys"
version = "4.0.0"
@ -745,6 +758,21 @@ version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7"
[[package]]
name = "git2"
version = "0.13.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a8057932925d3a9d9e4434ea016570d37420ddb1ceed45a174d577f24ed6700"
dependencies = [
"bitflags 1.2.1",
"libc",
"libgit2-sys",
"log",
"openssl-probe",
"openssl-sys",
"url",
]
[[package]]
name = "globset"
version = "0.4.8"
@ -890,6 +918,17 @@ dependencies = [
"tokio-io-timeout",
]
[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indenter"
version = "0.3.3"
@ -971,6 +1010,15 @@ version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "jobserver"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa"
dependencies = [
"libc",
]
[[package]]
name = "kqueue"
version = "1.0.4"
@ -1016,6 +1064,46 @@ version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
[[package]]
name = "libgit2-sys"
version = "0.12.24+1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddbd6021eef06fb289a8f54b3c2acfdd85ff2a585dfbb24b8576325373d2152c"
dependencies = [
"cc",
"libc",
"libssh2-sys",
"libz-sys",
"openssl-sys",
"pkg-config",
]
[[package]]
name = "libssh2-sys"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0186af0d8f171ae6b9c4c90ec51898bad5d08a2d5e470903a50d9ad8959cbee"
dependencies = [
"cc",
"libc",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
]
[[package]]
name = "libz-sys"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linked-hash-map"
version = "0.5.4"
@ -1070,6 +1158,12 @@ dependencies = [
"regex-automata",
]
[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "memchr"
version = "2.4.1"
@ -1343,6 +1437,25 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "openssl-probe"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
[[package]]
name = "openssl-sys"
version = "0.9.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69df2d8dfc6ce3aaf44b40dec6f487d5a886516cf6879c49e98e0710f310a058"
dependencies = [
"autocfg",
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "owo-colors"
version = "1.3.0"
@ -1472,6 +1585,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb"
[[package]]
name = "polling"
version = "2.1.0"
@ -2143,6 +2262,21 @@ dependencies = [
"winapi",
]
[[package]]
name = "tinyvec"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.12.0"
@ -2411,6 +2545,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "unicode-bidi"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
[[package]]
name = "unicode-linebreak"
version = "0.1.2"
@ -2420,6 +2560,15 @@ dependencies = [
"regex",
]
[[package]]
name = "unicode-normalization"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.8.0"
@ -2444,12 +2593,30 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "url"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
dependencies = [
"form_urlencoded",
"idna",
"matches",
"percent-encoding",
]
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.3"
@ -2535,12 +2702,14 @@ name = "watchexec"
version = "1.17.1"
dependencies = [
"async-recursion",
"async-stream",
"atomic-take",
"clearscreen",
"color-eyre",
"command-group",
"dunce",
"futures",
"git2",
"globset",
"miette",
"nom 7.0.0",

View File

@ -29,6 +29,8 @@ regex = "1.5.4"
thiserror = "1.0.26"
tracing = "0.1.26"
unicase = "2.6.0"
async-stream = "0.3.2"
git2 = "0.13.22"
[dependencies.command-group]
version = "1.0.5"

View File

@ -185,13 +185,13 @@ pub struct InitConfig {
}
impl Default for InitConfig {
fn default() -> Self {
Self {
fn default() -> Self {
Self {
error_handler: Box::new(()) as _,
error_channel_size: 64,
event_channel_size: 1024,
}
}
}
}
impl InitConfig {

View File

@ -1,6 +1,6 @@
//! Error types for critical, runtime, and specialised errors.
use std::{path::PathBuf};
use std::path::PathBuf;
use miette::Diagnostic;
use thiserror::Error;
@ -9,7 +9,11 @@ use tokio::{
task::JoinError,
};
use crate::{action, event::Event, fs::{self, Watcher}};
use crate::{
action,
event::Event,
fs::{self, Watcher},
};
/// Errors which are not recoverable and stop watchexec execution.
#[derive(Debug, Diagnostic, Error)]

View File

@ -11,15 +11,14 @@ use unicase::UniCase;
use crate::error::RuntimeError;
use crate::event::{Event, Tag};
use crate::filter::Filterer;
use crate::project::ProjectType;
// to make filters
pub use globset::Glob;
pub use regex::Regex;
pub mod error;
mod parse;
pub mod swaplock;
pub mod error;
#[derive(Debug)]
pub struct TaggedFilterer {
@ -121,7 +120,11 @@ impl TaggedFilterer {
// Ok(Some(bool)) => the match was applied, bool is the result
// Ok(None) => for some precondition, the match was not done (mismatched tag, out of context, …)
fn match_tag(&self, filter: &Filter, tag: &Tag) -> Result<Option<bool>, error::TaggedFiltererError> {
fn match_tag(
&self,
filter: &Filter,
tag: &Tag,
) -> Result<Option<bool>, error::TaggedFiltererError> {
trace!(?tag, matcher=?filter.on, "matching filter to tag");
match (tag, filter.on) {
(tag, Matcher::Tag) => filter.matches(tag.discriminant_name()),
@ -220,7 +223,9 @@ impl TaggedFilterer {
///
/// This parses and compiles the glob, and wraps any error with nice [miette] diagnostics.
pub fn glob(s: &str) -> Result<Pattern, error::TaggedFiltererError> {
Glob::new(s).map_err(error::TaggedFiltererError::GlobParse).map(|g| Pattern::Glob(g.compile_matcher()))
Glob::new(s)
.map_err(error::TaggedFiltererError::GlobParse)
.map(|g| Pattern::Glob(g.compile_matcher()))
}
}
@ -269,38 +274,6 @@ impl Filter {
}
})
}
/// Returns a set of ignores for the given project type.
pub fn for_project(project_type: ProjectType) -> Vec<Self> {
const ERR: &str = "static pattern";
// TODO: use gitignores
match project_type {
ProjectType::Git => vec![
Self::from_glob_ignore("/.git").expect(ERR),
],
ProjectType::Mercurial => todo!(),
ProjectType::Pijul => todo!(),
ProjectType::Fossil => todo!(),
ProjectType::Cargo => vec![
Self::from_glob_ignore("/debug").expect(ERR),
Self::from_glob_ignore("/target").expect(ERR),
Self::from_glob_ignore("Cargo.lock").expect(ERR),
Self::from_glob_ignore("**/*.rs.bk").expect(ERR),
Self::from_glob_ignore("*.pdb").expect(ERR),
],
ProjectType::JavaScript => todo!(),
ProjectType::Bundler => todo!(),
ProjectType::RubyGem => todo!(),
ProjectType::Pip => todo!(),
}
}
pub(crate) fn from_glob_ignore(glob: impl AsRef<str>) -> Result<Self, error::TaggedFiltererError> {
let glob = Glob::new(glob.as_ref())?.compile_matcher();
Ok(Self { in_path: None, on: Matcher::Path, op: Op::NotGlob, pat: Pattern::Glob(glob), negate: false })
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]

View File

@ -1,12 +1,15 @@
//! Error type for TaggedFilterer.
use std::{collections::HashMap};
use std::collections::HashMap;
use miette::Diagnostic;
use thiserror::Error;
use tokio::sync::watch::{error::SendError};
use tokio::sync::watch::error::SendError;
use crate::{error::RuntimeError, filter::tagged::{Filter, Matcher}};
use crate::{
error::RuntimeError,
filter::tagged::{Filter, Matcher},
};
/// Errors emitted by the TaggedFilterer.
#[derive(Debug, Diagnostic, Error)]
@ -42,10 +45,10 @@ pub enum TaggedFiltererError {
}
impl From<TaggedFiltererError> for RuntimeError {
fn from(err: TaggedFiltererError) -> Self {
Self::Filterer {
fn from(err: TaggedFiltererError) -> Self {
Self::Filterer {
kind: "tagged",
err: Box::new(err) as _,
}
}
}
}

View File

@ -1,13 +1,29 @@
//! Find ignore files, like `.gitignore`, `.ignore`, and others.
use std::path::{Path, PathBuf};
use std::{
env,
io::{Error, ErrorKind},
path::{Path, PathBuf},
};
use crate::{error::RuntimeError, project::ProjectType};
use futures::{pin_mut, Stream, StreamExt};
use tokio::fs::{metadata, read_dir};
use crate::project::ProjectType;
/// 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).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct IgnoreFile {
/// The path to the ignore file.
pub path: PathBuf,
pub applies_in: PathBuf,
/// 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.
pub applies_to: Option<ProjectType>,
}
@ -22,22 +38,257 @@ pub struct IgnoreFile {
///
/// 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
/// [`project::origin`](crate::project::origin) function for that.
/// [`project::origins`](crate::project::origins) function for that.
///
/// 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.
pub async fn from_origin(path: impl AsRef<Path>) -> Result<Vec<IgnoreFile>, RuntimeError> {
todo!()
///
/// 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;
let dirs = all_dirs(base);
pin_mut!(dirs);
while let Some(p) = dirs.next().await {
match p {
Err(err) => errors.push(err),
Ok(dir) => {
discover_file(
&mut files,
&mut errors,
Some(dir.clone()),
None,
dir.join(".ignore"),
)
.await;
discover_file(
&mut files,
&mut errors,
Some(dir.clone()),
Some(ProjectType::Git),
dir.join(".gitignore"),
)
.await;
discover_file(
&mut files,
&mut errors,
Some(dir.clone()),
Some(ProjectType::Mercurial),
dir.join(".hgignore"),
)
.await;
}
}
}
(files, errors)
}
/// Finds all ignore files that apply to the current runtime.
///
/// This considers:
/// - System-wide ignore files (e.g. `/etc/git/ignore`)
/// - User-specific ignore files (e.g. `~/.gitignore`)
/// - User-specific git ignore files (e.g. `~/.gitignore`)
/// - Git configurable ignore files (e.g. with `core.excludesFile` in system or user config)
/// - Other VCS ignore files in system and user locations and config (e.g. `~/.hgignore`)
/// - Files from the `WATCHEXEC_IGNORE_FILES` environment variable (comma-separated).
pub async fn from_environment() -> Result<Vec<IgnoreFile>, RuntimeError> {
todo!()
/// - `$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()
.split(',')
{
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;
}
}
}
let mut homes = Vec::with_capacity(5);
if let Ok(home) = env::var("XDG_CONFIG_HOME") {
homes.push(Path::new(&home).join("watchexec/ignore"));
}
if let Ok(home) = env::var("APPDATA") {
homes.push(Path::new(&home).join("watchexec/ignore"));
}
if let Ok(home) = env::var("USERPROFILE") {
homes.push(Path::new(&home).join(".watchexec/ignore"));
}
if let Ok(home) = env::var("HOME") {
homes.push(Path::new(&home).join(".watchexec/ignore"));
}
for path in homes {
if discover_file(&mut files, &mut errors, None, None, path).await {
break;
}
}
(files, errors)
}
#[inline]
async fn discover_file(
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) => {
errors.push(err);
false
}
Ok(None) => false,
Ok(Some(path)) => {
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),
}
}
fn all_dirs(path: PathBuf) -> impl Stream<Item = Result<PathBuf, Error>> {
async_stream::try_stream! {
yield path.clone();
let mut to_visit = vec![path];
while let Some(path) = to_visit.pop() {
let mut dir = read_dir(&path).await?;
while let Some(entry) = dir.next_entry().await? {
if entry.file_type().await?.is_dir() {
let path = entry.path();
to_visit.push(path.clone());
yield path;
}
}
}
}
}

View File

@ -1,31 +1,35 @@
//! Detect project type and origin.
use std::path::{Path, PathBuf};
use std::{
io::Error,
path::{Path, PathBuf},
};
use crate::error::CriticalError;
pub async fn origin(path: impl AsRef<Path>) -> Result<PathBuf, CriticalError> {
pub async fn origins(path: impl AsRef<Path>) -> Result<Vec<PathBuf>, Error> {
todo!()
}
/// Returns all project types detected at this given origin.
///
/// This should be called with the result of [`origin()`], or a project origin if already known; it
/// This should be called with a result of [`origins()`], or a project origin if already known; it
/// will not find the origin itself.
pub async fn types(path: impl AsRef<Path>) -> Result<Vec<ProjectType>, CriticalError> {
pub async fn types(path: impl AsRef<Path>) -> Result<Vec<ProjectType>, Error> {
todo!()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ProjectType {
Bazaar,
Darcs,
Fossil,
Git,
Mercurial,
Pijul,
Fossil,
Bundler,
Cargo,
JavaScript,
Bundler,
RubyGem,
Pip,
RubyGem,
}