watchexec_filterer_globset/
lib.rs1#![doc(html_favicon_url = "https://watchexec.github.io/logo:watchexec.svg")]
7#![doc(html_logo_url = "https://watchexec.github.io/logo:watchexec.svg")]
8#![warn(clippy::unwrap_used, missing_docs)]
9#![cfg_attr(not(test), warn(unused_crate_dependencies))]
10#![deny(rust_2018_idioms)]
11
12use std::{
13 ffi::OsString,
14 path::{Path, PathBuf},
15};
16
17use ignore::gitignore::{Gitignore, GitignoreBuilder};
18use ignore_files::{Error, IgnoreFile, IgnoreFilter};
19use tracing::{debug, trace, trace_span};
20use watchexec::{error::RuntimeError, filter::Filterer};
21use watchexec_events::{Event, FileType, Priority};
22use watchexec_filterer_ignore::IgnoreFilterer;
23
24#[cfg_attr(feature = "full_debug", derive(Debug))]
26pub struct GlobsetFilterer {
27 #[cfg_attr(not(unix), allow(dead_code))]
28 origin: PathBuf,
29 filters: Gitignore,
30 ignores: Gitignore,
31 whitelist: Vec<PathBuf>,
32 ignore_files: IgnoreFilterer,
33 extensions: Vec<OsString>,
34}
35
36#[cfg(not(feature = "full_debug"))]
37impl std::fmt::Debug for GlobsetFilterer {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 f.debug_struct("GlobsetFilterer")
40 .field("origin", &self.origin)
41 .field("filters", &"ignore::gitignore::Gitignore{...}")
42 .field("ignores", &"ignore::gitignore::Gitignore{...}")
43 .field("ignore_files", &self.ignore_files)
44 .field("extensions", &self.extensions)
45 .finish()
46 }
47}
48
49impl GlobsetFilterer {
50 #[allow(clippy::future_not_send)]
66 pub async fn new(
67 origin: impl AsRef<Path>,
68 filters: impl IntoIterator<Item = (String, Option<PathBuf>)>,
69 ignores: impl IntoIterator<Item = (String, Option<PathBuf>)>,
70 whitelist: impl IntoIterator<Item = PathBuf>,
71 ignore_files: impl IntoIterator<Item = IgnoreFile>,
72 extensions: impl IntoIterator<Item = OsString>,
73 ) -> Result<Self, Error> {
74 let origin = origin.as_ref();
75 let mut filters_builder = GitignoreBuilder::new(origin);
76 let mut ignores_builder = GitignoreBuilder::new(origin);
77
78 for (filter, in_path) in filters {
79 trace!(filter=?&filter, "add filter to globset filterer");
80 filters_builder
81 .add_line(in_path.clone(), &filter)
82 .map_err(|err| Error::Glob { file: in_path, err })?;
83 }
84
85 for (ignore, in_path) in ignores {
86 trace!(ignore=?&ignore, "add ignore to globset filterer");
87 ignores_builder
88 .add_line(in_path.clone(), &ignore)
89 .map_err(|err| Error::Glob { file: in_path, err })?;
90 }
91
92 let filters = filters_builder
93 .build()
94 .map_err(|err| Error::Glob { file: None, err })?;
95 let ignores = ignores_builder
96 .build()
97 .map_err(|err| Error::Glob { file: None, err })?;
98
99 let extensions: Vec<OsString> = extensions.into_iter().collect();
100
101 let mut ignore_files =
102 IgnoreFilter::new(origin, &ignore_files.into_iter().collect::<Vec<_>>()).await?;
103 ignore_files.finish();
104 let ignore_files = IgnoreFilterer(ignore_files);
105
106 let whitelist = whitelist.into_iter().collect::<Vec<_>>();
107
108 debug!(
109 ?origin,
110 num_filters=%filters.num_ignores(),
111 num_neg_filters=%filters.num_whitelists(),
112 num_ignores=%ignores.num_ignores(),
113 num_in_ignore_files=?ignore_files.0.num_ignores(),
114 num_neg_ignores=%ignores.num_whitelists(),
115 num_extensions=%extensions.len(),
116 "globset filterer built");
117
118 Ok(Self {
119 origin: origin.into(),
120 filters,
121 ignores,
122 whitelist,
123 ignore_files,
124 extensions,
125 })
126 }
127}
128
129impl Filterer for GlobsetFilterer {
130 fn check_event(&self, event: &Event, priority: Priority) -> Result<bool, RuntimeError> {
134 let _span = trace_span!("filterer_check").entered();
135
136 {
137 trace!("checking internal whitelist");
138 if event
141 .paths()
142 .any(|(p, _)| self.whitelist.iter().any(|w| w == p))
143 {
144 trace!("internal whitelist filterer matched (success)");
145 return Ok(true);
146 }
147 }
148
149 {
150 trace!("checking internal ignore filterer");
151 if !self
152 .ignore_files
153 .check_event(event, priority)
154 .expect("IgnoreFilterer never errors")
155 {
156 trace!("internal ignore filterer matched (fail)");
157 return Ok(false);
158 }
159 }
160
161 let mut paths = event.paths().peekable();
162 if paths.peek().is_none() {
163 trace!("non-path event (pass)");
164 Ok(true)
165 } else {
166 Ok(paths.any(|(path, file_type)| {
167 let _span = trace_span!("path", ?path).entered();
168 let is_dir = file_type.map_or(false, |t| matches!(t, FileType::Dir));
169
170 if self.ignores.matched(path, is_dir).is_ignore() {
171 trace!("ignored by globset ignore");
172 return false;
173 }
174
175 let mut filtered = false;
176 if self.filters.num_ignores() > 0 {
177 trace!("running through glob filters");
178 filtered = true;
179
180 if self.filters.matched(path, is_dir).is_ignore() {
181 trace!("allowed by globset filters");
182 return true;
183 }
184
185 #[cfg(unix)]
187 if let Ok(based) = path.strip_prefix(&self.origin) {
188 let rebased = {
189 use std::path::MAIN_SEPARATOR;
190 let mut b = self.origin.clone().into_os_string();
191 b.push(PathBuf::from(String::from(MAIN_SEPARATOR)));
192 b.push(PathBuf::from(String::from(MAIN_SEPARATOR)));
193 b.push(based.as_os_str());
194 b
195 };
196
197 trace!(?rebased, "testing on rebased path, 1.x bug compat (#258)");
198 if self.filters.matched(rebased, is_dir).is_ignore() {
199 trace!("allowed by globset filters, 1.x bug compat (#258)");
200 return true;
201 }
202 }
203 }
204
205 if !self.extensions.is_empty() {
206 trace!("running through extension filters");
207 filtered = true;
208
209 if is_dir {
210 trace!("failed on extension check due to being a dir");
211 return false;
212 }
213
214 if let Some(ext) = path.extension() {
215 if self.extensions.iter().any(|e| e == ext) {
216 trace!("allowed by extension filter");
217 return true;
218 }
219 } else {
220 trace!(
221 ?path,
222 "failed on extension check due to having no extension"
223 );
224 return false;
225 }
226 }
227
228 !filtered
229 }))
230 }
231 }
232}