Repository: Canop/bacon (language: rust)

Issue:
ignore list in job
I read https://github.com/watchexec/cargo-watch/releases/tag/v8.5.3 and I would like to know how to use Bacon instead of it.

Right now the only usage I need is to relaunch the project with `cargo run` if some file changes in some specific directory excluding some files.

I'm using:

```shell
cargo watch --ignore "dir1" --ignore "dir2/dir3/dir4" --ignore "dir7" --ignore "tests.rs" -x run
```

Is there an equivalent bacon command?

_Thanks a lot for your job!_


Discussion / hints:
Do you really need to ignore files which aren't in the .gitignore ?

If not, just do

    bacon run

Or alternatively, launch bacon with `bacon`, then hit <kbd>r</kbd>.
Thank you for the answer.

I don't understand: is there a way to launch bacon with that "ignore" flags?

Or using a .toml file?

I need to ignore many directories which are NOT ignored in .gitignore.
> is there a way to launch bacon with that "ignore" flags?

If you don't want bacon to watch the default "source" files and directories (`["src", "tests", "benches", "examples", "build.rs"]`), you may set `default_watch` to `false` and set your own `watch` list: https://dystroy.org/bacon/config/#jobs

But we might have a misunderstanding: what are those directories you want to ignore that aren't currently ignored by bacon ?
In one of my project for example I have a "sources" directory which I change manually or from another software.

Then this directory content is used in the cargo project I'm watching. But I don't want to re-run the project when that directory content changes.

And this directory is not in .gitignore.

You're suggesting a whitelist. I'm searching for a blacklist, right?
> You're suggesting a whitelist. I'm searching for a blacklist, right?

Before to add a blacklist feature in bacon, I'm trying to understand how one can end up needing a blacklist, and whether that's a real need. There are already many parameters in bacon and I never rush to add some before verifying they're useful.

If your folder is called "sources", then it's not watched by bacon anyway. But  perhaps you meant it's inside one of the  `["src", "tests", "benches", "examples", "build.rs"]` directories ?

In such case, the best solution would be to add a `ignore = [...]` setting in jobs, right ?
Yeah, it's inside a sub-directory under the main "src" one.

Any option is fine for me.

It would also be useful to have it as an arg for the bacon executable, as it is for `cargo watch`:

```
cargo watch --ignore "dir1" --ignore "dir2/dir3/dir4" --ignore "dir7" --ignore "tests.rs" -x run
```

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index afe13e4..de3dd0b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 - fix Miri output seen as wrong when there's only warnings
 - more lenient detection of warnings and errors due to 'miri run' not supporting `--color` - Fix #251
 - eslint analyzer (set `analyzer = "eslint"` in your job definition)
+- new `ignore` job parameter, accepts a list of glob patterns
 
 <a name="v3.2.0"></a>
 ### v3.2.0 - 2024/11/04
diff --git a/Cargo.lock b/Cargo.lock
index b21a82a..f60be01 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -135,6 +135,7 @@ dependencies = [
  "crokey",
  "directories-next",
  "gix",
+ "glob",
  "lazy-regex",
  "notify",
  "rustc-hash",
@@ -1093,6 +1094,12 @@ dependencies = [
  "gix-path",
 ]
 
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
 [[package]]
 name = "hashbrown"
 version = "0.14.5"
diff --git a/Cargo.toml b/Cargo.toml
index 3839aee..6189d18 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,6 +20,7 @@ cli-log = "2.1"
 crokey = "1.1"
 directories-next = "2.0.0"
 gix = { version = "0.67", default-features = false, features = ["index", "excludes", "parallel"] }
+glob = "0.3"
 lazy-regex = "3.3"
 notify = "7.0"
 rustc-hash = "2"
diff --git a/src/app.rs b/src/app.rs
index 1dba21b..f0540ce 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -91,7 +91,7 @@ fn run_mission(
     let mission_watcher = Watcher::new(&mission.paths_to_watch, ignorer)?;
 
     // create the watcher for config file changes
-    let config_watcher = Watcher::new(&mission.settings.config_files, None)?;
+    let config_watcher = Watcher::new(&mission.settings.config_files, IgnorerSet::default())?;
 
     // create the executor, mission, and state
     let mut executor = MissionExecutor::new(&mission)?;
diff --git a/src/conf/config.rs b/src/conf/config.rs
index e0cd19d..485ea80 100644
--- a/src/conf/config.rs
+++ b/src/conf/config.rs
@@ -21,6 +21,14 @@ pub struct Config {
 
     pub default_job: Option<ConcreteJobRef>,
 
+    /// Whether to apply the default watch list, which is
+    /// `["src", "tests", "benches", "examples", "build.rs"]`
+    ///
+    /// This is true by default. Set it to false if you want
+    /// to watch nothing, or only the directories you set in
+    /// `watch`.
+    pub default_watch: Option<bool>,
+
     /// locations export
     #[deprecated(since = "2.22.0", note = "use exports.locations")]
     pub export: Option<ExportConfig>,
@@ -31,8 +39,21 @@ pub struct Config {
     #[serde(default)]
     pub exports: HashMap<String, ExportConfig>,
 
+    /// The delay between a file event and the real start of the
+    /// task. Other file events occuring during this period will be
+    /// ignored.
+    pub grace_period: Option<Period>,
+
     pub help_line: Option<bool>,
 
+    /// A lit of glob patterns to ignore
+    #[serde(default)]
+    pub ignore: Vec<String>,
+
+    /// Patterns of lines which should be ignored. Patterns of
+    /// the prefs or bacon.toml can be overridden at the job
+    pub ignored_lines: Option<Vec<LinePattern>>,
+
     #[serde(default)]
     pub jobs: HashMap<String, Job>,
 
@@ -49,31 +70,14 @@ pub struct Config {
     #[deprecated(since = "2.0.0", note = "use keybindings")]
     pub vim_keys: Option<bool>,
 
-    pub wrap: Option<bool>,
-
-    /// Patterns of lines which should be ignored. Patterns of
-    /// the prefs or bacon.toml can be overridden at the job
-    pub ignored_lines: Option<Vec<LinePattern>>,
-
-    /// The delay between a file event and the real start of the
-    /// task. Other file events occuring during this period will be
-    /// ignored.
-    pub grace_period: Option<Period>,
-
-    /// Whether to apply the default watch list, which is
-    /// `["src", "tests", "benches", "examples", "build.rs"]`
-    ///
-    /// This is true by default. Set it to false if you want
-    /// to watch nothing, or only the directories you set in
-    /// `watch`.
-    pub default_watch: Option<bool>,
-
     /// A list of files and directories that will be watched if the job
     /// is run on a package.
     ///
     /// src, examples, tests, benches, and build.rs are implicitly
     /// included unless you `set default_watch` to false.
     pub watch: Option<Vec<String>>,
+
+    pub wrap: Option<bool>,
 }
 
 impl Config {
diff --git a/src/conf/settings.rs b/src/conf/settings.rs
index 0c8f740..52c3dce 100644
--- a/src/conf/settings.rs
+++ b/src/conf/settings.rs
@@ -21,29 +21,30 @@ use {
 /// They're immutable during the execution of the missions.
 #[derive(Debug, Clone)]
 pub struct Settings {
-    pub arg_job: Option<ConcreteJobRef>,
-    pub additional_job_args: Vec<String>,
     pub additional_alias_args: Option<Vec<String>>,
-    pub summary: bool,
-    pub wrap: bool,
-    pub reverse: bool,
-    pub help_line: bool,
-    pub no_default_features: bool,
+    pub additional_job_args: Vec<String>,
     pub all_features: bool,
-    pub features: Option<String>, // comma separated list
-    pub keybindings: KeyBindings,
-    pub jobs: HashMap<String, Job>,
-    pub default_job: ConcreteJobRef,
-    pub exports: ExportsSettings,
-    pub show_changes_count: bool,
-    pub on_change_strategy: Option<OnChangeStrategy>,
-    pub ignored_lines: Option<Vec<LinePattern>>,
-    pub grace_period: Period,
+    pub arg_job: Option<ConcreteJobRef>,
     /// Path of the files which were used to build the settings
     /// (note that not all settings come from files)
     pub config_files: Vec<PathBuf>,
+    pub default_job: ConcreteJobRef,
     pub default_watch: bool,
+    pub exports: ExportsSettings,
+    pub features: Option<String>, // comma separated list
+    pub grace_period: Period,
+    pub help_line: bool,
+    pub ignore: Vec<String>,
+    pub ignored_lines: Option<Vec<LinePattern>>,
+    pub jobs: HashMap<String, Job>,
+    pub keybindings: KeyBindings,
+    pub no_default_features: bool,
+    pub on_change_strategy: Option<OnChangeStrategy>,
+    pub reverse: bool,
+    pub show_changes_count: bool,
+    pub summary: bool,
     pub watch: Vec<String>,
+    pub wrap: bool,
 }
 
 impl Default for Settings {
@@ -65,6 +66,7 @@ impl Default for Settings {
             exports: Default::default(),
             show_changes_count: false,
             on_change_strategy: None,
+            ignore: Default::default(),
             ignored_lines: Default::default(),
             grace_period: Duration::from_millis(5).into(),
             config_files: Default::default(),
@@ -202,6 +204,9 @@ impl Settings {
         if let Some(watch) = config.watch.as_ref() {
             self.watch = watch.clone();
         }
+        for pattern in &config.ignore {
+            self.ignore.push(pattern.clone());
+        }
     }
     pub fn apply_args(
         &mut self,
diff --git a/src/ignorer.rs b/src/ignorer/git_ignorer.rs
similarity index 84%
rename from src/ignorer.rs
rename to src/ignorer/git_ignorer.rs
index afb087a..2c9b7a5 100644
--- a/src/ignorer.rs
+++ b/src/ignorer/git_ignorer.rs
@@ -1,4 +1,5 @@
 use {
+    crate::*,
     anyhow::{
         Context,
         Result,
@@ -7,19 +8,16 @@ use {
         self as git,
         Repository,
     },
-    std::path::{
-        Path,
-        PathBuf,
-    },
+    std::path::Path,
 };
 
 /// An object able to tell whether a file is excluded
 /// by gitignore rules
-pub struct Ignorer {
+pub struct GitIgnorer {
     repo: Repository,
 }
 
-impl Ignorer {
+impl GitIgnorer {
     /// Create an Ignorer from any directory path: the closest
     /// surrounding git repository will be found (if there's one)
     /// and its gitignore rules used.
@@ -29,15 +27,20 @@ impl Ignorer {
         let repo = git::discover(root_path)?;
         Ok(Self { repo })
     }
+}
 
-    /// Tell whether all given paths are excluded according to
-    /// either the global gitignore rules or the ones of the repository.
-    ///
-    /// Return Ok(false) when at least one file is included (i.e. we should
-    /// execute the job)
-    pub fn excludes_all(
+impl Ignorer for GitIgnorer {
+    fn excludes(
+        &mut self,
+        paths: &Path,
+    ) -> Result<bool> {
+        self.excludes_all_paths(&[paths])
+    }
+}
+impl GitIgnorer {
+    fn excludes_all_paths(
         &mut self,
-        paths: &[PathBuf],
+        paths: &[&Path],
     ) -> Result<bool> {
         let worktree = self.repo.worktree().context("a worktree should exist")?;
 
diff --git a/src/ignorer/glob_ignorer.rs b/src/ignorer/glob_ignorer.rs
new file mode 100644
index 0000000..f6c8c1e
--- /dev/null
+++ b/src/ignorer/glob_ignorer.rs
@@ -0,0 +1,45 @@
+use {
+    crate::*,
+    anyhow::Result,
+    std::path::Path,
+};
+
+#[derive(Default)]
+pub struct GlobIgnorer {
+    globs: Vec<glob::Pattern>,
+}
+
+impl GlobIgnorer {
+    pub fn add(
+        &mut self,
+        pattern: &str,
+        root: &Path,
+    ) -> Result<()> {
+        if pattern.starts_with('/') {
+            self.globs.push(glob::Pattern::new(pattern)?);
+            // it's probably a path relative to the root of the package
+            let pattern = root.join(pattern);
+            let pattern = pattern.to_string_lossy();
+            self.globs.push(glob::Pattern::new(&pattern)?);
+        } else {
+            // as glob doesn't work with non absolute paths, we make it absolute
+            self.globs
+                .push(glob::Pattern::new(&format!("/**/{}", pattern))?);
+        }
+        Ok(())
+    }
+}
+
+impl Ignorer for GlobIgnorer {
+    fn excludes(
+        &mut self,
+        paths: &Path,
+    ) -> Result<bool> {
+        for glob in &self.globs {
+            if glob.matches_path(paths) {
+                return Ok(true);
+            }
+        }
+        Ok(false)
+    }
+}
diff --git a/src/ignorer/mod.rs b/src/ignorer/mod.rs
new file mode 100644
index 0000000..f816d88
--- /dev/null
+++ b/src/ignorer/mod.rs
@@ -0,0 +1,62 @@
+use {
+    anyhow::Result,
+    std::path::{
+        Path,
+        PathBuf,
+    },
+};
+
+mod git_ignorer;
+mod glob_ignorer;
+
+pub use {
+    git_ignorer::GitIgnorer,
+    glob_ignorer::GlobIgnorer,
+};
+
+pub trait Ignorer {
+    /// Tell whether all given paths are excluded according to
+    /// either the global gitignore rules or the ones of the repository.
+    ///
+    /// Return Ok(false) when at least one file is included (i.e. we should
+    /// execute the job)
+    fn excludes(
+        &mut self,
+        paths: &Path,
+    ) -> Result<bool>;
+}
+
+/// A set of ignorers
+#[derive(Default)]
+pub struct IgnorerSet {
+    ignorers: Vec<Box<dyn Ignorer + Send>>,
+}
+impl IgnorerSet {
+    pub fn add(
+        &mut self,
+        ignorer: Box<dyn Ignorer + Send>,
+    ) {
+        self.ignorers.push(ignorer);
+    }
+    pub fn excludes_all_pathbufs(
+        &mut self,
+        paths: &[PathBuf],
+    ) -> Result<bool> {
+        if self.ignorers.is_empty() {
+            return Ok(false);
+        }
+        for path in paths {
+            let mut excluded = false;
+            for ignorer in &mut self.ignorers {
+                if ignorer.excludes(path)? {
+                    excluded = true;
+                    break;
+                }
+            }
+            if !excluded {
+                return Ok(false);
+            }
+        }
+        Ok(true)
+    }
+}
diff --git a/src/jobs/job.rs b/src/jobs/job.rs
index 50ee2bb..6218448 100644
--- a/src/jobs/job.rs
+++ b/src/jobs/job.rs
@@ -18,6 +18,10 @@ pub struct Job {
     #[serde(default)]
     pub allow_warnings: bool,
 
+    /// The analyzer interpreting the output of the command, the
+    /// standard cargo dedicated one if not provided
+    pub analyzer: Option<Analyzer>,
+
     /// Whether gitignore rules must be applied
     pub apply_gitignore: Option<bool>,
 
@@ -36,8 +40,13 @@ pub struct Job {
     /// by the PackageConfig::from_path loader
     pub command: Vec<String>,
 
-    /// A kill command. If not provided, SIGKILL is used.
-    pub kill: Option<Vec<String>>,
+    /// Whether to apply the default watch list, which is
+    /// `["src", "tests", "benches", "examples", "build.rs"]`
+    ///
+    /// This is true by default. Set it to false if you want
+    /// to watch nothing, or only the directories you set in
+    /// `watch`.
+    pub default_watch: Option<bool>,
 
     /// Env vars to set for this job execution
     #[serde(default)]
@@ -47,48 +56,43 @@ pub struct Job {
     #[serde(default = "default_true")]
     pub expand_env_vars: bool,
 
+    /// Whether to insert extraneous arguments provided by bacon or end users
+    ///
+    /// Eg: --all-features or anything after -- in bacon incantation
+    #[serde(default = "default_true")]
+    pub extraneous_args: bool,
+
+    /// A lit of glob patterns to ignore
+    #[serde(default)]
+    pub ignore: Vec<String>,
+
+    /// Patterns of lines which should be ignored. Patterns of
+    /// the prefs or bacon.toml can be overridden at the job
+    pub ignored_lines: Option<Vec<LinePattern>>,
+
+    /// A kill command. If not provided, SIGKILL is used.
+    pub kill: Option<Vec<String>>,
+
     /// Whether we need to capture stdout too (stderr is
     /// always captured)
     #[serde(default)]
     pub need_stdout: bool,
 
+    /// How to handle changes: either immediately kill the current job
+    /// then restart it, or wait for the current job to finish before
+    /// restarting it.
+    pub on_change_strategy: Option<OnChangeStrategy>,
+
     /// The optional action to run when there's no
     /// error, warning or test failures
     #[serde(default)]
     pub on_success: Option<Action>,
 
-    /// Whether to apply the default watch list, which is
-    /// `["src", "tests", "benches", "examples", "build.rs"]`
-    ///
-    /// This is true by default. Set it to false if you want
-    /// to watch nothing, or only the directories you set in
-    /// `watch`.
-    pub default_watch: Option<bool>,
-
     /// A list of directories that will be watched if the job
     /// is run on a package.
     /// src, examples, tests, and benches are implicitly included
     /// unless you `set default_watch` to false.
     pub watch: Option<Vec<String>>,
-
-    /// Whether to insert extraneous arguments provided by bacon or end users
-    ///
-    /// Eg: --all-features or anything after -- in bacon incantation
-    #[serde(default = "default_true")]
-    pub extraneous_args: bool,
-
-    /// How to handle changes: either immediately kill the current job
-    /// then restart it, or wait for the current job to finish before
-    /// restarting it.
-    pub on_change_strategy: Option<OnChangeStrategy>,
-
-    /// The analyzer interpreting the output of the command, the
-    /// standard cargo dedicated one if not provided
-    pub analyzer: Option<Analyzer>,
-
-    /// Patterns of lines which should be ignored. Patterns of
-    /// the prefs or bacon.toml can be overridden at the job
-    pub ignored_lines: Option<Vec<LinePattern>>,
 }
 
 static DEFAULT_ARGS: &[&str] = &["--color", "always"];
@@ -131,6 +135,7 @@ impl Job {
             on_change_strategy: None,
             analyzer: Some(Analyzer::Standard),
             ignored_lines: None,
+            ignore: Default::default(),
         }
     }
 }
diff --git a/src/mission.rs b/src/mission.rs
index 2234854..d1369a9 100644
--- a/src/mission.rs
+++ b/src/mission.rs
@@ -19,26 +19,32 @@ pub struct Mission<'s> {
 }
 
 impl<'s> Mission<'s> {
-    /// Return an Ignorer if required by the job's settings
-    /// and if the mission takes place in a git repository
-    pub fn ignorer(&self) -> Option<Ignorer> {
-        match self.job.apply_gitignore {
-            Some(false) => {
-                debug!("No gitignorer because of settings");
-                None
+    /// Return an Ignorer according to the job's settings
+    pub fn ignorer(&self) -> IgnorerSet {
+        let mut set = IgnorerSet::default();
+        if self.job.apply_gitignore != Some(false) {
+            match GitIgnorer::new(&self.package_directory) {
+                Ok(git_ignorer) => {
+                    set.add(Box::new(git_ignorer));
+                }
+                Err(e) => {
+                    // might be normal, eg not in a git repo
+                    debug!("Failed to initialise git ignorer: {e}");
+                }
             }
-            _ => {
-                // by default we apply gitignore rules
-                match Ignorer::new(&self.package_directory) {
-                    Ok(ignorer) => Some(ignorer),
-                    Err(e) => {
-                        // might be normal, eg not in a git repo
-                        debug!("Failed to initialise git ignorer: {e}");
-                        None
-                    }
+        }
+        if !self.job.ignore.is_empty() {
+            let mut glob_ignorer = GlobIgnorer::default();
+            info!("CREATE IGNORER");
+            for pattern in &self.job.ignore {
+                info!("ADD PATTERN {pattern}");
+                if let Err(e) = glob_ignorer.add(pattern, &self.package_directory) {
+                    warn!("Failed to add ignore pattern {pattern}: {e}");
                 }
             }
+            set.add(Box::new(glob_ignorer));
         }
+        set
     }
 
     /// the action bound to success on this job
diff --git a/src/watcher.rs b/src/watcher.rs
index e1c0a83..0b5bacb 100644
--- a/src/watcher.rs
+++ b/src/watcher.rs
@@ -29,7 +29,7 @@ pub struct Watcher {
 impl Watcher {
     pub fn new(
         paths_to_watch: &[PathBuf],
-        mut ignorer: Option<Ignorer>,
+        mut ignorer: IgnorerSet,
     ) -> Result<Self> {
         let (sender, receiver) = bounded(0);
         let mut notify_watcher =
@@ -55,18 +55,16 @@ impl Watcher {
                             debug!("notify event: {we:?}");
                         }
                     }
-                    if let Some(ignorer) = ignorer.as_mut() {
-                        match time!(Info, ignorer.excludes_all(&we.paths)) {
-                            Ok(true) => {
-                                debug!("all excluded");
-                                return;
-                            }
-                            Ok(false) => {
-                                debug!("at least one is included");
-                            }
-                            Err(e) => {
-                                warn!("exclusion check failed: {e}");
-                            }
+                    match time!(Info, ignorer.excludes_all_pathbufs(&we.paths)) {
+                        Ok(true) => {
+                            debug!("all excluded");
+                            return;
+                        }
+                        Ok(false) => {
+                            debug!("at least one is included");
+                        }
+                        Err(e) => {
+                            warn!("exclusion check failed: {e}");
                         }
                     }
                     if let Err(e) = sender.send(()) {
diff --git a/website/docs/config.md b/website/docs/config.md
index 3f7d950..cefe288 100644
--- a/website/docs/config.md
+++ b/website/docs/config.md
@@ -78,14 +78,15 @@ command | the tokens making the command to execute (first one is the executable)
 default_watch | whether to watch default files (`src`, `tests`, `examples`, `build.rs`, and `benches`). When it's set to `false`, only the files in your `watch` parameter are watched | `true`
 env | a map of environment vars, for example `env.LOG_LEVEL="die"` |
 kill | a command replacing the default job interruption (platform dependant, `SIGKILL` on unix). For example `kill = ["kill", "-s", "INT"]` |
-ignored_lines | regular expressions for lines to ignores |
+ignore | list of glob patterns for files to ignore |
+ignored_lines | regular expressions for lines to ignore |
 extraneous_args | if `false`, the action is run "as is" from `bacon.toml`, eg: no `--all-features` or `--features` inclusion | `true`
 need_stdout |whether we need to capture stdout too (stderr is always captured) | `false`
 on_change_strategy | `wait_then_restart` or `kill_then_restart` |
 on_success | the action to run when there's no error, warning or test failures |
 watch | a list of files and directories that will be watched if the job is run on a package. Usual source directories are implicitly included unless `default_watch` is set to false |
 
-Some of these properties can also be defined before jobs and will apply to all of them unless overriden: `watch`, `default_watch`, `ignored_lines`, and `on_change_strategy`.
+Some of these properties can also be defined before jobs and will apply to all of them unless overriden: `watch`, `default_watch`, `ignore` (additive), `ignored_lines`, and `on_change_strategy`.
 
 Don't forget to include `--color always` in most jobs, because bacon uses style information to parse the output of cargo.
 


Test patch: