##// END OF EJS Templates
rust-status: only involve ignore mechanism when needed...
Raphaël Gomès -
r45088:e62052d0 default
parent child Browse files
Show More
@@ -1,895 +1,911 b''
1 // status.rs
1 // status.rs
2 //
2 //
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
6 // GNU General Public License version 2 or any later version.
7
7
8 //! Rust implementation of dirstate.status (dirstate.py).
8 //! Rust implementation of dirstate.status (dirstate.py).
9 //! It is currently missing a lot of functionality compared to the Python one
9 //! It is currently missing a lot of functionality compared to the Python one
10 //! and will only be triggered in narrow cases.
10 //! and will only be triggered in narrow cases.
11
11
12 use crate::{
12 use crate::{
13 dirstate::SIZE_FROM_OTHER_PARENT,
13 dirstate::SIZE_FROM_OTHER_PARENT,
14 filepatterns::PatternFileWarning,
14 filepatterns::PatternFileWarning,
15 matchers::{get_ignore_function, Matcher, VisitChildrenSet},
15 matchers::{get_ignore_function, Matcher, VisitChildrenSet},
16 utils::{
16 utils::{
17 files::{find_dirs, HgMetadata},
17 files::{find_dirs, HgMetadata},
18 hg_path::{
18 hg_path::{
19 hg_path_to_path_buf, os_string_to_hg_path_buf, HgPath, HgPathBuf,
19 hg_path_to_path_buf, os_string_to_hg_path_buf, HgPath, HgPathBuf,
20 HgPathError,
20 HgPathError,
21 },
21 },
22 path_auditor::PathAuditor,
22 path_auditor::PathAuditor,
23 },
23 },
24 CopyMap, DirstateEntry, DirstateMap, EntryState, FastHashMap,
24 CopyMap, DirstateEntry, DirstateMap, EntryState, FastHashMap,
25 PatternError,
25 PatternError,
26 };
26 };
27 use lazy_static::lazy_static;
27 use lazy_static::lazy_static;
28 use micro_timer::timed;
28 use micro_timer::timed;
29 use rayon::prelude::*;
29 use rayon::prelude::*;
30 use std::{
30 use std::{
31 borrow::Cow,
31 borrow::Cow,
32 collections::HashSet,
32 collections::HashSet,
33 fs::{read_dir, DirEntry},
33 fs::{read_dir, DirEntry},
34 io::ErrorKind,
34 io::ErrorKind,
35 ops::Deref,
35 ops::Deref,
36 path::Path,
36 path::{Path, PathBuf},
37 };
37 };
38
38
39 /// Wrong type of file from a `BadMatch`
39 /// Wrong type of file from a `BadMatch`
40 /// Note: a lot of those don't exist on all platforms.
40 /// Note: a lot of those don't exist on all platforms.
41 #[derive(Debug, Copy, Clone)]
41 #[derive(Debug, Copy, Clone)]
42 pub enum BadType {
42 pub enum BadType {
43 CharacterDevice,
43 CharacterDevice,
44 BlockDevice,
44 BlockDevice,
45 FIFO,
45 FIFO,
46 Socket,
46 Socket,
47 Directory,
47 Directory,
48 Unknown,
48 Unknown,
49 }
49 }
50
50
51 impl ToString for BadType {
51 impl ToString for BadType {
52 fn to_string(&self) -> String {
52 fn to_string(&self) -> String {
53 match self {
53 match self {
54 BadType::CharacterDevice => "character device",
54 BadType::CharacterDevice => "character device",
55 BadType::BlockDevice => "block device",
55 BadType::BlockDevice => "block device",
56 BadType::FIFO => "fifo",
56 BadType::FIFO => "fifo",
57 BadType::Socket => "socket",
57 BadType::Socket => "socket",
58 BadType::Directory => "directory",
58 BadType::Directory => "directory",
59 BadType::Unknown => "unknown",
59 BadType::Unknown => "unknown",
60 }
60 }
61 .to_string()
61 .to_string()
62 }
62 }
63 }
63 }
64
64
65 /// Was explicitly matched but cannot be found/accessed
65 /// Was explicitly matched but cannot be found/accessed
66 #[derive(Debug, Copy, Clone)]
66 #[derive(Debug, Copy, Clone)]
67 pub enum BadMatch {
67 pub enum BadMatch {
68 OsError(i32),
68 OsError(i32),
69 BadType(BadType),
69 BadType(BadType),
70 }
70 }
71
71
72 /// Marker enum used to dispatch new status entries into the right collections.
72 /// Marker enum used to dispatch new status entries into the right collections.
73 /// Is similar to `crate::EntryState`, but represents the transient state of
73 /// Is similar to `crate::EntryState`, but represents the transient state of
74 /// entries during the lifetime of a command.
74 /// entries during the lifetime of a command.
75 #[derive(Debug, Copy, Clone)]
75 #[derive(Debug, Copy, Clone)]
76 enum Dispatch {
76 enum Dispatch {
77 Unsure,
77 Unsure,
78 Modified,
78 Modified,
79 Added,
79 Added,
80 Removed,
80 Removed,
81 Deleted,
81 Deleted,
82 Clean,
82 Clean,
83 Unknown,
83 Unknown,
84 Ignored,
84 Ignored,
85 /// Empty dispatch, the file is not worth listing
85 /// Empty dispatch, the file is not worth listing
86 None,
86 None,
87 /// Was explicitly matched but cannot be found/accessed
87 /// Was explicitly matched but cannot be found/accessed
88 Bad(BadMatch),
88 Bad(BadMatch),
89 Directory {
89 Directory {
90 /// True if the directory used to be a file in the dmap so we can say
90 /// True if the directory used to be a file in the dmap so we can say
91 /// that it's been removed.
91 /// that it's been removed.
92 was_file: bool,
92 was_file: bool,
93 },
93 },
94 }
94 }
95
95
96 type IoResult<T> = std::io::Result<T>;
96 type IoResult<T> = std::io::Result<T>;
97 /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait, 'static>`, so add
98 /// an explicit lifetime here to not fight `'static` bounds "out of nowhere".
99 type IgnoreFnType<'a> = Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>;
97
100
98 /// Dates and times that are outside the 31-bit signed range are compared
101 /// Dates and times that are outside the 31-bit signed range are compared
99 /// modulo 2^31. This should prevent hg from behaving badly with very large
102 /// modulo 2^31. This should prevent hg from behaving badly with very large
100 /// files or corrupt dates while still having a high probability of detecting
103 /// files or corrupt dates while still having a high probability of detecting
101 /// changes. (issue2608)
104 /// changes. (issue2608)
102 /// TODO I haven't found a way of having `b` be `Into<i32>`, since `From<u64>`
105 /// TODO I haven't found a way of having `b` be `Into<i32>`, since `From<u64>`
103 /// is not defined for `i32`, and there is no `As` trait. This forces the
106 /// is not defined for `i32`, and there is no `As` trait. This forces the
104 /// caller to cast `b` as `i32`.
107 /// caller to cast `b` as `i32`.
105 fn mod_compare(a: i32, b: i32) -> bool {
108 fn mod_compare(a: i32, b: i32) -> bool {
106 a & i32::max_value() != b & i32::max_value()
109 a & i32::max_value() != b & i32::max_value()
107 }
110 }
108
111
109 /// Return a sorted list containing information about the entries
112 /// Return a sorted list containing information about the entries
110 /// in the directory.
113 /// in the directory.
111 ///
114 ///
112 /// * `skip_dot_hg` - Return an empty vec if `path` contains a `.hg` directory
115 /// * `skip_dot_hg` - Return an empty vec if `path` contains a `.hg` directory
113 fn list_directory(
116 fn list_directory(
114 path: impl AsRef<Path>,
117 path: impl AsRef<Path>,
115 skip_dot_hg: bool,
118 skip_dot_hg: bool,
116 ) -> std::io::Result<Vec<(HgPathBuf, DirEntry)>> {
119 ) -> std::io::Result<Vec<(HgPathBuf, DirEntry)>> {
117 let mut results = vec![];
120 let mut results = vec![];
118 let entries = read_dir(path.as_ref())?;
121 let entries = read_dir(path.as_ref())?;
119
122
120 for entry in entries {
123 for entry in entries {
121 let entry = entry?;
124 let entry = entry?;
122 let filename = os_string_to_hg_path_buf(entry.file_name())?;
125 let filename = os_string_to_hg_path_buf(entry.file_name())?;
123 let file_type = entry.file_type()?;
126 let file_type = entry.file_type()?;
124 if skip_dot_hg && filename.as_bytes() == b".hg" && file_type.is_dir() {
127 if skip_dot_hg && filename.as_bytes() == b".hg" && file_type.is_dir() {
125 return Ok(vec![]);
128 return Ok(vec![]);
126 } else {
129 } else {
127 results.push((HgPathBuf::from(filename), entry))
130 results.push((HgPathBuf::from(filename), entry))
128 }
131 }
129 }
132 }
130
133
131 results.sort_unstable_by_key(|e| e.0.clone());
134 results.sort_unstable_by_key(|e| e.0.clone());
132 Ok(results)
135 Ok(results)
133 }
136 }
134
137
135 /// The file corresponding to the dirstate entry was found on the filesystem.
138 /// The file corresponding to the dirstate entry was found on the filesystem.
136 fn dispatch_found(
139 fn dispatch_found(
137 filename: impl AsRef<HgPath>,
140 filename: impl AsRef<HgPath>,
138 entry: DirstateEntry,
141 entry: DirstateEntry,
139 metadata: HgMetadata,
142 metadata: HgMetadata,
140 copy_map: &CopyMap,
143 copy_map: &CopyMap,
141 options: StatusOptions,
144 options: StatusOptions,
142 ) -> Dispatch {
145 ) -> Dispatch {
143 let DirstateEntry {
146 let DirstateEntry {
144 state,
147 state,
145 mode,
148 mode,
146 mtime,
149 mtime,
147 size,
150 size,
148 } = entry;
151 } = entry;
149
152
150 let HgMetadata {
153 let HgMetadata {
151 st_mode,
154 st_mode,
152 st_size,
155 st_size,
153 st_mtime,
156 st_mtime,
154 ..
157 ..
155 } = metadata;
158 } = metadata;
156
159
157 match state {
160 match state {
158 EntryState::Normal => {
161 EntryState::Normal => {
159 let size_changed = mod_compare(size, st_size as i32);
162 let size_changed = mod_compare(size, st_size as i32);
160 let mode_changed =
163 let mode_changed =
161 (mode ^ st_mode as i32) & 0o100 != 0o000 && options.check_exec;
164 (mode ^ st_mode as i32) & 0o100 != 0o000 && options.check_exec;
162 let metadata_changed = size >= 0 && (size_changed || mode_changed);
165 let metadata_changed = size >= 0 && (size_changed || mode_changed);
163 let other_parent = size == SIZE_FROM_OTHER_PARENT;
166 let other_parent = size == SIZE_FROM_OTHER_PARENT;
164 if metadata_changed
167 if metadata_changed
165 || other_parent
168 || other_parent
166 || copy_map.contains_key(filename.as_ref())
169 || copy_map.contains_key(filename.as_ref())
167 {
170 {
168 Dispatch::Modified
171 Dispatch::Modified
169 } else if mod_compare(mtime, st_mtime as i32) {
172 } else if mod_compare(mtime, st_mtime as i32) {
170 Dispatch::Unsure
173 Dispatch::Unsure
171 } else if st_mtime == options.last_normal_time {
174 } else if st_mtime == options.last_normal_time {
172 // the file may have just been marked as normal and
175 // the file may have just been marked as normal and
173 // it may have changed in the same second without
176 // it may have changed in the same second without
174 // changing its size. This can happen if we quickly
177 // changing its size. This can happen if we quickly
175 // do multiple commits. Force lookup, so we don't
178 // do multiple commits. Force lookup, so we don't
176 // miss such a racy file change.
179 // miss such a racy file change.
177 Dispatch::Unsure
180 Dispatch::Unsure
178 } else if options.list_clean {
181 } else if options.list_clean {
179 Dispatch::Clean
182 Dispatch::Clean
180 } else {
183 } else {
181 Dispatch::None
184 Dispatch::None
182 }
185 }
183 }
186 }
184 EntryState::Merged => Dispatch::Modified,
187 EntryState::Merged => Dispatch::Modified,
185 EntryState::Added => Dispatch::Added,
188 EntryState::Added => Dispatch::Added,
186 EntryState::Removed => Dispatch::Removed,
189 EntryState::Removed => Dispatch::Removed,
187 EntryState::Unknown => Dispatch::Unknown,
190 EntryState::Unknown => Dispatch::Unknown,
188 }
191 }
189 }
192 }
190
193
191 /// The file corresponding to this Dirstate entry is missing.
194 /// The file corresponding to this Dirstate entry is missing.
192 fn dispatch_missing(state: EntryState) -> Dispatch {
195 fn dispatch_missing(state: EntryState) -> Dispatch {
193 match state {
196 match state {
194 // File was removed from the filesystem during commands
197 // File was removed from the filesystem during commands
195 EntryState::Normal | EntryState::Merged | EntryState::Added => {
198 EntryState::Normal | EntryState::Merged | EntryState::Added => {
196 Dispatch::Deleted
199 Dispatch::Deleted
197 }
200 }
198 // File was removed, everything is normal
201 // File was removed, everything is normal
199 EntryState::Removed => Dispatch::Removed,
202 EntryState::Removed => Dispatch::Removed,
200 // File is unknown to Mercurial, everything is normal
203 // File is unknown to Mercurial, everything is normal
201 EntryState::Unknown => Dispatch::Unknown,
204 EntryState::Unknown => Dispatch::Unknown,
202 }
205 }
203 }
206 }
204
207
205 lazy_static! {
208 lazy_static! {
206 static ref DEFAULT_WORK: HashSet<&'static HgPath> = {
209 static ref DEFAULT_WORK: HashSet<&'static HgPath> = {
207 let mut h = HashSet::new();
210 let mut h = HashSet::new();
208 h.insert(HgPath::new(b""));
211 h.insert(HgPath::new(b""));
209 h
212 h
210 };
213 };
211 }
214 }
212
215
213 /// Get stat data about the files explicitly specified by match.
216 /// Get stat data about the files explicitly specified by match.
214 /// TODO subrepos
217 /// TODO subrepos
215 #[timed]
218 #[timed]
216 fn walk_explicit<'a>(
219 fn walk_explicit<'a>(
217 files: Option<&'a HashSet<&HgPath>>,
220 files: Option<&'a HashSet<&HgPath>>,
218 dmap: &'a DirstateMap,
221 dmap: &'a DirstateMap,
219 root_dir: impl AsRef<Path> + Sync + Send + 'a,
222 root_dir: impl AsRef<Path> + Sync + Send + 'a,
220 options: StatusOptions,
223 options: StatusOptions,
221 ) -> impl ParallelIterator<Item = IoResult<(&'a HgPath, Dispatch)>> {
224 ) -> impl ParallelIterator<Item = IoResult<(&'a HgPath, Dispatch)>> {
222 files
225 files
223 .unwrap_or(&DEFAULT_WORK)
226 .unwrap_or(&DEFAULT_WORK)
224 .par_iter()
227 .par_iter()
225 .map(move |filename| {
228 .map(move |filename| {
226 // TODO normalization
229 // TODO normalization
227 let normalized = filename.as_ref();
230 let normalized = filename.as_ref();
228
231
229 let buf = match hg_path_to_path_buf(normalized) {
232 let buf = match hg_path_to_path_buf(normalized) {
230 Ok(x) => x,
233 Ok(x) => x,
231 Err(e) => return Some(Err(e.into())),
234 Err(e) => return Some(Err(e.into())),
232 };
235 };
233 let target = root_dir.as_ref().join(buf);
236 let target = root_dir.as_ref().join(buf);
234 let st = target.symlink_metadata();
237 let st = target.symlink_metadata();
235 let in_dmap = dmap.get(normalized);
238 let in_dmap = dmap.get(normalized);
236 match st {
239 match st {
237 Ok(meta) => {
240 Ok(meta) => {
238 let file_type = meta.file_type();
241 let file_type = meta.file_type();
239 return if file_type.is_file() || file_type.is_symlink() {
242 return if file_type.is_file() || file_type.is_symlink() {
240 if let Some(entry) = in_dmap {
243 if let Some(entry) = in_dmap {
241 return Some(Ok((
244 return Some(Ok((
242 normalized,
245 normalized,
243 dispatch_found(
246 dispatch_found(
244 &normalized,
247 &normalized,
245 *entry,
248 *entry,
246 HgMetadata::from_metadata(meta),
249 HgMetadata::from_metadata(meta),
247 &dmap.copy_map,
250 &dmap.copy_map,
248 options,
251 options,
249 ),
252 ),
250 )));
253 )));
251 }
254 }
252 Some(Ok((normalized, Dispatch::Unknown)))
255 Some(Ok((normalized, Dispatch::Unknown)))
253 } else {
256 } else {
254 if file_type.is_dir() {
257 if file_type.is_dir() {
255 Some(Ok((
258 Some(Ok((
256 normalized,
259 normalized,
257 Dispatch::Directory {
260 Dispatch::Directory {
258 was_file: in_dmap.is_some(),
261 was_file: in_dmap.is_some(),
259 },
262 },
260 )))
263 )))
261 } else {
264 } else {
262 Some(Ok((
265 Some(Ok((
263 normalized,
266 normalized,
264 Dispatch::Bad(BadMatch::BadType(
267 Dispatch::Bad(BadMatch::BadType(
265 // TODO do more than unknown
268 // TODO do more than unknown
266 // Support for all `BadType` variant
269 // Support for all `BadType` variant
267 // varies greatly between platforms.
270 // varies greatly between platforms.
268 // So far, no tests check the type and
271 // So far, no tests check the type and
269 // this should be good enough for most
272 // this should be good enough for most
270 // users.
273 // users.
271 BadType::Unknown,
274 BadType::Unknown,
272 )),
275 )),
273 )))
276 )))
274 }
277 }
275 };
278 };
276 }
279 }
277 Err(_) => {
280 Err(_) => {
278 if let Some(entry) = in_dmap {
281 if let Some(entry) = in_dmap {
279 return Some(Ok((
282 return Some(Ok((
280 normalized,
283 normalized,
281 dispatch_missing(entry.state),
284 dispatch_missing(entry.state),
282 )));
285 )));
283 }
286 }
284 }
287 }
285 };
288 };
286 None
289 None
287 })
290 })
288 .flatten()
291 .flatten()
289 }
292 }
290
293
291 #[derive(Debug, Copy, Clone)]
294 #[derive(Debug, Copy, Clone)]
292 pub struct StatusOptions {
295 pub struct StatusOptions {
293 /// Remember the most recent modification timeslot for status, to make
296 /// Remember the most recent modification timeslot for status, to make
294 /// sure we won't miss future size-preserving file content modifications
297 /// sure we won't miss future size-preserving file content modifications
295 /// that happen within the same timeslot.
298 /// that happen within the same timeslot.
296 pub last_normal_time: i64,
299 pub last_normal_time: i64,
297 /// Whether we are on a filesystem with UNIX-like exec flags
300 /// Whether we are on a filesystem with UNIX-like exec flags
298 pub check_exec: bool,
301 pub check_exec: bool,
299 pub list_clean: bool,
302 pub list_clean: bool,
300 pub list_unknown: bool,
303 pub list_unknown: bool,
301 pub list_ignored: bool,
304 pub list_ignored: bool,
302 }
305 }
303
306
304 /// Dispatch a single entry (file, folder, symlink...) found during `traverse`.
307 /// Dispatch a single entry (file, folder, symlink...) found during `traverse`.
305 /// If the entry is a folder that needs to be traversed, it will be handled
308 /// If the entry is a folder that needs to be traversed, it will be handled
306 /// in a separate thread.
309 /// in a separate thread.
307
310
308 fn handle_traversed_entry<'a>(
311 fn handle_traversed_entry<'a>(
309 scope: &rayon::Scope<'a>,
312 scope: &rayon::Scope<'a>,
310 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
313 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
311 matcher: &'a (impl Matcher + Sync),
314 matcher: &'a (impl Matcher + Sync),
312 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
315 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
313 dmap: &'a DirstateMap,
316 dmap: &'a DirstateMap,
314 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
317 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
315 ignore_fn: &'a (impl for<'r> Fn(&'r HgPath) -> bool + Sync),
318 ignore_fn: &'a IgnoreFnType,
316 dir_ignore_fn: &'a (impl for<'r> Fn(&'r HgPath) -> bool + Sync),
319 dir_ignore_fn: &'a IgnoreFnType,
317 options: StatusOptions,
320 options: StatusOptions,
318 filename: HgPathBuf,
321 filename: HgPathBuf,
319 dir_entry: DirEntry,
322 dir_entry: DirEntry,
320 ) -> IoResult<()> {
323 ) -> IoResult<()> {
321 let file_type = dir_entry.file_type()?;
324 let file_type = dir_entry.file_type()?;
322 let entry_option = dmap.get(&filename);
325 let entry_option = dmap.get(&filename);
323
326
324 if file_type.is_dir() {
327 if file_type.is_dir() {
325 handle_traversed_dir(
328 handle_traversed_dir(
326 scope,
329 scope,
327 files_sender,
330 files_sender,
328 matcher,
331 matcher,
329 root_dir,
332 root_dir,
330 dmap,
333 dmap,
331 old_results,
334 old_results,
332 ignore_fn,
335 ignore_fn,
333 dir_ignore_fn,
336 dir_ignore_fn,
334 options,
337 options,
335 entry_option,
338 entry_option,
336 filename,
339 filename,
337 );
340 );
338 } else if file_type.is_file() || file_type.is_symlink() {
341 } else if file_type.is_file() || file_type.is_symlink() {
339 if let Some(entry) = entry_option {
342 if let Some(entry) = entry_option {
340 if matcher.matches_everything() || matcher.matches(&filename) {
343 if matcher.matches_everything() || matcher.matches(&filename) {
341 let metadata = dir_entry.metadata()?;
344 let metadata = dir_entry.metadata()?;
342 files_sender
345 files_sender
343 .send(Ok((
346 .send(Ok((
344 filename.to_owned(),
347 filename.to_owned(),
345 dispatch_found(
348 dispatch_found(
346 &filename,
349 &filename,
347 *entry,
350 *entry,
348 HgMetadata::from_metadata(metadata),
351 HgMetadata::from_metadata(metadata),
349 &dmap.copy_map,
352 &dmap.copy_map,
350 options,
353 options,
351 ),
354 ),
352 )))
355 )))
353 .unwrap();
356 .unwrap();
354 }
357 }
355 } else if (matcher.matches_everything() || matcher.matches(&filename))
358 } else if (matcher.matches_everything() || matcher.matches(&filename))
356 && !ignore_fn(&filename)
359 && !ignore_fn(&filename)
357 {
360 {
358 if (options.list_ignored || matcher.exact_match(&filename))
361 if (options.list_ignored || matcher.exact_match(&filename))
359 && dir_ignore_fn(&filename)
362 && dir_ignore_fn(&filename)
360 {
363 {
361 if options.list_ignored {
364 if options.list_ignored {
362 files_sender
365 files_sender
363 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
366 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
364 .unwrap();
367 .unwrap();
365 }
368 }
366 } else {
369 } else {
367 files_sender
370 files_sender
368 .send(Ok((filename.to_owned(), Dispatch::Unknown)))
371 .send(Ok((filename.to_owned(), Dispatch::Unknown)))
369 .unwrap();
372 .unwrap();
370 }
373 }
371 } else if ignore_fn(&filename) && options.list_ignored {
374 } else if ignore_fn(&filename) && options.list_ignored {
372 files_sender
375 files_sender
373 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
376 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
374 .unwrap();
377 .unwrap();
375 }
378 }
376 } else if let Some(entry) = entry_option {
379 } else if let Some(entry) = entry_option {
377 // Used to be a file or a folder, now something else.
380 // Used to be a file or a folder, now something else.
378 if matcher.matches_everything() || matcher.matches(&filename) {
381 if matcher.matches_everything() || matcher.matches(&filename) {
379 files_sender
382 files_sender
380 .send(Ok((filename.to_owned(), dispatch_missing(entry.state))))
383 .send(Ok((filename.to_owned(), dispatch_missing(entry.state))))
381 .unwrap();
384 .unwrap();
382 }
385 }
383 }
386 }
384
387
385 Ok(())
388 Ok(())
386 }
389 }
387
390
388 /// A directory was found in the filesystem and needs to be traversed
391 /// A directory was found in the filesystem and needs to be traversed
389 fn handle_traversed_dir<'a>(
392 fn handle_traversed_dir<'a>(
390 scope: &rayon::Scope<'a>,
393 scope: &rayon::Scope<'a>,
391 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
394 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
392 matcher: &'a (impl Matcher + Sync),
395 matcher: &'a (impl Matcher + Sync),
393 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
396 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
394 dmap: &'a DirstateMap,
397 dmap: &'a DirstateMap,
395 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
398 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
396 ignore_fn: &'a (impl for<'r> Fn(&'r HgPath) -> bool + Sync),
399 ignore_fn: &'a IgnoreFnType,
397 dir_ignore_fn: &'a (impl for<'r> Fn(&'r HgPath) -> bool + Sync),
400 dir_ignore_fn: &'a IgnoreFnType,
398 options: StatusOptions,
401 options: StatusOptions,
399 entry_option: Option<&'a DirstateEntry>,
402 entry_option: Option<&'a DirstateEntry>,
400 directory: HgPathBuf,
403 directory: HgPathBuf,
401 ) {
404 ) {
402 scope.spawn(move |_| {
405 scope.spawn(move |_| {
403 // Nested `if` until `rust-lang/rust#53668` is stable
406 // Nested `if` until `rust-lang/rust#53668` is stable
404 if let Some(entry) = entry_option {
407 if let Some(entry) = entry_option {
405 // Used to be a file, is now a folder
408 // Used to be a file, is now a folder
406 if matcher.matches_everything() || matcher.matches(&directory) {
409 if matcher.matches_everything() || matcher.matches(&directory) {
407 files_sender
410 files_sender
408 .send(Ok((
411 .send(Ok((
409 directory.to_owned(),
412 directory.to_owned(),
410 dispatch_missing(entry.state),
413 dispatch_missing(entry.state),
411 )))
414 )))
412 .unwrap();
415 .unwrap();
413 }
416 }
414 }
417 }
415 // Do we need to traverse it?
418 // Do we need to traverse it?
416 if !ignore_fn(&directory) || options.list_ignored {
419 if !ignore_fn(&directory) || options.list_ignored {
417 traverse_dir(
420 traverse_dir(
418 files_sender,
421 files_sender,
419 matcher,
422 matcher,
420 root_dir,
423 root_dir,
421 dmap,
424 dmap,
422 directory,
425 directory,
423 &old_results,
426 &old_results,
424 ignore_fn,
427 ignore_fn,
425 dir_ignore_fn,
428 dir_ignore_fn,
426 options,
429 options,
427 )
430 )
428 .unwrap_or_else(|e| files_sender.send(Err(e)).unwrap())
431 .unwrap_or_else(|e| files_sender.send(Err(e)).unwrap())
429 }
432 }
430 });
433 });
431 }
434 }
432
435
433 /// Decides whether the directory needs to be listed, and if so handles the
436 /// Decides whether the directory needs to be listed, and if so handles the
434 /// entries in a separate thread.
437 /// entries in a separate thread.
435 fn traverse_dir<'a>(
438 fn traverse_dir<'a>(
436 files_sender: &crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
439 files_sender: &crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
437 matcher: &'a (impl Matcher + Sync),
440 matcher: &'a (impl Matcher + Sync),
438 root_dir: impl AsRef<Path> + Sync + Send + Copy,
441 root_dir: impl AsRef<Path> + Sync + Send + Copy,
439 dmap: &'a DirstateMap,
442 dmap: &'a DirstateMap,
440 directory: impl AsRef<HgPath>,
443 directory: impl AsRef<HgPath>,
441 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
444 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
442 ignore_fn: &(impl for<'r> Fn(&'r HgPath) -> bool + Sync),
445 ignore_fn: &IgnoreFnType,
443 dir_ignore_fn: &(impl for<'r> Fn(&'r HgPath) -> bool + Sync),
446 dir_ignore_fn: &IgnoreFnType,
444 options: StatusOptions,
447 options: StatusOptions,
445 ) -> IoResult<()> {
448 ) -> IoResult<()> {
446 let directory = directory.as_ref();
449 let directory = directory.as_ref();
447 if directory.as_bytes() == b".hg" {
450 if directory.as_bytes() == b".hg" {
448 return Ok(());
451 return Ok(());
449 }
452 }
450 let visit_entries = match matcher.visit_children_set(directory) {
453 let visit_entries = match matcher.visit_children_set(directory) {
451 VisitChildrenSet::Empty => return Ok(()),
454 VisitChildrenSet::Empty => return Ok(()),
452 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
455 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
453 VisitChildrenSet::Set(set) => Some(set),
456 VisitChildrenSet::Set(set) => Some(set),
454 };
457 };
455 let buf = hg_path_to_path_buf(directory)?;
458 let buf = hg_path_to_path_buf(directory)?;
456 let dir_path = root_dir.as_ref().join(buf);
459 let dir_path = root_dir.as_ref().join(buf);
457
460
458 let skip_dot_hg = !directory.as_bytes().is_empty();
461 let skip_dot_hg = !directory.as_bytes().is_empty();
459 let entries = match list_directory(dir_path, skip_dot_hg) {
462 let entries = match list_directory(dir_path, skip_dot_hg) {
460 Err(e) => match e.kind() {
463 Err(e) => match e.kind() {
461 ErrorKind::NotFound | ErrorKind::PermissionDenied => {
464 ErrorKind::NotFound | ErrorKind::PermissionDenied => {
462 files_sender
465 files_sender
463 .send(Ok((
466 .send(Ok((
464 directory.to_owned(),
467 directory.to_owned(),
465 Dispatch::Bad(BadMatch::OsError(
468 Dispatch::Bad(BadMatch::OsError(
466 // Unwrapping here is OK because the error always
469 // Unwrapping here is OK because the error always
467 // is a real os error
470 // is a real os error
468 e.raw_os_error().unwrap(),
471 e.raw_os_error().unwrap(),
469 )),
472 )),
470 )))
473 )))
471 .unwrap();
474 .unwrap();
472 return Ok(());
475 return Ok(());
473 }
476 }
474 _ => return Err(e),
477 _ => return Err(e),
475 },
478 },
476 Ok(entries) => entries,
479 Ok(entries) => entries,
477 };
480 };
478
481
479 rayon::scope(|scope| -> IoResult<()> {
482 rayon::scope(|scope| -> IoResult<()> {
480 for (filename, dir_entry) in entries {
483 for (filename, dir_entry) in entries {
481 if let Some(ref set) = visit_entries {
484 if let Some(ref set) = visit_entries {
482 if !set.contains(filename.deref()) {
485 if !set.contains(filename.deref()) {
483 continue;
486 continue;
484 }
487 }
485 }
488 }
486 // TODO normalize
489 // TODO normalize
487 let filename = if directory.is_empty() {
490 let filename = if directory.is_empty() {
488 filename.to_owned()
491 filename.to_owned()
489 } else {
492 } else {
490 directory.join(&filename)
493 directory.join(&filename)
491 };
494 };
492
495
493 if !old_results.contains_key(filename.deref()) {
496 if !old_results.contains_key(filename.deref()) {
494 handle_traversed_entry(
497 handle_traversed_entry(
495 scope,
498 scope,
496 files_sender,
499 files_sender,
497 matcher,
500 matcher,
498 root_dir,
501 root_dir,
499 dmap,
502 dmap,
500 old_results,
503 old_results,
501 ignore_fn,
504 ignore_fn,
502 dir_ignore_fn,
505 dir_ignore_fn,
503 options,
506 options,
504 filename,
507 filename,
505 dir_entry,
508 dir_entry,
506 )?;
509 )?;
507 }
510 }
508 }
511 }
509 Ok(())
512 Ok(())
510 })
513 })
511 }
514 }
512
515
513 /// Walk the working directory recursively to look for changes compared to the
516 /// Walk the working directory recursively to look for changes compared to the
514 /// current `DirstateMap`.
517 /// current `DirstateMap`.
515 ///
518 ///
516 /// This takes a mutable reference to the results to account for the `extend`
519 /// This takes a mutable reference to the results to account for the `extend`
517 /// in timings
520 /// in timings
518 #[timed]
521 #[timed]
519 fn traverse<'a>(
522 fn traverse<'a>(
520 matcher: &'a (impl Matcher + Sync),
523 matcher: &'a (impl Matcher + Sync),
521 root_dir: impl AsRef<Path> + Sync + Send + Copy,
524 root_dir: impl AsRef<Path> + Sync + Send + Copy,
522 dmap: &'a DirstateMap,
525 dmap: &'a DirstateMap,
523 path: impl AsRef<HgPath>,
526 path: impl AsRef<HgPath>,
524 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
527 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
525 ignore_fn: &(impl for<'r> Fn(&'r HgPath) -> bool + Sync),
528 ignore_fn: &IgnoreFnType,
526 dir_ignore_fn: &(impl for<'r> Fn(&'r HgPath) -> bool + Sync),
529 dir_ignore_fn: &IgnoreFnType,
527 options: StatusOptions,
530 options: StatusOptions,
528 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
531 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
529 ) -> IoResult<()> {
532 ) -> IoResult<()> {
530 let root_dir = root_dir.as_ref();
533 let root_dir = root_dir.as_ref();
531
534
532 // The traversal is done in parallel, so use a channel to gather entries.
535 // The traversal is done in parallel, so use a channel to gather entries.
533 // `crossbeam::Sender` is `Send`, while `mpsc::Sender` is not.
536 // `crossbeam::Sender` is `Send`, while `mpsc::Sender` is not.
534 let (files_transmitter, files_receiver) = crossbeam::channel::unbounded();
537 let (files_transmitter, files_receiver) = crossbeam::channel::unbounded();
535
538
536 traverse_dir(
539 traverse_dir(
537 &files_transmitter,
540 &files_transmitter,
538 matcher,
541 matcher,
539 root_dir,
542 root_dir,
540 &dmap,
543 &dmap,
541 path,
544 path,
542 &old_results,
545 &old_results,
543 &ignore_fn,
546 &ignore_fn,
544 &dir_ignore_fn,
547 &dir_ignore_fn,
545 options,
548 options,
546 )?;
549 )?;
547
550
548 // Disconnect the channel so the receiver stops waiting
551 // Disconnect the channel so the receiver stops waiting
549 drop(files_transmitter);
552 drop(files_transmitter);
550
553
551 // TODO don't collect. Find a way of replicating the behavior of
554 // TODO don't collect. Find a way of replicating the behavior of
552 // `itertools::process_results`, but for `rayon::ParallelIterator`
555 // `itertools::process_results`, but for `rayon::ParallelIterator`
553 let new_results: IoResult<Vec<(Cow<'a, HgPath>, Dispatch)>> =
556 let new_results: IoResult<Vec<(Cow<'a, HgPath>, Dispatch)>> =
554 files_receiver
557 files_receiver
555 .into_iter()
558 .into_iter()
556 .map(|item| {
559 .map(|item| {
557 let (f, d) = item?;
560 let (f, d) = item?;
558 Ok((Cow::Owned(f), d))
561 Ok((Cow::Owned(f), d))
559 })
562 })
560 .collect();
563 .collect();
561
564
562 results.par_extend(new_results?);
565 results.par_extend(new_results?);
563
566
564 Ok(())
567 Ok(())
565 }
568 }
566
569
567 /// Stat all entries in the `DirstateMap` and mark them for dispatch.
570 /// Stat all entries in the `DirstateMap` and mark them for dispatch.
568 fn stat_dmap_entries(
571 fn stat_dmap_entries(
569 dmap: &DirstateMap,
572 dmap: &DirstateMap,
570 root_dir: impl AsRef<Path> + Sync + Send,
573 root_dir: impl AsRef<Path> + Sync + Send,
571 options: StatusOptions,
574 options: StatusOptions,
572 ) -> impl ParallelIterator<Item = IoResult<(&HgPath, Dispatch)>> {
575 ) -> impl ParallelIterator<Item = IoResult<(&HgPath, Dispatch)>> {
573 dmap.par_iter().map(move |(filename, entry)| {
576 dmap.par_iter().map(move |(filename, entry)| {
574 let filename: &HgPath = filename;
577 let filename: &HgPath = filename;
575 let filename_as_path = hg_path_to_path_buf(filename)?;
578 let filename_as_path = hg_path_to_path_buf(filename)?;
576 let meta = root_dir.as_ref().join(filename_as_path).symlink_metadata();
579 let meta = root_dir.as_ref().join(filename_as_path).symlink_metadata();
577
580
578 match meta {
581 match meta {
579 Ok(ref m)
582 Ok(ref m)
580 if !(m.file_type().is_file()
583 if !(m.file_type().is_file()
581 || m.file_type().is_symlink()) =>
584 || m.file_type().is_symlink()) =>
582 {
585 {
583 Ok((filename, dispatch_missing(entry.state)))
586 Ok((filename, dispatch_missing(entry.state)))
584 }
587 }
585 Ok(m) => Ok((
588 Ok(m) => Ok((
586 filename,
589 filename,
587 dispatch_found(
590 dispatch_found(
588 filename,
591 filename,
589 *entry,
592 *entry,
590 HgMetadata::from_metadata(m),
593 HgMetadata::from_metadata(m),
591 &dmap.copy_map,
594 &dmap.copy_map,
592 options,
595 options,
593 ),
596 ),
594 )),
597 )),
595 Err(ref e)
598 Err(ref e)
596 if e.kind() == ErrorKind::NotFound
599 if e.kind() == ErrorKind::NotFound
597 || e.raw_os_error() == Some(20) =>
600 || e.raw_os_error() == Some(20) =>
598 {
601 {
599 // Rust does not yet have an `ErrorKind` for
602 // Rust does not yet have an `ErrorKind` for
600 // `NotADirectory` (errno 20)
603 // `NotADirectory` (errno 20)
601 // It happens if the dirstate contains `foo/bar` and
604 // It happens if the dirstate contains `foo/bar` and
602 // foo is not a directory
605 // foo is not a directory
603 Ok((filename, dispatch_missing(entry.state)))
606 Ok((filename, dispatch_missing(entry.state)))
604 }
607 }
605 Err(e) => Err(e),
608 Err(e) => Err(e),
606 }
609 }
607 })
610 })
608 }
611 }
609
612
610 /// This takes a mutable reference to the results to account for the `extend`
613 /// This takes a mutable reference to the results to account for the `extend`
611 /// in timings
614 /// in timings
612 #[timed]
615 #[timed]
613 fn extend_from_dmap<'a>(
616 fn extend_from_dmap<'a>(
614 dmap: &'a DirstateMap,
617 dmap: &'a DirstateMap,
615 root_dir: impl AsRef<Path> + Sync + Send,
618 root_dir: impl AsRef<Path> + Sync + Send,
616 options: StatusOptions,
619 options: StatusOptions,
617 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
620 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
618 ) {
621 ) {
619 results.par_extend(
622 results.par_extend(
620 stat_dmap_entries(dmap, root_dir, options)
623 stat_dmap_entries(dmap, root_dir, options)
621 .flatten()
624 .flatten()
622 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch)),
625 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch)),
623 );
626 );
624 }
627 }
625
628
626 #[derive(Debug)]
629 #[derive(Debug)]
627 pub struct DirstateStatus<'a> {
630 pub struct DirstateStatus<'a> {
628 pub modified: Vec<Cow<'a, HgPath>>,
631 pub modified: Vec<Cow<'a, HgPath>>,
629 pub added: Vec<Cow<'a, HgPath>>,
632 pub added: Vec<Cow<'a, HgPath>>,
630 pub removed: Vec<Cow<'a, HgPath>>,
633 pub removed: Vec<Cow<'a, HgPath>>,
631 pub deleted: Vec<Cow<'a, HgPath>>,
634 pub deleted: Vec<Cow<'a, HgPath>>,
632 pub clean: Vec<Cow<'a, HgPath>>,
635 pub clean: Vec<Cow<'a, HgPath>>,
633 pub ignored: Vec<Cow<'a, HgPath>>,
636 pub ignored: Vec<Cow<'a, HgPath>>,
634 pub unknown: Vec<Cow<'a, HgPath>>,
637 pub unknown: Vec<Cow<'a, HgPath>>,
635 pub bad: Vec<(Cow<'a, HgPath>, BadMatch)>,
638 pub bad: Vec<(Cow<'a, HgPath>, BadMatch)>,
636 }
639 }
637
640
638 #[timed]
641 #[timed]
639 fn build_response<'a>(
642 fn build_response<'a>(
640 results: impl IntoIterator<Item = (Cow<'a, HgPath>, Dispatch)>,
643 results: impl IntoIterator<Item = (Cow<'a, HgPath>, Dispatch)>,
641 ) -> (Vec<Cow<'a, HgPath>>, DirstateStatus<'a>) {
644 ) -> (Vec<Cow<'a, HgPath>>, DirstateStatus<'a>) {
642 let mut lookup = vec![];
645 let mut lookup = vec![];
643 let mut modified = vec![];
646 let mut modified = vec![];
644 let mut added = vec![];
647 let mut added = vec![];
645 let mut removed = vec![];
648 let mut removed = vec![];
646 let mut deleted = vec![];
649 let mut deleted = vec![];
647 let mut clean = vec![];
650 let mut clean = vec![];
648 let mut ignored = vec![];
651 let mut ignored = vec![];
649 let mut unknown = vec![];
652 let mut unknown = vec![];
650 let mut bad = vec![];
653 let mut bad = vec![];
651
654
652 for (filename, dispatch) in results.into_iter() {
655 for (filename, dispatch) in results.into_iter() {
653 match dispatch {
656 match dispatch {
654 Dispatch::Unknown => unknown.push(filename),
657 Dispatch::Unknown => unknown.push(filename),
655 Dispatch::Unsure => lookup.push(filename),
658 Dispatch::Unsure => lookup.push(filename),
656 Dispatch::Modified => modified.push(filename),
659 Dispatch::Modified => modified.push(filename),
657 Dispatch::Added => added.push(filename),
660 Dispatch::Added => added.push(filename),
658 Dispatch::Removed => removed.push(filename),
661 Dispatch::Removed => removed.push(filename),
659 Dispatch::Deleted => deleted.push(filename),
662 Dispatch::Deleted => deleted.push(filename),
660 Dispatch::Clean => clean.push(filename),
663 Dispatch::Clean => clean.push(filename),
661 Dispatch::Ignored => ignored.push(filename),
664 Dispatch::Ignored => ignored.push(filename),
662 Dispatch::None => {}
665 Dispatch::None => {}
663 Dispatch::Bad(reason) => bad.push((filename, reason)),
666 Dispatch::Bad(reason) => bad.push((filename, reason)),
664 Dispatch::Directory { .. } => {}
667 Dispatch::Directory { .. } => {}
665 }
668 }
666 }
669 }
667
670
668 (
671 (
669 lookup,
672 lookup,
670 DirstateStatus {
673 DirstateStatus {
671 modified,
674 modified,
672 added,
675 added,
673 removed,
676 removed,
674 deleted,
677 deleted,
675 clean,
678 clean,
676 ignored,
679 ignored,
677 unknown,
680 unknown,
678 bad,
681 bad,
679 },
682 },
680 )
683 )
681 }
684 }
682
685
683 #[derive(Debug)]
686 #[derive(Debug)]
684 pub enum StatusError {
687 pub enum StatusError {
685 IO(std::io::Error),
688 IO(std::io::Error),
686 Path(HgPathError),
689 Path(HgPathError),
687 Pattern(PatternError),
690 Pattern(PatternError),
688 }
691 }
689
692
690 pub type StatusResult<T> = Result<T, StatusError>;
693 pub type StatusResult<T> = Result<T, StatusError>;
691
694
692 impl From<PatternError> for StatusError {
695 impl From<PatternError> for StatusError {
693 fn from(e: PatternError) -> Self {
696 fn from(e: PatternError) -> Self {
694 StatusError::Pattern(e)
697 StatusError::Pattern(e)
695 }
698 }
696 }
699 }
697 impl From<HgPathError> for StatusError {
700 impl From<HgPathError> for StatusError {
698 fn from(e: HgPathError) -> Self {
701 fn from(e: HgPathError) -> Self {
699 StatusError::Path(e)
702 StatusError::Path(e)
700 }
703 }
701 }
704 }
702 impl From<std::io::Error> for StatusError {
705 impl From<std::io::Error> for StatusError {
703 fn from(e: std::io::Error) -> Self {
706 fn from(e: std::io::Error) -> Self {
704 StatusError::IO(e)
707 StatusError::IO(e)
705 }
708 }
706 }
709 }
707
710
708 impl ToString for StatusError {
711 impl ToString for StatusError {
709 fn to_string(&self) -> String {
712 fn to_string(&self) -> String {
710 match self {
713 match self {
711 StatusError::IO(e) => e.to_string(),
714 StatusError::IO(e) => e.to_string(),
712 StatusError::Path(e) => e.to_string(),
715 StatusError::Path(e) => e.to_string(),
713 StatusError::Pattern(e) => e.to_string(),
716 StatusError::Pattern(e) => e.to_string(),
714 }
717 }
715 }
718 }
716 }
719 }
717
720
718 /// This takes a mutable reference to the results to account for the `extend`
721 /// This takes a mutable reference to the results to account for the `extend`
719 /// in timings
722 /// in timings
720 #[timed]
723 #[timed]
721 fn handle_unknowns<'a>(
724 fn handle_unknowns<'a>(
722 dmap: &'a DirstateMap,
725 dmap: &'a DirstateMap,
723 matcher: &(impl Matcher + Sync),
726 matcher: &(impl Matcher + Sync),
724 root_dir: impl AsRef<Path> + Sync + Send + Copy,
727 root_dir: impl AsRef<Path> + Sync + Send + Copy,
725 options: StatusOptions,
728 options: StatusOptions,
726 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
729 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
727 ) -> IoResult<()> {
730 ) -> IoResult<()> {
728 let to_visit: Vec<(&HgPath, &DirstateEntry)> = if results.is_empty()
731 let to_visit: Vec<(&HgPath, &DirstateEntry)> = if results.is_empty()
729 && matcher.matches_everything()
732 && matcher.matches_everything()
730 {
733 {
731 dmap.iter().map(|(f, e)| (f.deref(), e)).collect()
734 dmap.iter().map(|(f, e)| (f.deref(), e)).collect()
732 } else {
735 } else {
733 // Only convert to a hashmap if needed.
736 // Only convert to a hashmap if needed.
734 let old_results: FastHashMap<_, _> = results.iter().cloned().collect();
737 let old_results: FastHashMap<_, _> = results.iter().cloned().collect();
735 dmap.iter()
738 dmap.iter()
736 .filter_map(move |(f, e)| {
739 .filter_map(move |(f, e)| {
737 if !old_results.contains_key(f.deref()) && matcher.matches(f) {
740 if !old_results.contains_key(f.deref()) && matcher.matches(f) {
738 Some((f.deref(), e))
741 Some((f.deref(), e))
739 } else {
742 } else {
740 None
743 None
741 }
744 }
742 })
745 })
743 .collect()
746 .collect()
744 };
747 };
745
748
746 // We walked all dirs under the roots that weren't ignored, and
749 // We walked all dirs under the roots that weren't ignored, and
747 // everything that matched was stat'ed and is already in results.
750 // everything that matched was stat'ed and is already in results.
748 // The rest must thus be ignored or under a symlink.
751 // The rest must thus be ignored or under a symlink.
749 let path_auditor = PathAuditor::new(root_dir);
752 let path_auditor = PathAuditor::new(root_dir);
750
753
751 // TODO don't collect. Find a way of replicating the behavior of
754 // TODO don't collect. Find a way of replicating the behavior of
752 // `itertools::process_results`, but for `rayon::ParallelIterator`
755 // `itertools::process_results`, but for `rayon::ParallelIterator`
753 let new_results: IoResult<Vec<_>> = to_visit
756 let new_results: IoResult<Vec<_>> = to_visit
754 .into_par_iter()
757 .into_par_iter()
755 .filter_map(|(filename, entry)| -> Option<IoResult<_>> {
758 .filter_map(|(filename, entry)| -> Option<IoResult<_>> {
756 // Report ignored items in the dmap as long as they are not
759 // Report ignored items in the dmap as long as they are not
757 // under a symlink directory.
760 // under a symlink directory.
758 if path_auditor.check(filename) {
761 if path_auditor.check(filename) {
759 // TODO normalize for case-insensitive filesystems
762 // TODO normalize for case-insensitive filesystems
760 let buf = match hg_path_to_path_buf(filename) {
763 let buf = match hg_path_to_path_buf(filename) {
761 Ok(x) => x,
764 Ok(x) => x,
762 Err(e) => return Some(Err(e.into())),
765 Err(e) => return Some(Err(e.into())),
763 };
766 };
764 Some(Ok((
767 Some(Ok((
765 Cow::Borrowed(filename),
768 Cow::Borrowed(filename),
766 match root_dir.as_ref().join(&buf).symlink_metadata() {
769 match root_dir.as_ref().join(&buf).symlink_metadata() {
767 // File was just ignored, no links, and exists
770 // File was just ignored, no links, and exists
768 Ok(meta) => {
771 Ok(meta) => {
769 let metadata = HgMetadata::from_metadata(meta);
772 let metadata = HgMetadata::from_metadata(meta);
770 dispatch_found(
773 dispatch_found(
771 filename,
774 filename,
772 *entry,
775 *entry,
773 metadata,
776 metadata,
774 &dmap.copy_map,
777 &dmap.copy_map,
775 options,
778 options,
776 )
779 )
777 }
780 }
778 // File doesn't exist
781 // File doesn't exist
779 Err(_) => dispatch_missing(entry.state),
782 Err(_) => dispatch_missing(entry.state),
780 },
783 },
781 )))
784 )))
782 } else {
785 } else {
783 // It's either missing or under a symlink directory which
786 // It's either missing or under a symlink directory which
784 // we, in this case, report as missing.
787 // we, in this case, report as missing.
785 Some(Ok((
788 Some(Ok((
786 Cow::Borrowed(filename),
789 Cow::Borrowed(filename),
787 dispatch_missing(entry.state),
790 dispatch_missing(entry.state),
788 )))
791 )))
789 }
792 }
790 })
793 })
791 .collect();
794 .collect();
792
795
793 results.par_extend(new_results?);
796 results.par_extend(new_results?);
794
797
795 Ok(())
798 Ok(())
796 }
799 }
797
800
798 /// Get the status of files in the working directory.
801 /// Get the status of files in the working directory.
799 ///
802 ///
800 /// This is the current entry-point for `hg-core` and is realistically unusable
803 /// This is the current entry-point for `hg-core` and is realistically unusable
801 /// outside of a Python context because its arguments need to provide a lot of
804 /// outside of a Python context because its arguments need to provide a lot of
802 /// information that will not be necessary in the future.
805 /// information that will not be necessary in the future.
803 #[timed]
806 #[timed]
804 pub fn status<'a: 'c, 'b: 'c, 'c>(
807 pub fn status<'a: 'c, 'b: 'c, 'c>(
805 dmap: &'a DirstateMap,
808 dmap: &'a DirstateMap,
806 matcher: &'b (impl Matcher + Sync),
809 matcher: &'b (impl Matcher + Sync),
807 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'c,
810 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'c,
808 ignore_files: &[impl AsRef<Path> + 'c],
811 ignore_files: Vec<PathBuf>,
809 options: StatusOptions,
812 options: StatusOptions,
810 ) -> StatusResult<(
813 ) -> StatusResult<(
811 (Vec<Cow<'c, HgPath>>, DirstateStatus<'c>),
814 (Vec<Cow<'c, HgPath>>, DirstateStatus<'c>),
812 Vec<PatternFileWarning>,
815 Vec<PatternFileWarning>,
813 )> {
816 )> {
814 let (ignore_fn, warnings) = get_ignore_function(&ignore_files, root_dir)?;
817 // Needs to outlive `dir_ignore_fn` since it's captured.
818 let mut ignore_fn: IgnoreFnType;
815
819
820 // Only involve real ignore mechanism if we're listing unknowns or ignored.
821 let (dir_ignore_fn, warnings): (IgnoreFnType, _) = if options.list_ignored
822 || options.list_unknown
823 {
824 let (ignore, warnings) = get_ignore_function(ignore_files, root_dir)?;
825
826 ignore_fn = ignore;
827 let dir_ignore_fn = Box::new(|dir: &_| {
816 // Is the path or one of its ancestors ignored?
828 // Is the path or one of its ancestors ignored?
817 let dir_ignore_fn = |dir: &_| {
818 if ignore_fn(dir) {
829 if ignore_fn(dir) {
819 true
830 true
820 } else {
831 } else {
821 for p in find_dirs(dir) {
832 for p in find_dirs(dir) {
822 if ignore_fn(p) {
833 if ignore_fn(p) {
823 return true;
834 return true;
824 }
835 }
825 }
836 }
826 false
837 false
827 }
838 }
839 });
840 (dir_ignore_fn, warnings)
841 } else {
842 ignore_fn = Box::new(|&_| true);
843 (Box::new(|&_| true), vec![])
828 };
844 };
829
845
830 let files = matcher.file_set();
846 let files = matcher.file_set();
831
847
832 // Step 1: check the files explicitly mentioned by the user
848 // Step 1: check the files explicitly mentioned by the user
833 let explicit = walk_explicit(files, &dmap, root_dir, options);
849 let explicit = walk_explicit(files, &dmap, root_dir, options);
834
850
835 // Collect results into a `Vec` because we do very few lookups in most
851 // Collect results into a `Vec` because we do very few lookups in most
836 // cases.
852 // cases.
837 let (work, mut results): (Vec<_>, Vec<_>) = explicit
853 let (work, mut results): (Vec<_>, Vec<_>) = explicit
838 .filter_map(Result::ok)
854 .filter_map(Result::ok)
839 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch))
855 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch))
840 .partition(|(_, dispatch)| match dispatch {
856 .partition(|(_, dispatch)| match dispatch {
841 Dispatch::Directory { .. } => true,
857 Dispatch::Directory { .. } => true,
842 _ => false,
858 _ => false,
843 });
859 });
844
860
845 if !work.is_empty() {
861 if !work.is_empty() {
846 // Hashmaps are quite a bit slower to build than vecs, so only build it
862 // Hashmaps are quite a bit slower to build than vecs, so only build it
847 // if needed.
863 // if needed.
848 let old_results = results.iter().cloned().collect();
864 let old_results = results.iter().cloned().collect();
849
865
850 // Step 2: recursively check the working directory for changes if
866 // Step 2: recursively check the working directory for changes if
851 // needed
867 // needed
852 for (dir, dispatch) in work {
868 for (dir, dispatch) in work {
853 match dispatch {
869 match dispatch {
854 Dispatch::Directory { was_file } => {
870 Dispatch::Directory { was_file } => {
855 if was_file {
871 if was_file {
856 results.push((dir.to_owned(), Dispatch::Removed));
872 results.push((dir.to_owned(), Dispatch::Removed));
857 }
873 }
858 if options.list_ignored
874 if options.list_ignored
859 || options.list_unknown && !dir_ignore_fn(&dir)
875 || options.list_unknown && !dir_ignore_fn(&dir)
860 {
876 {
861 traverse(
877 traverse(
862 matcher,
878 matcher,
863 root_dir,
879 root_dir,
864 &dmap,
880 &dmap,
865 &dir,
881 &dir,
866 &old_results,
882 &old_results,
867 &ignore_fn,
883 &ignore_fn,
868 &dir_ignore_fn,
884 &dir_ignore_fn,
869 options,
885 options,
870 &mut results,
886 &mut results,
871 )?;
887 )?;
872 }
888 }
873 }
889 }
874 _ => unreachable!("There can only be directories in `work`"),
890 _ => unreachable!("There can only be directories in `work`"),
875 }
891 }
876 }
892 }
877 }
893 }
878
894
879 if !matcher.is_exact() {
895 if !matcher.is_exact() {
880 // Step 3: Check the remaining files from the dmap.
896 // Step 3: Check the remaining files from the dmap.
881 // If a dmap file is not in results yet, it was either
897 // If a dmap file is not in results yet, it was either
882 // a) not matched b) ignored, c) missing, or d) under a
898 // a) not matched b) ignored, c) missing, or d) under a
883 // symlink directory.
899 // symlink directory.
884
900
885 if options.list_unknown {
901 if options.list_unknown {
886 handle_unknowns(dmap, matcher, root_dir, options, &mut results)?;
902 handle_unknowns(dmap, matcher, root_dir, options, &mut results)?;
887 } else {
903 } else {
888 // We may not have walked the full directory tree above, so stat
904 // We may not have walked the full directory tree above, so stat
889 // and check everything we missed.
905 // and check everything we missed.
890 extend_from_dmap(&dmap, root_dir, options, &mut results);
906 extend_from_dmap(&dmap, root_dir, options, &mut results);
891 }
907 }
892 }
908 }
893
909
894 Ok((build_response(results), warnings))
910 Ok((build_response(results), warnings))
895 }
911 }
@@ -1,923 +1,926 b''
1 // matchers.rs
1 // matchers.rs
2 //
2 //
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
6 // GNU General Public License version 2 or any later version.
7
7
8 //! Structs and types for matching files and directories.
8 //! Structs and types for matching files and directories.
9
9
10 #[cfg(feature = "with-re2")]
10 #[cfg(feature = "with-re2")]
11 use crate::re2::Re2;
11 use crate::re2::Re2;
12 use crate::{
12 use crate::{
13 dirstate::dirs_multiset::DirsChildrenMultiset,
13 dirstate::dirs_multiset::DirsChildrenMultiset,
14 filepatterns::{
14 filepatterns::{
15 build_single_regex, filter_subincludes, get_patterns_from_file,
15 build_single_regex, filter_subincludes, get_patterns_from_file,
16 PatternFileWarning, PatternResult, SubInclude,
16 PatternFileWarning, PatternResult, SubInclude,
17 },
17 },
18 utils::{
18 utils::{
19 files::find_dirs,
19 files::find_dirs,
20 hg_path::{HgPath, HgPathBuf},
20 hg_path::{HgPath, HgPathBuf},
21 Escaped,
21 Escaped,
22 },
22 },
23 DirsMultiset, DirstateMapError, FastHashMap, IgnorePattern, PatternError,
23 DirsMultiset, DirstateMapError, FastHashMap, IgnorePattern, PatternError,
24 PatternSyntax,
24 PatternSyntax,
25 };
25 };
26
26
27 use micro_timer::timed;
27 use std::borrow::ToOwned;
28 use std::collections::HashSet;
28 use std::collections::HashSet;
29 use std::fmt::{Display, Error, Formatter};
29 use std::fmt::{Display, Error, Formatter};
30 use std::iter::FromIterator;
30 use std::iter::FromIterator;
31 use std::ops::Deref;
31 use std::ops::Deref;
32 use std::path::Path;
32 use std::path::{Path, PathBuf};
33
33
34 #[derive(Debug, PartialEq)]
34 #[derive(Debug, PartialEq)]
35 pub enum VisitChildrenSet<'a> {
35 pub enum VisitChildrenSet<'a> {
36 /// Don't visit anything
36 /// Don't visit anything
37 Empty,
37 Empty,
38 /// Only visit this directory
38 /// Only visit this directory
39 This,
39 This,
40 /// Visit this directory and these subdirectories
40 /// Visit this directory and these subdirectories
41 /// TODO Should we implement a `NonEmptyHashSet`?
41 /// TODO Should we implement a `NonEmptyHashSet`?
42 Set(HashSet<&'a HgPath>),
42 Set(HashSet<&'a HgPath>),
43 /// Visit this directory and all subdirectories
43 /// Visit this directory and all subdirectories
44 Recursive,
44 Recursive,
45 }
45 }
46
46
47 pub trait Matcher {
47 pub trait Matcher {
48 /// Explicitly listed files
48 /// Explicitly listed files
49 fn file_set(&self) -> Option<&HashSet<&HgPath>>;
49 fn file_set(&self) -> Option<&HashSet<&HgPath>>;
50 /// Returns whether `filename` is in `file_set`
50 /// Returns whether `filename` is in `file_set`
51 fn exact_match(&self, filename: impl AsRef<HgPath>) -> bool;
51 fn exact_match(&self, filename: impl AsRef<HgPath>) -> bool;
52 /// Returns whether `filename` is matched by this matcher
52 /// Returns whether `filename` is matched by this matcher
53 fn matches(&self, filename: impl AsRef<HgPath>) -> bool;
53 fn matches(&self, filename: impl AsRef<HgPath>) -> bool;
54 /// Decides whether a directory should be visited based on whether it
54 /// Decides whether a directory should be visited based on whether it
55 /// has potential matches in it or one of its subdirectories, and
55 /// has potential matches in it or one of its subdirectories, and
56 /// potentially lists which subdirectories of that directory should be
56 /// potentially lists which subdirectories of that directory should be
57 /// visited. This is based on the match's primary, included, and excluded
57 /// visited. This is based on the match's primary, included, and excluded
58 /// patterns.
58 /// patterns.
59 ///
59 ///
60 /// # Example
60 /// # Example
61 ///
61 ///
62 /// Assume matchers `['path:foo/bar', 'rootfilesin:qux']`, we would
62 /// Assume matchers `['path:foo/bar', 'rootfilesin:qux']`, we would
63 /// return the following values (assuming the implementation of
63 /// return the following values (assuming the implementation of
64 /// visit_children_set is capable of recognizing this; some implementations
64 /// visit_children_set is capable of recognizing this; some implementations
65 /// are not).
65 /// are not).
66 ///
66 ///
67 /// ```text
67 /// ```text
68 /// ```ignore
68 /// ```ignore
69 /// '' -> {'foo', 'qux'}
69 /// '' -> {'foo', 'qux'}
70 /// 'baz' -> set()
70 /// 'baz' -> set()
71 /// 'foo' -> {'bar'}
71 /// 'foo' -> {'bar'}
72 /// // Ideally this would be `Recursive`, but since the prefix nature of
72 /// // Ideally this would be `Recursive`, but since the prefix nature of
73 /// // matchers is applied to the entire matcher, we have to downgrade this
73 /// // matchers is applied to the entire matcher, we have to downgrade this
74 /// // to `This` due to the (yet to be implemented in Rust) non-prefix
74 /// // to `This` due to the (yet to be implemented in Rust) non-prefix
75 /// // `RootFilesIn'-kind matcher being mixed in.
75 /// // `RootFilesIn'-kind matcher being mixed in.
76 /// 'foo/bar' -> 'this'
76 /// 'foo/bar' -> 'this'
77 /// 'qux' -> 'this'
77 /// 'qux' -> 'this'
78 /// ```
78 /// ```
79 /// # Important
79 /// # Important
80 ///
80 ///
81 /// Most matchers do not know if they're representing files or
81 /// Most matchers do not know if they're representing files or
82 /// directories. They see `['path:dir/f']` and don't know whether `f` is a
82 /// directories. They see `['path:dir/f']` and don't know whether `f` is a
83 /// file or a directory, so `visit_children_set('dir')` for most matchers
83 /// file or a directory, so `visit_children_set('dir')` for most matchers
84 /// will return `HashSet{ HgPath { "f" } }`, but if the matcher knows it's
84 /// will return `HashSet{ HgPath { "f" } }`, but if the matcher knows it's
85 /// a file (like the yet to be implemented in Rust `ExactMatcher` does),
85 /// a file (like the yet to be implemented in Rust `ExactMatcher` does),
86 /// it may return `VisitChildrenSet::This`.
86 /// it may return `VisitChildrenSet::This`.
87 /// Do not rely on the return being a `HashSet` indicating that there are
87 /// Do not rely on the return being a `HashSet` indicating that there are
88 /// no files in this dir to investigate (or equivalently that if there are
88 /// no files in this dir to investigate (or equivalently that if there are
89 /// files to investigate in 'dir' that it will always return
89 /// files to investigate in 'dir' that it will always return
90 /// `VisitChildrenSet::This`).
90 /// `VisitChildrenSet::This`).
91 fn visit_children_set(
91 fn visit_children_set(
92 &self,
92 &self,
93 directory: impl AsRef<HgPath>,
93 directory: impl AsRef<HgPath>,
94 ) -> VisitChildrenSet;
94 ) -> VisitChildrenSet;
95 /// Matcher will match everything and `files_set()` will be empty:
95 /// Matcher will match everything and `files_set()` will be empty:
96 /// optimization might be possible.
96 /// optimization might be possible.
97 fn matches_everything(&self) -> bool;
97 fn matches_everything(&self) -> bool;
98 /// Matcher will match exactly the files in `files_set()`: optimization
98 /// Matcher will match exactly the files in `files_set()`: optimization
99 /// might be possible.
99 /// might be possible.
100 fn is_exact(&self) -> bool;
100 fn is_exact(&self) -> bool;
101 }
101 }
102
102
103 /// Matches everything.
103 /// Matches everything.
104 ///```
104 ///```
105 /// use hg::{ matchers::{Matcher, AlwaysMatcher}, utils::hg_path::HgPath };
105 /// use hg::{ matchers::{Matcher, AlwaysMatcher}, utils::hg_path::HgPath };
106 ///
106 ///
107 /// let matcher = AlwaysMatcher;
107 /// let matcher = AlwaysMatcher;
108 ///
108 ///
109 /// assert_eq!(matcher.matches(HgPath::new(b"whatever")), true);
109 /// assert_eq!(matcher.matches(HgPath::new(b"whatever")), true);
110 /// assert_eq!(matcher.matches(HgPath::new(b"b.txt")), true);
110 /// assert_eq!(matcher.matches(HgPath::new(b"b.txt")), true);
111 /// assert_eq!(matcher.matches(HgPath::new(b"main.c")), true);
111 /// assert_eq!(matcher.matches(HgPath::new(b"main.c")), true);
112 /// assert_eq!(matcher.matches(HgPath::new(br"re:.*\.c$")), true);
112 /// assert_eq!(matcher.matches(HgPath::new(br"re:.*\.c$")), true);
113 /// ```
113 /// ```
114 #[derive(Debug)]
114 #[derive(Debug)]
115 pub struct AlwaysMatcher;
115 pub struct AlwaysMatcher;
116
116
117 impl Matcher for AlwaysMatcher {
117 impl Matcher for AlwaysMatcher {
118 fn file_set(&self) -> Option<&HashSet<&HgPath>> {
118 fn file_set(&self) -> Option<&HashSet<&HgPath>> {
119 None
119 None
120 }
120 }
121 fn exact_match(&self, _filename: impl AsRef<HgPath>) -> bool {
121 fn exact_match(&self, _filename: impl AsRef<HgPath>) -> bool {
122 false
122 false
123 }
123 }
124 fn matches(&self, _filename: impl AsRef<HgPath>) -> bool {
124 fn matches(&self, _filename: impl AsRef<HgPath>) -> bool {
125 true
125 true
126 }
126 }
127 fn visit_children_set(
127 fn visit_children_set(
128 &self,
128 &self,
129 _directory: impl AsRef<HgPath>,
129 _directory: impl AsRef<HgPath>,
130 ) -> VisitChildrenSet {
130 ) -> VisitChildrenSet {
131 VisitChildrenSet::Recursive
131 VisitChildrenSet::Recursive
132 }
132 }
133 fn matches_everything(&self) -> bool {
133 fn matches_everything(&self) -> bool {
134 true
134 true
135 }
135 }
136 fn is_exact(&self) -> bool {
136 fn is_exact(&self) -> bool {
137 false
137 false
138 }
138 }
139 }
139 }
140
140
141 /// Matches the input files exactly. They are interpreted as paths, not
141 /// Matches the input files exactly. They are interpreted as paths, not
142 /// patterns.
142 /// patterns.
143 ///
143 ///
144 ///```
144 ///```
145 /// use hg::{ matchers::{Matcher, FileMatcher}, utils::hg_path::HgPath };
145 /// use hg::{ matchers::{Matcher, FileMatcher}, utils::hg_path::HgPath };
146 ///
146 ///
147 /// let files = [HgPath::new(b"a.txt"), HgPath::new(br"re:.*\.c$")];
147 /// let files = [HgPath::new(b"a.txt"), HgPath::new(br"re:.*\.c$")];
148 /// let matcher = FileMatcher::new(&files).unwrap();
148 /// let matcher = FileMatcher::new(&files).unwrap();
149 ///
149 ///
150 /// assert_eq!(matcher.matches(HgPath::new(b"a.txt")), true);
150 /// assert_eq!(matcher.matches(HgPath::new(b"a.txt")), true);
151 /// assert_eq!(matcher.matches(HgPath::new(b"b.txt")), false);
151 /// assert_eq!(matcher.matches(HgPath::new(b"b.txt")), false);
152 /// assert_eq!(matcher.matches(HgPath::new(b"main.c")), false);
152 /// assert_eq!(matcher.matches(HgPath::new(b"main.c")), false);
153 /// assert_eq!(matcher.matches(HgPath::new(br"re:.*\.c$")), true);
153 /// assert_eq!(matcher.matches(HgPath::new(br"re:.*\.c$")), true);
154 /// ```
154 /// ```
155 #[derive(Debug)]
155 #[derive(Debug)]
156 pub struct FileMatcher<'a> {
156 pub struct FileMatcher<'a> {
157 files: HashSet<&'a HgPath>,
157 files: HashSet<&'a HgPath>,
158 dirs: DirsMultiset,
158 dirs: DirsMultiset,
159 }
159 }
160
160
161 impl<'a> FileMatcher<'a> {
161 impl<'a> FileMatcher<'a> {
162 pub fn new(
162 pub fn new(
163 files: &'a [impl AsRef<HgPath>],
163 files: &'a [impl AsRef<HgPath>],
164 ) -> Result<Self, DirstateMapError> {
164 ) -> Result<Self, DirstateMapError> {
165 Ok(Self {
165 Ok(Self {
166 files: HashSet::from_iter(files.iter().map(|f| f.as_ref())),
166 files: HashSet::from_iter(files.iter().map(|f| f.as_ref())),
167 dirs: DirsMultiset::from_manifest(files)?,
167 dirs: DirsMultiset::from_manifest(files)?,
168 })
168 })
169 }
169 }
170 fn inner_matches(&self, filename: impl AsRef<HgPath>) -> bool {
170 fn inner_matches(&self, filename: impl AsRef<HgPath>) -> bool {
171 self.files.contains(filename.as_ref())
171 self.files.contains(filename.as_ref())
172 }
172 }
173 }
173 }
174
174
175 impl<'a> Matcher for FileMatcher<'a> {
175 impl<'a> Matcher for FileMatcher<'a> {
176 fn file_set(&self) -> Option<&HashSet<&HgPath>> {
176 fn file_set(&self) -> Option<&HashSet<&HgPath>> {
177 Some(&self.files)
177 Some(&self.files)
178 }
178 }
179 fn exact_match(&self, filename: impl AsRef<HgPath>) -> bool {
179 fn exact_match(&self, filename: impl AsRef<HgPath>) -> bool {
180 self.inner_matches(filename)
180 self.inner_matches(filename)
181 }
181 }
182 fn matches(&self, filename: impl AsRef<HgPath>) -> bool {
182 fn matches(&self, filename: impl AsRef<HgPath>) -> bool {
183 self.inner_matches(filename)
183 self.inner_matches(filename)
184 }
184 }
185 fn visit_children_set(
185 fn visit_children_set(
186 &self,
186 &self,
187 directory: impl AsRef<HgPath>,
187 directory: impl AsRef<HgPath>,
188 ) -> VisitChildrenSet {
188 ) -> VisitChildrenSet {
189 if self.files.is_empty() || !self.dirs.contains(&directory) {
189 if self.files.is_empty() || !self.dirs.contains(&directory) {
190 return VisitChildrenSet::Empty;
190 return VisitChildrenSet::Empty;
191 }
191 }
192 let dirs_as_set = self.dirs.iter().map(|k| k.deref()).collect();
192 let dirs_as_set = self.dirs.iter().map(|k| k.deref()).collect();
193
193
194 let mut candidates: HashSet<&HgPath> =
194 let mut candidates: HashSet<&HgPath> =
195 self.files.union(&dirs_as_set).map(|k| *k).collect();
195 self.files.union(&dirs_as_set).map(|k| *k).collect();
196 candidates.remove(HgPath::new(b""));
196 candidates.remove(HgPath::new(b""));
197
197
198 if !directory.as_ref().is_empty() {
198 if !directory.as_ref().is_empty() {
199 let directory = [directory.as_ref().as_bytes(), b"/"].concat();
199 let directory = [directory.as_ref().as_bytes(), b"/"].concat();
200 candidates = candidates
200 candidates = candidates
201 .iter()
201 .iter()
202 .filter_map(|c| {
202 .filter_map(|c| {
203 if c.as_bytes().starts_with(&directory) {
203 if c.as_bytes().starts_with(&directory) {
204 Some(HgPath::new(&c.as_bytes()[directory.len()..]))
204 Some(HgPath::new(&c.as_bytes()[directory.len()..]))
205 } else {
205 } else {
206 None
206 None
207 }
207 }
208 })
208 })
209 .collect();
209 .collect();
210 }
210 }
211
211
212 // `self.dirs` includes all of the directories, recursively, so if
212 // `self.dirs` includes all of the directories, recursively, so if
213 // we're attempting to match 'foo/bar/baz.txt', it'll have '', 'foo',
213 // we're attempting to match 'foo/bar/baz.txt', it'll have '', 'foo',
214 // 'foo/bar' in it. Thus we can safely ignore a candidate that has a
214 // 'foo/bar' in it. Thus we can safely ignore a candidate that has a
215 // '/' in it, indicating it's for a subdir-of-a-subdir; the immediate
215 // '/' in it, indicating it's for a subdir-of-a-subdir; the immediate
216 // subdir will be in there without a slash.
216 // subdir will be in there without a slash.
217 VisitChildrenSet::Set(
217 VisitChildrenSet::Set(
218 candidates
218 candidates
219 .iter()
219 .iter()
220 .filter_map(|c| {
220 .filter_map(|c| {
221 if c.bytes().all(|b| *b != b'/') {
221 if c.bytes().all(|b| *b != b'/') {
222 Some(*c)
222 Some(*c)
223 } else {
223 } else {
224 None
224 None
225 }
225 }
226 })
226 })
227 .collect(),
227 .collect(),
228 )
228 )
229 }
229 }
230 fn matches_everything(&self) -> bool {
230 fn matches_everything(&self) -> bool {
231 false
231 false
232 }
232 }
233 fn is_exact(&self) -> bool {
233 fn is_exact(&self) -> bool {
234 true
234 true
235 }
235 }
236 }
236 }
237
237
238 /// Matches files that are included in the ignore rules.
238 /// Matches files that are included in the ignore rules.
239 #[cfg_attr(
239 #[cfg_attr(
240 feature = "with-re2",
240 feature = "with-re2",
241 doc = r##"
241 doc = r##"
242 ```
242 ```
243 use hg::{
243 use hg::{
244 matchers::{IncludeMatcher, Matcher},
244 matchers::{IncludeMatcher, Matcher},
245 IgnorePattern,
245 IgnorePattern,
246 PatternSyntax,
246 PatternSyntax,
247 utils::hg_path::HgPath
247 utils::hg_path::HgPath
248 };
248 };
249 use std::path::Path;
249 use std::path::Path;
250 ///
250 ///
251 let ignore_patterns =
251 let ignore_patterns =
252 vec![IgnorePattern::new(PatternSyntax::RootGlob, b"this*", Path::new(""))];
252 vec![IgnorePattern::new(PatternSyntax::RootGlob, b"this*", Path::new(""))];
253 let (matcher, _) = IncludeMatcher::new(ignore_patterns, "").unwrap();
253 let (matcher, _) = IncludeMatcher::new(ignore_patterns, "").unwrap();
254 ///
254 ///
255 assert_eq!(matcher.matches(HgPath::new(b"testing")), false);
255 assert_eq!(matcher.matches(HgPath::new(b"testing")), false);
256 assert_eq!(matcher.matches(HgPath::new(b"this should work")), true);
256 assert_eq!(matcher.matches(HgPath::new(b"this should work")), true);
257 assert_eq!(matcher.matches(HgPath::new(b"this also")), true);
257 assert_eq!(matcher.matches(HgPath::new(b"this also")), true);
258 assert_eq!(matcher.matches(HgPath::new(b"but not this")), false);
258 assert_eq!(matcher.matches(HgPath::new(b"but not this")), false);
259 ```
259 ```
260 "##
260 "##
261 )]
261 )]
262 pub struct IncludeMatcher<'a> {
262 pub struct IncludeMatcher<'a> {
263 patterns: Vec<u8>,
263 patterns: Vec<u8>,
264 match_fn: Box<dyn for<'r> Fn(&'r HgPath) -> bool + 'a + Sync>,
264 match_fn: Box<dyn for<'r> Fn(&'r HgPath) -> bool + 'a + Sync>,
265 /// Whether all the patterns match a prefix (i.e. recursively)
265 /// Whether all the patterns match a prefix (i.e. recursively)
266 prefix: bool,
266 prefix: bool,
267 roots: HashSet<HgPathBuf>,
267 roots: HashSet<HgPathBuf>,
268 dirs: HashSet<HgPathBuf>,
268 dirs: HashSet<HgPathBuf>,
269 parents: HashSet<HgPathBuf>,
269 parents: HashSet<HgPathBuf>,
270 }
270 }
271
271
272 impl<'a> Matcher for IncludeMatcher<'a> {
272 impl<'a> Matcher for IncludeMatcher<'a> {
273 fn file_set(&self) -> Option<&HashSet<&HgPath>> {
273 fn file_set(&self) -> Option<&HashSet<&HgPath>> {
274 None
274 None
275 }
275 }
276
276
277 fn exact_match(&self, _filename: impl AsRef<HgPath>) -> bool {
277 fn exact_match(&self, _filename: impl AsRef<HgPath>) -> bool {
278 false
278 false
279 }
279 }
280
280
281 fn matches(&self, filename: impl AsRef<HgPath>) -> bool {
281 fn matches(&self, filename: impl AsRef<HgPath>) -> bool {
282 (self.match_fn)(filename.as_ref())
282 (self.match_fn)(filename.as_ref())
283 }
283 }
284
284
285 fn visit_children_set(
285 fn visit_children_set(
286 &self,
286 &self,
287 directory: impl AsRef<HgPath>,
287 directory: impl AsRef<HgPath>,
288 ) -> VisitChildrenSet {
288 ) -> VisitChildrenSet {
289 let dir = directory.as_ref();
289 let dir = directory.as_ref();
290 if self.prefix && self.roots.contains(dir) {
290 if self.prefix && self.roots.contains(dir) {
291 return VisitChildrenSet::Recursive;
291 return VisitChildrenSet::Recursive;
292 }
292 }
293 if self.roots.contains(HgPath::new(b""))
293 if self.roots.contains(HgPath::new(b""))
294 || self.roots.contains(dir)
294 || self.roots.contains(dir)
295 || self.dirs.contains(dir)
295 || self.dirs.contains(dir)
296 || find_dirs(dir).any(|parent_dir| self.roots.contains(parent_dir))
296 || find_dirs(dir).any(|parent_dir| self.roots.contains(parent_dir))
297 {
297 {
298 return VisitChildrenSet::This;
298 return VisitChildrenSet::This;
299 }
299 }
300
300
301 if self.parents.contains(directory.as_ref()) {
301 if self.parents.contains(directory.as_ref()) {
302 let multiset = self.get_all_parents_children();
302 let multiset = self.get_all_parents_children();
303 if let Some(children) = multiset.get(dir) {
303 if let Some(children) = multiset.get(dir) {
304 return VisitChildrenSet::Set(children.to_owned());
304 return VisitChildrenSet::Set(children.to_owned());
305 }
305 }
306 }
306 }
307 VisitChildrenSet::Empty
307 VisitChildrenSet::Empty
308 }
308 }
309
309
310 fn matches_everything(&self) -> bool {
310 fn matches_everything(&self) -> bool {
311 false
311 false
312 }
312 }
313
313
314 fn is_exact(&self) -> bool {
314 fn is_exact(&self) -> bool {
315 false
315 false
316 }
316 }
317 }
317 }
318
318
319 #[cfg(feature = "with-re2")]
319 #[cfg(feature = "with-re2")]
320 /// Returns a function that matches an `HgPath` against the given regex
320 /// Returns a function that matches an `HgPath` against the given regex
321 /// pattern.
321 /// pattern.
322 ///
322 ///
323 /// This can fail when the pattern is invalid or not supported by the
323 /// This can fail when the pattern is invalid or not supported by the
324 /// underlying engine `Re2`, for instance anything with back-references.
324 /// underlying engine `Re2`, for instance anything with back-references.
325 fn re_matcher(
325 fn re_matcher(
326 pattern: &[u8],
326 pattern: &[u8],
327 ) -> PatternResult<impl Fn(&HgPath) -> bool + Sync> {
327 ) -> PatternResult<impl Fn(&HgPath) -> bool + Sync> {
328 let regex = Re2::new(pattern);
328 let regex = Re2::new(pattern);
329 let regex = regex.map_err(|e| PatternError::UnsupportedSyntax(e))?;
329 let regex = regex.map_err(|e| PatternError::UnsupportedSyntax(e))?;
330 Ok(move |path: &HgPath| regex.is_match(path.as_bytes()))
330 Ok(move |path: &HgPath| regex.is_match(path.as_bytes()))
331 }
331 }
332
332
333 #[cfg(not(feature = "with-re2"))]
333 #[cfg(not(feature = "with-re2"))]
334 /// Returns a function that matches an `HgPath` against the given regex
334 /// Returns a function that matches an `HgPath` against the given regex
335 /// pattern.
335 /// pattern.
336 ///
336 ///
337 /// This can fail when the pattern is invalid or not supported by the
337 /// This can fail when the pattern is invalid or not supported by the
338 /// underlying engine (the `regex` crate), for instance anything with
338 /// underlying engine (the `regex` crate), for instance anything with
339 /// back-references.
339 /// back-references.
340 fn re_matcher(
340 fn re_matcher(
341 pattern: &[u8],
341 pattern: &[u8],
342 ) -> PatternResult<impl Fn(&HgPath) -> bool + Sync> {
342 ) -> PatternResult<impl Fn(&HgPath) -> bool + Sync> {
343 use std::io::Write;
343 use std::io::Write;
344
344
345 let mut escaped_bytes = vec![];
345 let mut escaped_bytes = vec![];
346 for byte in pattern {
346 for byte in pattern {
347 if *byte > 127 {
347 if *byte > 127 {
348 write!(escaped_bytes, "\\x{:x}", *byte).unwrap();
348 write!(escaped_bytes, "\\x{:x}", *byte).unwrap();
349 } else {
349 } else {
350 escaped_bytes.push(*byte);
350 escaped_bytes.push(*byte);
351 }
351 }
352 }
352 }
353
353
354 // Avoid the cost of UTF8 checking
354 // Avoid the cost of UTF8 checking
355 //
355 //
356 // # Safety
356 // # Safety
357 // This is safe because we escaped all non-ASCII bytes.
357 // This is safe because we escaped all non-ASCII bytes.
358 let pattern_string = unsafe { String::from_utf8_unchecked(escaped_bytes) };
358 let pattern_string = unsafe { String::from_utf8_unchecked(escaped_bytes) };
359 let re = regex::bytes::RegexBuilder::new(&pattern_string)
359 let re = regex::bytes::RegexBuilder::new(&pattern_string)
360 .unicode(false)
360 .unicode(false)
361 .build()
361 .build()
362 .map_err(|e| PatternError::UnsupportedSyntax(e.to_string()))?;
362 .map_err(|e| PatternError::UnsupportedSyntax(e.to_string()))?;
363
363
364 Ok(move |path: &HgPath| re.is_match(path.as_bytes()))
364 Ok(move |path: &HgPath| re.is_match(path.as_bytes()))
365 }
365 }
366
366
367 /// Returns the regex pattern and a function that matches an `HgPath` against
367 /// Returns the regex pattern and a function that matches an `HgPath` against
368 /// said regex formed by the given ignore patterns.
368 /// said regex formed by the given ignore patterns.
369 fn build_regex_match<'a>(
369 fn build_regex_match<'a>(
370 ignore_patterns: &'a [&'a IgnorePattern],
370 ignore_patterns: &'a [&'a IgnorePattern],
371 ) -> PatternResult<(Vec<u8>, Box<dyn Fn(&HgPath) -> bool + Sync>)> {
371 ) -> PatternResult<(Vec<u8>, Box<dyn Fn(&HgPath) -> bool + Sync>)> {
372 let regexps: Result<Vec<_>, PatternError> = ignore_patterns
372 let regexps: Result<Vec<_>, PatternError> = ignore_patterns
373 .into_iter()
373 .into_iter()
374 .map(|k| build_single_regex(*k))
374 .map(|k| build_single_regex(*k))
375 .collect();
375 .collect();
376 let regexps = regexps?;
376 let regexps = regexps?;
377 let full_regex = regexps.join(&b'|');
377 let full_regex = regexps.join(&b'|');
378
378
379 let matcher = re_matcher(&full_regex)?;
379 let matcher = re_matcher(&full_regex)?;
380 let func = Box::new(move |filename: &HgPath| matcher(filename));
380 let func = Box::new(move |filename: &HgPath| matcher(filename));
381
381
382 Ok((full_regex, func))
382 Ok((full_regex, func))
383 }
383 }
384
384
385 /// Returns roots and directories corresponding to each pattern.
385 /// Returns roots and directories corresponding to each pattern.
386 ///
386 ///
387 /// This calculates the roots and directories exactly matching the patterns and
387 /// This calculates the roots and directories exactly matching the patterns and
388 /// returns a tuple of (roots, dirs). It does not return other directories
388 /// returns a tuple of (roots, dirs). It does not return other directories
389 /// which may also need to be considered, like the parent directories.
389 /// which may also need to be considered, like the parent directories.
390 fn roots_and_dirs(
390 fn roots_and_dirs(
391 ignore_patterns: &[IgnorePattern],
391 ignore_patterns: &[IgnorePattern],
392 ) -> (Vec<HgPathBuf>, Vec<HgPathBuf>) {
392 ) -> (Vec<HgPathBuf>, Vec<HgPathBuf>) {
393 let mut roots = Vec::new();
393 let mut roots = Vec::new();
394 let mut dirs = Vec::new();
394 let mut dirs = Vec::new();
395
395
396 for ignore_pattern in ignore_patterns {
396 for ignore_pattern in ignore_patterns {
397 let IgnorePattern {
397 let IgnorePattern {
398 syntax, pattern, ..
398 syntax, pattern, ..
399 } = ignore_pattern;
399 } = ignore_pattern;
400 match syntax {
400 match syntax {
401 PatternSyntax::RootGlob | PatternSyntax::Glob => {
401 PatternSyntax::RootGlob | PatternSyntax::Glob => {
402 let mut root = vec![];
402 let mut root = vec![];
403
403
404 for p in pattern.split(|c| *c == b'/') {
404 for p in pattern.split(|c| *c == b'/') {
405 if p.iter().any(|c| match *c {
405 if p.iter().any(|c| match *c {
406 b'[' | b'{' | b'*' | b'?' => true,
406 b'[' | b'{' | b'*' | b'?' => true,
407 _ => false,
407 _ => false,
408 }) {
408 }) {
409 break;
409 break;
410 }
410 }
411 root.push(HgPathBuf::from_bytes(p));
411 root.push(HgPathBuf::from_bytes(p));
412 }
412 }
413 let buf =
413 let buf =
414 root.iter().fold(HgPathBuf::new(), |acc, r| acc.join(r));
414 root.iter().fold(HgPathBuf::new(), |acc, r| acc.join(r));
415 roots.push(buf);
415 roots.push(buf);
416 }
416 }
417 PatternSyntax::Path | PatternSyntax::RelPath => {
417 PatternSyntax::Path | PatternSyntax::RelPath => {
418 let pat = HgPath::new(if pattern == b"." {
418 let pat = HgPath::new(if pattern == b"." {
419 &[] as &[u8]
419 &[] as &[u8]
420 } else {
420 } else {
421 pattern
421 pattern
422 });
422 });
423 roots.push(pat.to_owned());
423 roots.push(pat.to_owned());
424 }
424 }
425 PatternSyntax::RootFiles => {
425 PatternSyntax::RootFiles => {
426 let pat = if pattern == b"." {
426 let pat = if pattern == b"." {
427 &[] as &[u8]
427 &[] as &[u8]
428 } else {
428 } else {
429 pattern
429 pattern
430 };
430 };
431 dirs.push(HgPathBuf::from_bytes(pat));
431 dirs.push(HgPathBuf::from_bytes(pat));
432 }
432 }
433 _ => {
433 _ => {
434 roots.push(HgPathBuf::new());
434 roots.push(HgPathBuf::new());
435 }
435 }
436 }
436 }
437 }
437 }
438 (roots, dirs)
438 (roots, dirs)
439 }
439 }
440
440
441 /// Paths extracted from patterns
441 /// Paths extracted from patterns
442 #[derive(Debug, PartialEq)]
442 #[derive(Debug, PartialEq)]
443 struct RootsDirsAndParents {
443 struct RootsDirsAndParents {
444 /// Directories to match recursively
444 /// Directories to match recursively
445 pub roots: HashSet<HgPathBuf>,
445 pub roots: HashSet<HgPathBuf>,
446 /// Directories to match non-recursively
446 /// Directories to match non-recursively
447 pub dirs: HashSet<HgPathBuf>,
447 pub dirs: HashSet<HgPathBuf>,
448 /// Implicitly required directories to go to items in either roots or dirs
448 /// Implicitly required directories to go to items in either roots or dirs
449 pub parents: HashSet<HgPathBuf>,
449 pub parents: HashSet<HgPathBuf>,
450 }
450 }
451
451
452 /// Extract roots, dirs and parents from patterns.
452 /// Extract roots, dirs and parents from patterns.
453 fn roots_dirs_and_parents(
453 fn roots_dirs_and_parents(
454 ignore_patterns: &[IgnorePattern],
454 ignore_patterns: &[IgnorePattern],
455 ) -> PatternResult<RootsDirsAndParents> {
455 ) -> PatternResult<RootsDirsAndParents> {
456 let (roots, dirs) = roots_and_dirs(ignore_patterns);
456 let (roots, dirs) = roots_and_dirs(ignore_patterns);
457
457
458 let mut parents = HashSet::new();
458 let mut parents = HashSet::new();
459
459
460 parents.extend(
460 parents.extend(
461 DirsMultiset::from_manifest(&dirs)
461 DirsMultiset::from_manifest(&dirs)
462 .map_err(|e| match e {
462 .map_err(|e| match e {
463 DirstateMapError::InvalidPath(e) => e,
463 DirstateMapError::InvalidPath(e) => e,
464 _ => unreachable!(),
464 _ => unreachable!(),
465 })?
465 })?
466 .iter()
466 .iter()
467 .map(|k| k.to_owned()),
467 .map(|k| k.to_owned()),
468 );
468 );
469 parents.extend(
469 parents.extend(
470 DirsMultiset::from_manifest(&roots)
470 DirsMultiset::from_manifest(&roots)
471 .map_err(|e| match e {
471 .map_err(|e| match e {
472 DirstateMapError::InvalidPath(e) => e,
472 DirstateMapError::InvalidPath(e) => e,
473 _ => unreachable!(),
473 _ => unreachable!(),
474 })?
474 })?
475 .iter()
475 .iter()
476 .map(|k| k.to_owned()),
476 .map(|k| k.to_owned()),
477 );
477 );
478
478
479 Ok(RootsDirsAndParents {
479 Ok(RootsDirsAndParents {
480 roots: HashSet::from_iter(roots),
480 roots: HashSet::from_iter(roots),
481 dirs: HashSet::from_iter(dirs),
481 dirs: HashSet::from_iter(dirs),
482 parents,
482 parents,
483 })
483 })
484 }
484 }
485
485
486 /// Returns a function that checks whether a given file (in the general sense)
486 /// Returns a function that checks whether a given file (in the general sense)
487 /// should be matched.
487 /// should be matched.
488 fn build_match<'a, 'b>(
488 fn build_match<'a, 'b>(
489 ignore_patterns: &'a [IgnorePattern],
489 ignore_patterns: &'a [IgnorePattern],
490 root_dir: impl AsRef<Path>,
490 root_dir: impl AsRef<Path>,
491 ) -> PatternResult<(
491 ) -> PatternResult<(
492 Vec<u8>,
492 Vec<u8>,
493 Box<dyn Fn(&HgPath) -> bool + 'b + Sync>,
493 Box<dyn Fn(&HgPath) -> bool + 'b + Sync>,
494 Vec<PatternFileWarning>,
494 Vec<PatternFileWarning>,
495 )> {
495 )> {
496 let mut match_funcs: Vec<Box<dyn Fn(&HgPath) -> bool + Sync>> = vec![];
496 let mut match_funcs: Vec<Box<dyn Fn(&HgPath) -> bool + Sync>> = vec![];
497 // For debugging and printing
497 // For debugging and printing
498 let mut patterns = vec![];
498 let mut patterns = vec![];
499 let mut all_warnings = vec![];
499 let mut all_warnings = vec![];
500
500
501 let (subincludes, ignore_patterns) =
501 let (subincludes, ignore_patterns) =
502 filter_subincludes(ignore_patterns, root_dir)?;
502 filter_subincludes(ignore_patterns, root_dir)?;
503
503
504 if !subincludes.is_empty() {
504 if !subincludes.is_empty() {
505 // Build prefix-based matcher functions for subincludes
505 // Build prefix-based matcher functions for subincludes
506 let mut submatchers = FastHashMap::default();
506 let mut submatchers = FastHashMap::default();
507 let mut prefixes = vec![];
507 let mut prefixes = vec![];
508
508
509 for SubInclude { prefix, root, path } in subincludes.into_iter() {
509 for SubInclude { prefix, root, path } in subincludes.into_iter() {
510 let (match_fn, warnings) = get_ignore_function(&[path], root)?;
510 let (match_fn, warnings) =
511 get_ignore_function(vec![path.to_path_buf()], root)?;
511 all_warnings.extend(warnings);
512 all_warnings.extend(warnings);
512 prefixes.push(prefix.to_owned());
513 prefixes.push(prefix.to_owned());
513 submatchers.insert(prefix.to_owned(), match_fn);
514 submatchers.insert(prefix.to_owned(), match_fn);
514 }
515 }
515
516
516 let match_subinclude = move |filename: &HgPath| {
517 let match_subinclude = move |filename: &HgPath| {
517 for prefix in prefixes.iter() {
518 for prefix in prefixes.iter() {
518 if let Some(rel) = filename.relative_to(prefix) {
519 if let Some(rel) = filename.relative_to(prefix) {
519 if (submatchers.get(prefix).unwrap())(rel) {
520 if (submatchers.get(prefix).unwrap())(rel) {
520 return true;
521 return true;
521 }
522 }
522 }
523 }
523 }
524 }
524 false
525 false
525 };
526 };
526
527
527 match_funcs.push(Box::new(match_subinclude));
528 match_funcs.push(Box::new(match_subinclude));
528 }
529 }
529
530
530 if !ignore_patterns.is_empty() {
531 if !ignore_patterns.is_empty() {
531 // Either do dumb matching if all patterns are rootfiles, or match
532 // Either do dumb matching if all patterns are rootfiles, or match
532 // with a regex.
533 // with a regex.
533 if ignore_patterns
534 if ignore_patterns
534 .iter()
535 .iter()
535 .all(|k| k.syntax == PatternSyntax::RootFiles)
536 .all(|k| k.syntax == PatternSyntax::RootFiles)
536 {
537 {
537 let dirs: HashSet<_> = ignore_patterns
538 let dirs: HashSet<_> = ignore_patterns
538 .iter()
539 .iter()
539 .map(|k| k.pattern.to_owned())
540 .map(|k| k.pattern.to_owned())
540 .collect();
541 .collect();
541 let mut dirs_vec: Vec<_> = dirs.iter().cloned().collect();
542 let mut dirs_vec: Vec<_> = dirs.iter().cloned().collect();
542
543
543 let match_func = move |path: &HgPath| -> bool {
544 let match_func = move |path: &HgPath| -> bool {
544 let path = path.as_bytes();
545 let path = path.as_bytes();
545 let i = path.iter().rfind(|a| **a == b'/');
546 let i = path.iter().rfind(|a| **a == b'/');
546 let dir = if let Some(i) = i {
547 let dir = if let Some(i) = i {
547 &path[..*i as usize]
548 &path[..*i as usize]
548 } else {
549 } else {
549 b"."
550 b"."
550 };
551 };
551 dirs.contains(dir.deref())
552 dirs.contains(dir.deref())
552 };
553 };
553 match_funcs.push(Box::new(match_func));
554 match_funcs.push(Box::new(match_func));
554
555
555 patterns.extend(b"rootfilesin: ");
556 patterns.extend(b"rootfilesin: ");
556 dirs_vec.sort();
557 dirs_vec.sort();
557 patterns.extend(dirs_vec.escaped_bytes());
558 patterns.extend(dirs_vec.escaped_bytes());
558 } else {
559 } else {
559 let (new_re, match_func) = build_regex_match(&ignore_patterns)?;
560 let (new_re, match_func) = build_regex_match(&ignore_patterns)?;
560 patterns = new_re;
561 patterns = new_re;
561 match_funcs.push(match_func)
562 match_funcs.push(match_func)
562 }
563 }
563 }
564 }
564
565
565 Ok(if match_funcs.len() == 1 {
566 Ok(if match_funcs.len() == 1 {
566 (patterns, match_funcs.remove(0), all_warnings)
567 (patterns, match_funcs.remove(0), all_warnings)
567 } else {
568 } else {
568 (
569 (
569 patterns,
570 patterns,
570 Box::new(move |f: &HgPath| -> bool {
571 Box::new(move |f: &HgPath| -> bool {
571 match_funcs.iter().any(|match_func| match_func(f))
572 match_funcs.iter().any(|match_func| match_func(f))
572 }),
573 }),
573 all_warnings,
574 all_warnings,
574 )
575 )
575 })
576 })
576 }
577 }
577
578
578 /// Parses all "ignore" files with their recursive includes and returns a
579 /// Parses all "ignore" files with their recursive includes and returns a
579 /// function that checks whether a given file (in the general sense) should be
580 /// function that checks whether a given file (in the general sense) should be
580 /// ignored.
581 /// ignored.
581 #[timed]
582 pub fn get_ignore_function<'a>(
582 pub fn get_ignore_function<'a>(
583 all_pattern_files: &[impl AsRef<Path>],
583 all_pattern_files: Vec<PathBuf>,
584 root_dir: impl AsRef<Path>,
584 root_dir: impl AsRef<Path>,
585 ) -> PatternResult<(
585 ) -> PatternResult<(
586 impl for<'r> Fn(&'r HgPath) -> bool + Sync,
586 Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>,
587 Vec<PatternFileWarning>,
587 Vec<PatternFileWarning>,
588 )> {
588 )> {
589 let mut all_patterns = vec![];
589 let mut all_patterns = vec![];
590 let mut all_warnings = vec![];
590 let mut all_warnings = vec![];
591
591
592 for pattern_file in all_pattern_files.into_iter() {
592 for pattern_file in all_pattern_files.into_iter() {
593 let (patterns, warnings) =
593 let (patterns, warnings) =
594 get_patterns_from_file(pattern_file, &root_dir)?;
594 get_patterns_from_file(pattern_file, &root_dir)?;
595
595
596 all_patterns.extend(patterns);
596 all_patterns.extend(patterns.to_owned());
597 all_warnings.extend(warnings);
597 all_warnings.extend(warnings);
598 }
598 }
599 let (matcher, warnings) = IncludeMatcher::new(all_patterns, root_dir)?;
599 let (matcher, warnings) = IncludeMatcher::new(all_patterns, root_dir)?;
600 all_warnings.extend(warnings);
600 all_warnings.extend(warnings);
601 Ok((move |path: &HgPath| matcher.matches(path), all_warnings))
601 Ok((
602 Box::new(move |path: &HgPath| matcher.matches(path)),
603 all_warnings,
604 ))
602 }
605 }
603
606
604 impl<'a> IncludeMatcher<'a> {
607 impl<'a> IncludeMatcher<'a> {
605 pub fn new(
608 pub fn new(
606 ignore_patterns: Vec<IgnorePattern>,
609 ignore_patterns: Vec<IgnorePattern>,
607 root_dir: impl AsRef<Path>,
610 root_dir: impl AsRef<Path>,
608 ) -> PatternResult<(Self, Vec<PatternFileWarning>)> {
611 ) -> PatternResult<(Self, Vec<PatternFileWarning>)> {
609 let (patterns, match_fn, warnings) =
612 let (patterns, match_fn, warnings) =
610 build_match(&ignore_patterns, root_dir)?;
613 build_match(&ignore_patterns, root_dir)?;
611 let RootsDirsAndParents {
614 let RootsDirsAndParents {
612 roots,
615 roots,
613 dirs,
616 dirs,
614 parents,
617 parents,
615 } = roots_dirs_and_parents(&ignore_patterns)?;
618 } = roots_dirs_and_parents(&ignore_patterns)?;
616
619
617 let prefix = ignore_patterns.iter().any(|k| match k.syntax {
620 let prefix = ignore_patterns.iter().any(|k| match k.syntax {
618 PatternSyntax::Path | PatternSyntax::RelPath => true,
621 PatternSyntax::Path | PatternSyntax::RelPath => true,
619 _ => false,
622 _ => false,
620 });
623 });
621
624
622 Ok((
625 Ok((
623 Self {
626 Self {
624 patterns,
627 patterns,
625 match_fn,
628 match_fn,
626 prefix,
629 prefix,
627 roots,
630 roots,
628 dirs,
631 dirs,
629 parents,
632 parents,
630 },
633 },
631 warnings,
634 warnings,
632 ))
635 ))
633 }
636 }
634
637
635 fn get_all_parents_children(&self) -> DirsChildrenMultiset {
638 fn get_all_parents_children(&self) -> DirsChildrenMultiset {
636 // TODO cache
639 // TODO cache
637 let thing = self
640 let thing = self
638 .dirs
641 .dirs
639 .iter()
642 .iter()
640 .chain(self.roots.iter())
643 .chain(self.roots.iter())
641 .chain(self.parents.iter());
644 .chain(self.parents.iter());
642 DirsChildrenMultiset::new(thing, Some(&self.parents))
645 DirsChildrenMultiset::new(thing, Some(&self.parents))
643 }
646 }
644 }
647 }
645
648
646 impl<'a> Display for IncludeMatcher<'a> {
649 impl<'a> Display for IncludeMatcher<'a> {
647 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
650 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
648 write!(
651 write!(
649 f,
652 f,
650 "IncludeMatcher(includes='{}')",
653 "IncludeMatcher(includes='{}')",
651 String::from_utf8_lossy(&self.patterns.escaped_bytes())
654 String::from_utf8_lossy(&self.patterns.escaped_bytes())
652 )
655 )
653 }
656 }
654 }
657 }
655
658
656 #[cfg(test)]
659 #[cfg(test)]
657 mod tests {
660 mod tests {
658 use super::*;
661 use super::*;
659 use pretty_assertions::assert_eq;
662 use pretty_assertions::assert_eq;
660 use std::path::Path;
663 use std::path::Path;
661
664
662 #[test]
665 #[test]
663 fn test_roots_and_dirs() {
666 fn test_roots_and_dirs() {
664 let pats = vec![
667 let pats = vec![
665 IgnorePattern::new(PatternSyntax::Glob, b"g/h/*", Path::new("")),
668 IgnorePattern::new(PatternSyntax::Glob, b"g/h/*", Path::new("")),
666 IgnorePattern::new(PatternSyntax::Glob, b"g/h", Path::new("")),
669 IgnorePattern::new(PatternSyntax::Glob, b"g/h", Path::new("")),
667 IgnorePattern::new(PatternSyntax::Glob, b"g*", Path::new("")),
670 IgnorePattern::new(PatternSyntax::Glob, b"g*", Path::new("")),
668 ];
671 ];
669 let (roots, dirs) = roots_and_dirs(&pats);
672 let (roots, dirs) = roots_and_dirs(&pats);
670
673
671 assert_eq!(
674 assert_eq!(
672 roots,
675 roots,
673 vec!(
676 vec!(
674 HgPathBuf::from_bytes(b"g/h"),
677 HgPathBuf::from_bytes(b"g/h"),
675 HgPathBuf::from_bytes(b"g/h"),
678 HgPathBuf::from_bytes(b"g/h"),
676 HgPathBuf::new()
679 HgPathBuf::new()
677 ),
680 ),
678 );
681 );
679 assert_eq!(dirs, vec!());
682 assert_eq!(dirs, vec!());
680 }
683 }
681
684
682 #[test]
685 #[test]
683 fn test_roots_dirs_and_parents() {
686 fn test_roots_dirs_and_parents() {
684 let pats = vec![
687 let pats = vec![
685 IgnorePattern::new(PatternSyntax::Glob, b"g/h/*", Path::new("")),
688 IgnorePattern::new(PatternSyntax::Glob, b"g/h/*", Path::new("")),
686 IgnorePattern::new(PatternSyntax::Glob, b"g/h", Path::new("")),
689 IgnorePattern::new(PatternSyntax::Glob, b"g/h", Path::new("")),
687 IgnorePattern::new(PatternSyntax::Glob, b"g*", Path::new("")),
690 IgnorePattern::new(PatternSyntax::Glob, b"g*", Path::new("")),
688 ];
691 ];
689
692
690 let mut roots = HashSet::new();
693 let mut roots = HashSet::new();
691 roots.insert(HgPathBuf::from_bytes(b"g/h"));
694 roots.insert(HgPathBuf::from_bytes(b"g/h"));
692 roots.insert(HgPathBuf::new());
695 roots.insert(HgPathBuf::new());
693
696
694 let dirs = HashSet::new();
697 let dirs = HashSet::new();
695
698
696 let mut parents = HashSet::new();
699 let mut parents = HashSet::new();
697 parents.insert(HgPathBuf::new());
700 parents.insert(HgPathBuf::new());
698 parents.insert(HgPathBuf::from_bytes(b"g"));
701 parents.insert(HgPathBuf::from_bytes(b"g"));
699
702
700 assert_eq!(
703 assert_eq!(
701 roots_dirs_and_parents(&pats).unwrap(),
704 roots_dirs_and_parents(&pats).unwrap(),
702 RootsDirsAndParents {
705 RootsDirsAndParents {
703 roots,
706 roots,
704 dirs,
707 dirs,
705 parents
708 parents
706 }
709 }
707 );
710 );
708 }
711 }
709
712
710 #[test]
713 #[test]
711 fn test_filematcher_visit_children_set() {
714 fn test_filematcher_visit_children_set() {
712 // Visitchildrenset
715 // Visitchildrenset
713 let files = vec![HgPath::new(b"dir/subdir/foo.txt")];
716 let files = vec![HgPath::new(b"dir/subdir/foo.txt")];
714 let matcher = FileMatcher::new(&files).unwrap();
717 let matcher = FileMatcher::new(&files).unwrap();
715
718
716 let mut set = HashSet::new();
719 let mut set = HashSet::new();
717 set.insert(HgPath::new(b"dir"));
720 set.insert(HgPath::new(b"dir"));
718 assert_eq!(
721 assert_eq!(
719 matcher.visit_children_set(HgPath::new(b"")),
722 matcher.visit_children_set(HgPath::new(b"")),
720 VisitChildrenSet::Set(set)
723 VisitChildrenSet::Set(set)
721 );
724 );
722
725
723 let mut set = HashSet::new();
726 let mut set = HashSet::new();
724 set.insert(HgPath::new(b"subdir"));
727 set.insert(HgPath::new(b"subdir"));
725 assert_eq!(
728 assert_eq!(
726 matcher.visit_children_set(HgPath::new(b"dir")),
729 matcher.visit_children_set(HgPath::new(b"dir")),
727 VisitChildrenSet::Set(set)
730 VisitChildrenSet::Set(set)
728 );
731 );
729
732
730 let mut set = HashSet::new();
733 let mut set = HashSet::new();
731 set.insert(HgPath::new(b"foo.txt"));
734 set.insert(HgPath::new(b"foo.txt"));
732 assert_eq!(
735 assert_eq!(
733 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
736 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
734 VisitChildrenSet::Set(set)
737 VisitChildrenSet::Set(set)
735 );
738 );
736
739
737 assert_eq!(
740 assert_eq!(
738 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
741 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
739 VisitChildrenSet::Empty
742 VisitChildrenSet::Empty
740 );
743 );
741 assert_eq!(
744 assert_eq!(
742 matcher.visit_children_set(HgPath::new(b"dir/subdir/foo.txt")),
745 matcher.visit_children_set(HgPath::new(b"dir/subdir/foo.txt")),
743 VisitChildrenSet::Empty
746 VisitChildrenSet::Empty
744 );
747 );
745 assert_eq!(
748 assert_eq!(
746 matcher.visit_children_set(HgPath::new(b"folder")),
749 matcher.visit_children_set(HgPath::new(b"folder")),
747 VisitChildrenSet::Empty
750 VisitChildrenSet::Empty
748 );
751 );
749 }
752 }
750
753
751 #[test]
754 #[test]
752 fn test_filematcher_visit_children_set_files_and_dirs() {
755 fn test_filematcher_visit_children_set_files_and_dirs() {
753 let files = vec![
756 let files = vec![
754 HgPath::new(b"rootfile.txt"),
757 HgPath::new(b"rootfile.txt"),
755 HgPath::new(b"a/file1.txt"),
758 HgPath::new(b"a/file1.txt"),
756 HgPath::new(b"a/b/file2.txt"),
759 HgPath::new(b"a/b/file2.txt"),
757 // No file in a/b/c
760 // No file in a/b/c
758 HgPath::new(b"a/b/c/d/file4.txt"),
761 HgPath::new(b"a/b/c/d/file4.txt"),
759 ];
762 ];
760 let matcher = FileMatcher::new(&files).unwrap();
763 let matcher = FileMatcher::new(&files).unwrap();
761
764
762 let mut set = HashSet::new();
765 let mut set = HashSet::new();
763 set.insert(HgPath::new(b"a"));
766 set.insert(HgPath::new(b"a"));
764 set.insert(HgPath::new(b"rootfile.txt"));
767 set.insert(HgPath::new(b"rootfile.txt"));
765 assert_eq!(
768 assert_eq!(
766 matcher.visit_children_set(HgPath::new(b"")),
769 matcher.visit_children_set(HgPath::new(b"")),
767 VisitChildrenSet::Set(set)
770 VisitChildrenSet::Set(set)
768 );
771 );
769
772
770 let mut set = HashSet::new();
773 let mut set = HashSet::new();
771 set.insert(HgPath::new(b"b"));
774 set.insert(HgPath::new(b"b"));
772 set.insert(HgPath::new(b"file1.txt"));
775 set.insert(HgPath::new(b"file1.txt"));
773 assert_eq!(
776 assert_eq!(
774 matcher.visit_children_set(HgPath::new(b"a")),
777 matcher.visit_children_set(HgPath::new(b"a")),
775 VisitChildrenSet::Set(set)
778 VisitChildrenSet::Set(set)
776 );
779 );
777
780
778 let mut set = HashSet::new();
781 let mut set = HashSet::new();
779 set.insert(HgPath::new(b"c"));
782 set.insert(HgPath::new(b"c"));
780 set.insert(HgPath::new(b"file2.txt"));
783 set.insert(HgPath::new(b"file2.txt"));
781 assert_eq!(
784 assert_eq!(
782 matcher.visit_children_set(HgPath::new(b"a/b")),
785 matcher.visit_children_set(HgPath::new(b"a/b")),
783 VisitChildrenSet::Set(set)
786 VisitChildrenSet::Set(set)
784 );
787 );
785
788
786 let mut set = HashSet::new();
789 let mut set = HashSet::new();
787 set.insert(HgPath::new(b"d"));
790 set.insert(HgPath::new(b"d"));
788 assert_eq!(
791 assert_eq!(
789 matcher.visit_children_set(HgPath::new(b"a/b/c")),
792 matcher.visit_children_set(HgPath::new(b"a/b/c")),
790 VisitChildrenSet::Set(set)
793 VisitChildrenSet::Set(set)
791 );
794 );
792 let mut set = HashSet::new();
795 let mut set = HashSet::new();
793 set.insert(HgPath::new(b"file4.txt"));
796 set.insert(HgPath::new(b"file4.txt"));
794 assert_eq!(
797 assert_eq!(
795 matcher.visit_children_set(HgPath::new(b"a/b/c/d")),
798 matcher.visit_children_set(HgPath::new(b"a/b/c/d")),
796 VisitChildrenSet::Set(set)
799 VisitChildrenSet::Set(set)
797 );
800 );
798
801
799 assert_eq!(
802 assert_eq!(
800 matcher.visit_children_set(HgPath::new(b"a/b/c/d/e")),
803 matcher.visit_children_set(HgPath::new(b"a/b/c/d/e")),
801 VisitChildrenSet::Empty
804 VisitChildrenSet::Empty
802 );
805 );
803 assert_eq!(
806 assert_eq!(
804 matcher.visit_children_set(HgPath::new(b"folder")),
807 matcher.visit_children_set(HgPath::new(b"folder")),
805 VisitChildrenSet::Empty
808 VisitChildrenSet::Empty
806 );
809 );
807 }
810 }
808
811
809 #[cfg(feature = "with-re2")]
812 #[cfg(feature = "with-re2")]
810 #[test]
813 #[test]
811 fn test_includematcher() {
814 fn test_includematcher() {
812 // VisitchildrensetPrefix
815 // VisitchildrensetPrefix
813 let (matcher, _) = IncludeMatcher::new(
816 let (matcher, _) = IncludeMatcher::new(
814 vec![IgnorePattern::new(
817 vec![IgnorePattern::new(
815 PatternSyntax::RelPath,
818 PatternSyntax::RelPath,
816 b"dir/subdir",
819 b"dir/subdir",
817 Path::new(""),
820 Path::new(""),
818 )],
821 )],
819 "",
822 "",
820 )
823 )
821 .unwrap();
824 .unwrap();
822
825
823 let mut set = HashSet::new();
826 let mut set = HashSet::new();
824 set.insert(HgPath::new(b"dir"));
827 set.insert(HgPath::new(b"dir"));
825 assert_eq!(
828 assert_eq!(
826 matcher.visit_children_set(HgPath::new(b"")),
829 matcher.visit_children_set(HgPath::new(b"")),
827 VisitChildrenSet::Set(set)
830 VisitChildrenSet::Set(set)
828 );
831 );
829
832
830 let mut set = HashSet::new();
833 let mut set = HashSet::new();
831 set.insert(HgPath::new(b"subdir"));
834 set.insert(HgPath::new(b"subdir"));
832 assert_eq!(
835 assert_eq!(
833 matcher.visit_children_set(HgPath::new(b"dir")),
836 matcher.visit_children_set(HgPath::new(b"dir")),
834 VisitChildrenSet::Set(set)
837 VisitChildrenSet::Set(set)
835 );
838 );
836 assert_eq!(
839 assert_eq!(
837 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
840 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
838 VisitChildrenSet::Recursive
841 VisitChildrenSet::Recursive
839 );
842 );
840 // OPT: This should probably be 'all' if its parent is?
843 // OPT: This should probably be 'all' if its parent is?
841 assert_eq!(
844 assert_eq!(
842 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
845 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
843 VisitChildrenSet::This
846 VisitChildrenSet::This
844 );
847 );
845 assert_eq!(
848 assert_eq!(
846 matcher.visit_children_set(HgPath::new(b"folder")),
849 matcher.visit_children_set(HgPath::new(b"folder")),
847 VisitChildrenSet::Empty
850 VisitChildrenSet::Empty
848 );
851 );
849
852
850 // VisitchildrensetRootfilesin
853 // VisitchildrensetRootfilesin
851 let (matcher, _) = IncludeMatcher::new(
854 let (matcher, _) = IncludeMatcher::new(
852 vec![IgnorePattern::new(
855 vec![IgnorePattern::new(
853 PatternSyntax::RootFiles,
856 PatternSyntax::RootFiles,
854 b"dir/subdir",
857 b"dir/subdir",
855 Path::new(""),
858 Path::new(""),
856 )],
859 )],
857 "",
860 "",
858 )
861 )
859 .unwrap();
862 .unwrap();
860
863
861 let mut set = HashSet::new();
864 let mut set = HashSet::new();
862 set.insert(HgPath::new(b"dir"));
865 set.insert(HgPath::new(b"dir"));
863 assert_eq!(
866 assert_eq!(
864 matcher.visit_children_set(HgPath::new(b"")),
867 matcher.visit_children_set(HgPath::new(b"")),
865 VisitChildrenSet::Set(set)
868 VisitChildrenSet::Set(set)
866 );
869 );
867
870
868 let mut set = HashSet::new();
871 let mut set = HashSet::new();
869 set.insert(HgPath::new(b"subdir"));
872 set.insert(HgPath::new(b"subdir"));
870 assert_eq!(
873 assert_eq!(
871 matcher.visit_children_set(HgPath::new(b"dir")),
874 matcher.visit_children_set(HgPath::new(b"dir")),
872 VisitChildrenSet::Set(set)
875 VisitChildrenSet::Set(set)
873 );
876 );
874
877
875 assert_eq!(
878 assert_eq!(
876 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
879 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
877 VisitChildrenSet::This
880 VisitChildrenSet::This
878 );
881 );
879 assert_eq!(
882 assert_eq!(
880 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
883 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
881 VisitChildrenSet::Empty
884 VisitChildrenSet::Empty
882 );
885 );
883 assert_eq!(
886 assert_eq!(
884 matcher.visit_children_set(HgPath::new(b"folder")),
887 matcher.visit_children_set(HgPath::new(b"folder")),
885 VisitChildrenSet::Empty
888 VisitChildrenSet::Empty
886 );
889 );
887
890
888 // VisitchildrensetGlob
891 // VisitchildrensetGlob
889 let (matcher, _) = IncludeMatcher::new(
892 let (matcher, _) = IncludeMatcher::new(
890 vec![IgnorePattern::new(
893 vec![IgnorePattern::new(
891 PatternSyntax::Glob,
894 PatternSyntax::Glob,
892 b"dir/z*",
895 b"dir/z*",
893 Path::new(""),
896 Path::new(""),
894 )],
897 )],
895 "",
898 "",
896 )
899 )
897 .unwrap();
900 .unwrap();
898
901
899 let mut set = HashSet::new();
902 let mut set = HashSet::new();
900 set.insert(HgPath::new(b"dir"));
903 set.insert(HgPath::new(b"dir"));
901 assert_eq!(
904 assert_eq!(
902 matcher.visit_children_set(HgPath::new(b"")),
905 matcher.visit_children_set(HgPath::new(b"")),
903 VisitChildrenSet::Set(set)
906 VisitChildrenSet::Set(set)
904 );
907 );
905 assert_eq!(
908 assert_eq!(
906 matcher.visit_children_set(HgPath::new(b"folder")),
909 matcher.visit_children_set(HgPath::new(b"folder")),
907 VisitChildrenSet::Empty
910 VisitChildrenSet::Empty
908 );
911 );
909 assert_eq!(
912 assert_eq!(
910 matcher.visit_children_set(HgPath::new(b"dir")),
913 matcher.visit_children_set(HgPath::new(b"dir")),
911 VisitChildrenSet::This
914 VisitChildrenSet::This
912 );
915 );
913 // OPT: these should probably be set().
916 // OPT: these should probably be set().
914 assert_eq!(
917 assert_eq!(
915 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
918 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
916 VisitChildrenSet::This
919 VisitChildrenSet::This
917 );
920 );
918 assert_eq!(
921 assert_eq!(
919 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
922 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
920 VisitChildrenSet::This
923 VisitChildrenSet::This
921 );
924 );
922 }
925 }
923 }
926 }
@@ -1,297 +1,297 b''
1 // status.rs
1 // status.rs
2 //
2 //
3 // Copyright 2019, Raphaël Gomès <rgomes@octobus.net>
3 // Copyright 2019, Raphaël Gomès <rgomes@octobus.net>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
6 // GNU General Public License version 2 or any later version.
7
7
8 //! Bindings for the `hg::status` module provided by the
8 //! Bindings for the `hg::status` module provided by the
9 //! `hg-core` crate. From Python, this will be seen as
9 //! `hg-core` crate. From Python, this will be seen as
10 //! `rustext.dirstate.status`.
10 //! `rustext.dirstate.status`.
11
11
12 use crate::{dirstate::DirstateMap, exceptions::FallbackError};
12 use crate::{dirstate::DirstateMap, exceptions::FallbackError};
13 use cpython::{
13 use cpython::{
14 exc::ValueError, ObjectProtocol, PyBytes, PyErr, PyList, PyObject,
14 exc::ValueError, ObjectProtocol, PyBytes, PyErr, PyList, PyObject,
15 PyResult, PyTuple, Python, PythonObject, ToPyObject,
15 PyResult, PyTuple, Python, PythonObject, ToPyObject,
16 };
16 };
17 use hg::{
17 use hg::{
18 matchers::{AlwaysMatcher, FileMatcher, IncludeMatcher},
18 matchers::{AlwaysMatcher, FileMatcher, IncludeMatcher},
19 parse_pattern_syntax, status,
19 parse_pattern_syntax, status,
20 utils::{
20 utils::{
21 files::{get_bytes_from_path, get_path_from_bytes},
21 files::{get_bytes_from_path, get_path_from_bytes},
22 hg_path::{HgPath, HgPathBuf},
22 hg_path::{HgPath, HgPathBuf},
23 },
23 },
24 BadMatch, DirstateStatus, IgnorePattern, PatternFileWarning, StatusError,
24 BadMatch, DirstateStatus, IgnorePattern, PatternFileWarning, StatusError,
25 StatusOptions,
25 StatusOptions,
26 };
26 };
27 use std::borrow::{Borrow, Cow};
27 use std::borrow::{Borrow, Cow};
28
28
29 /// This will be useless once trait impls for collection are added to `PyBytes`
29 /// This will be useless once trait impls for collection are added to `PyBytes`
30 /// upstream.
30 /// upstream.
31 fn collect_pybytes_list(
31 fn collect_pybytes_list(
32 py: Python,
32 py: Python,
33 collection: &[impl AsRef<HgPath>],
33 collection: &[impl AsRef<HgPath>],
34 ) -> PyList {
34 ) -> PyList {
35 let list = PyList::new(py, &[]);
35 let list = PyList::new(py, &[]);
36
36
37 for path in collection.iter() {
37 for path in collection.iter() {
38 list.append(
38 list.append(
39 py,
39 py,
40 PyBytes::new(py, path.as_ref().as_bytes()).into_object(),
40 PyBytes::new(py, path.as_ref().as_bytes()).into_object(),
41 )
41 )
42 }
42 }
43
43
44 list
44 list
45 }
45 }
46
46
47 fn collect_bad_matches(
47 fn collect_bad_matches(
48 py: Python,
48 py: Python,
49 collection: &[(impl AsRef<HgPath>, BadMatch)],
49 collection: &[(impl AsRef<HgPath>, BadMatch)],
50 ) -> PyResult<PyList> {
50 ) -> PyResult<PyList> {
51 let list = PyList::new(py, &[]);
51 let list = PyList::new(py, &[]);
52
52
53 let os = py.import("os")?;
53 let os = py.import("os")?;
54 let get_error_message = |code: i32| -> PyResult<_> {
54 let get_error_message = |code: i32| -> PyResult<_> {
55 os.call(
55 os.call(
56 py,
56 py,
57 "strerror",
57 "strerror",
58 PyTuple::new(py, &[code.to_py_object(py).into_object()]),
58 PyTuple::new(py, &[code.to_py_object(py).into_object()]),
59 None,
59 None,
60 )
60 )
61 };
61 };
62
62
63 for (path, bad_match) in collection.iter() {
63 for (path, bad_match) in collection.iter() {
64 let message = match bad_match {
64 let message = match bad_match {
65 BadMatch::OsError(code) => get_error_message(*code)?,
65 BadMatch::OsError(code) => get_error_message(*code)?,
66 BadMatch::BadType(bad_type) => format!(
66 BadMatch::BadType(bad_type) => format!(
67 "unsupported file type (type is {})",
67 "unsupported file type (type is {})",
68 bad_type.to_string()
68 bad_type.to_string()
69 )
69 )
70 .to_py_object(py)
70 .to_py_object(py)
71 .into_object(),
71 .into_object(),
72 };
72 };
73 list.append(
73 list.append(
74 py,
74 py,
75 (PyBytes::new(py, path.as_ref().as_bytes()), message)
75 (PyBytes::new(py, path.as_ref().as_bytes()), message)
76 .to_py_object(py)
76 .to_py_object(py)
77 .into_object(),
77 .into_object(),
78 )
78 )
79 }
79 }
80
80
81 Ok(list)
81 Ok(list)
82 }
82 }
83
83
84 fn handle_fallback(py: Python, err: StatusError) -> PyErr {
84 fn handle_fallback(py: Python, err: StatusError) -> PyErr {
85 match err {
85 match err {
86 StatusError::Pattern(e) => {
86 StatusError::Pattern(e) => {
87 let as_string = e.to_string();
87 let as_string = e.to_string();
88 log::trace!("Rust status fallback: `{}`", &as_string);
88 log::trace!("Rust status fallback: `{}`", &as_string);
89
89
90 PyErr::new::<FallbackError, _>(py, &as_string)
90 PyErr::new::<FallbackError, _>(py, &as_string)
91 }
91 }
92 e => PyErr::new::<ValueError, _>(py, e.to_string()),
92 e => PyErr::new::<ValueError, _>(py, e.to_string()),
93 }
93 }
94 }
94 }
95
95
96 pub fn status_wrapper(
96 pub fn status_wrapper(
97 py: Python,
97 py: Python,
98 dmap: DirstateMap,
98 dmap: DirstateMap,
99 matcher: PyObject,
99 matcher: PyObject,
100 root_dir: PyObject,
100 root_dir: PyObject,
101 ignore_files: PyList,
101 ignore_files: PyList,
102 check_exec: bool,
102 check_exec: bool,
103 last_normal_time: i64,
103 last_normal_time: i64,
104 list_clean: bool,
104 list_clean: bool,
105 list_ignored: bool,
105 list_ignored: bool,
106 list_unknown: bool,
106 list_unknown: bool,
107 ) -> PyResult<PyTuple> {
107 ) -> PyResult<PyTuple> {
108 let bytes = root_dir.extract::<PyBytes>(py)?;
108 let bytes = root_dir.extract::<PyBytes>(py)?;
109 let root_dir = get_path_from_bytes(bytes.data(py));
109 let root_dir = get_path_from_bytes(bytes.data(py));
110
110
111 let dmap: DirstateMap = dmap.to_py_object(py);
111 let dmap: DirstateMap = dmap.to_py_object(py);
112 let dmap = dmap.get_inner(py);
112 let dmap = dmap.get_inner(py);
113
113
114 let ignore_files: PyResult<Vec<_>> = ignore_files
114 let ignore_files: PyResult<Vec<_>> = ignore_files
115 .iter(py)
115 .iter(py)
116 .map(|b| {
116 .map(|b| {
117 let file = b.extract::<PyBytes>(py)?;
117 let file = b.extract::<PyBytes>(py)?;
118 Ok(get_path_from_bytes(file.data(py)).to_owned())
118 Ok(get_path_from_bytes(file.data(py)).to_owned())
119 })
119 })
120 .collect();
120 .collect();
121 let ignore_files = ignore_files?;
121 let ignore_files = ignore_files?;
122
122
123 match matcher.get_type(py).name(py).borrow() {
123 match matcher.get_type(py).name(py).borrow() {
124 "alwaysmatcher" => {
124 "alwaysmatcher" => {
125 let matcher = AlwaysMatcher;
125 let matcher = AlwaysMatcher;
126 let ((lookup, status_res), warnings) = status(
126 let ((lookup, status_res), warnings) = status(
127 &dmap,
127 &dmap,
128 &matcher,
128 &matcher,
129 &root_dir,
129 &root_dir,
130 &ignore_files,
130 ignore_files,
131 StatusOptions {
131 StatusOptions {
132 check_exec,
132 check_exec,
133 last_normal_time,
133 last_normal_time,
134 list_clean,
134 list_clean,
135 list_ignored,
135 list_ignored,
136 list_unknown,
136 list_unknown,
137 },
137 },
138 )
138 )
139 .map_err(|e| handle_fallback(py, e))?;
139 .map_err(|e| handle_fallback(py, e))?;
140 build_response(py, lookup, status_res, warnings)
140 build_response(py, lookup, status_res, warnings)
141 }
141 }
142 "exactmatcher" => {
142 "exactmatcher" => {
143 let files = matcher.call_method(
143 let files = matcher.call_method(
144 py,
144 py,
145 "files",
145 "files",
146 PyTuple::new(py, &[]),
146 PyTuple::new(py, &[]),
147 None,
147 None,
148 )?;
148 )?;
149 let files: PyList = files.cast_into(py)?;
149 let files: PyList = files.cast_into(py)?;
150 let files: PyResult<Vec<HgPathBuf>> = files
150 let files: PyResult<Vec<HgPathBuf>> = files
151 .iter(py)
151 .iter(py)
152 .map(|f| {
152 .map(|f| {
153 Ok(HgPathBuf::from_bytes(
153 Ok(HgPathBuf::from_bytes(
154 f.extract::<PyBytes>(py)?.data(py),
154 f.extract::<PyBytes>(py)?.data(py),
155 ))
155 ))
156 })
156 })
157 .collect();
157 .collect();
158
158
159 let files = files?;
159 let files = files?;
160 let matcher = FileMatcher::new(&files)
160 let matcher = FileMatcher::new(&files)
161 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
161 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
162 let ((lookup, status_res), warnings) = status(
162 let ((lookup, status_res), warnings) = status(
163 &dmap,
163 &dmap,
164 &matcher,
164 &matcher,
165 &root_dir,
165 &root_dir,
166 &ignore_files,
166 ignore_files,
167 StatusOptions {
167 StatusOptions {
168 check_exec,
168 check_exec,
169 last_normal_time,
169 last_normal_time,
170 list_clean,
170 list_clean,
171 list_ignored,
171 list_ignored,
172 list_unknown,
172 list_unknown,
173 },
173 },
174 )
174 )
175 .map_err(|e| handle_fallback(py, e))?;
175 .map_err(|e| handle_fallback(py, e))?;
176 build_response(py, lookup, status_res, warnings)
176 build_response(py, lookup, status_res, warnings)
177 }
177 }
178 "includematcher" => {
178 "includematcher" => {
179 // Get the patterns from Python even though most of them are
179 // Get the patterns from Python even though most of them are
180 // redundant with those we will parse later on, as they include
180 // redundant with those we will parse later on, as they include
181 // those passed from the command line.
181 // those passed from the command line.
182 let ignore_patterns: PyResult<Vec<_>> = matcher
182 let ignore_patterns: PyResult<Vec<_>> = matcher
183 .getattr(py, "_kindpats")?
183 .getattr(py, "_kindpats")?
184 .iter(py)?
184 .iter(py)?
185 .map(|k| {
185 .map(|k| {
186 let k = k?;
186 let k = k?;
187 let syntax = parse_pattern_syntax(
187 let syntax = parse_pattern_syntax(
188 &[
188 &[
189 k.get_item(py, 0)?
189 k.get_item(py, 0)?
190 .extract::<PyBytes>(py)?
190 .extract::<PyBytes>(py)?
191 .data(py),
191 .data(py),
192 &b":"[..],
192 &b":"[..],
193 ]
193 ]
194 .concat(),
194 .concat(),
195 )
195 )
196 .map_err(|e| {
196 .map_err(|e| {
197 handle_fallback(py, StatusError::Pattern(e))
197 handle_fallback(py, StatusError::Pattern(e))
198 })?;
198 })?;
199 let pattern = k.get_item(py, 1)?.extract::<PyBytes>(py)?;
199 let pattern = k.get_item(py, 1)?.extract::<PyBytes>(py)?;
200 let pattern = pattern.data(py);
200 let pattern = pattern.data(py);
201 let source = k.get_item(py, 2)?.extract::<PyBytes>(py)?;
201 let source = k.get_item(py, 2)?.extract::<PyBytes>(py)?;
202 let source = get_path_from_bytes(source.data(py));
202 let source = get_path_from_bytes(source.data(py));
203 let new = IgnorePattern::new(syntax, pattern, source);
203 let new = IgnorePattern::new(syntax, pattern, source);
204 Ok(new)
204 Ok(new)
205 })
205 })
206 .collect();
206 .collect();
207
207
208 let ignore_patterns = ignore_patterns?;
208 let ignore_patterns = ignore_patterns?;
209 let mut all_warnings = vec![];
209 let mut all_warnings = vec![];
210
210
211 let (matcher, warnings) =
211 let (matcher, warnings) =
212 IncludeMatcher::new(ignore_patterns, &root_dir)
212 IncludeMatcher::new(ignore_patterns, &root_dir)
213 .map_err(|e| handle_fallback(py, e.into()))?;
213 .map_err(|e| handle_fallback(py, e.into()))?;
214 all_warnings.extend(warnings);
214 all_warnings.extend(warnings);
215
215
216 let ((lookup, status_res), warnings) = status(
216 let ((lookup, status_res), warnings) = status(
217 &dmap,
217 &dmap,
218 &matcher,
218 &matcher,
219 &root_dir,
219 &root_dir,
220 &ignore_files,
220 ignore_files,
221 StatusOptions {
221 StatusOptions {
222 check_exec,
222 check_exec,
223 last_normal_time,
223 last_normal_time,
224 list_clean,
224 list_clean,
225 list_ignored,
225 list_ignored,
226 list_unknown,
226 list_unknown,
227 },
227 },
228 )
228 )
229 .map_err(|e| handle_fallback(py, e))?;
229 .map_err(|e| handle_fallback(py, e))?;
230
230
231 all_warnings.extend(warnings);
231 all_warnings.extend(warnings);
232
232
233 build_response(py, lookup, status_res, all_warnings)
233 build_response(py, lookup, status_res, all_warnings)
234 }
234 }
235 e => {
235 e => {
236 return Err(PyErr::new::<ValueError, _>(
236 return Err(PyErr::new::<ValueError, _>(
237 py,
237 py,
238 format!("Unsupported matcher {}", e),
238 format!("Unsupported matcher {}", e),
239 ));
239 ));
240 }
240 }
241 }
241 }
242 }
242 }
243
243
244 fn build_response(
244 fn build_response(
245 py: Python,
245 py: Python,
246 lookup: Vec<Cow<HgPath>>,
246 lookup: Vec<Cow<HgPath>>,
247 status_res: DirstateStatus,
247 status_res: DirstateStatus,
248 warnings: Vec<PatternFileWarning>,
248 warnings: Vec<PatternFileWarning>,
249 ) -> PyResult<PyTuple> {
249 ) -> PyResult<PyTuple> {
250 let modified = collect_pybytes_list(py, status_res.modified.as_ref());
250 let modified = collect_pybytes_list(py, status_res.modified.as_ref());
251 let added = collect_pybytes_list(py, status_res.added.as_ref());
251 let added = collect_pybytes_list(py, status_res.added.as_ref());
252 let removed = collect_pybytes_list(py, status_res.removed.as_ref());
252 let removed = collect_pybytes_list(py, status_res.removed.as_ref());
253 let deleted = collect_pybytes_list(py, status_res.deleted.as_ref());
253 let deleted = collect_pybytes_list(py, status_res.deleted.as_ref());
254 let clean = collect_pybytes_list(py, status_res.clean.as_ref());
254 let clean = collect_pybytes_list(py, status_res.clean.as_ref());
255 let ignored = collect_pybytes_list(py, status_res.ignored.as_ref());
255 let ignored = collect_pybytes_list(py, status_res.ignored.as_ref());
256 let unknown = collect_pybytes_list(py, status_res.unknown.as_ref());
256 let unknown = collect_pybytes_list(py, status_res.unknown.as_ref());
257 let lookup = collect_pybytes_list(py, lookup.as_ref());
257 let lookup = collect_pybytes_list(py, lookup.as_ref());
258 let bad = collect_bad_matches(py, status_res.bad.as_ref())?;
258 let bad = collect_bad_matches(py, status_res.bad.as_ref())?;
259 let py_warnings = PyList::new(py, &[]);
259 let py_warnings = PyList::new(py, &[]);
260 for warning in warnings.iter() {
260 for warning in warnings.iter() {
261 // We use duck-typing on the Python side for dispatch, good enough for
261 // We use duck-typing on the Python side for dispatch, good enough for
262 // now.
262 // now.
263 match warning {
263 match warning {
264 PatternFileWarning::InvalidSyntax(file, syn) => {
264 PatternFileWarning::InvalidSyntax(file, syn) => {
265 py_warnings.append(
265 py_warnings.append(
266 py,
266 py,
267 (
267 (
268 PyBytes::new(py, &get_bytes_from_path(&file)),
268 PyBytes::new(py, &get_bytes_from_path(&file)),
269 PyBytes::new(py, syn),
269 PyBytes::new(py, syn),
270 )
270 )
271 .to_py_object(py)
271 .to_py_object(py)
272 .into_object(),
272 .into_object(),
273 );
273 );
274 }
274 }
275 PatternFileWarning::NoSuchFile(file) => py_warnings.append(
275 PatternFileWarning::NoSuchFile(file) => py_warnings.append(
276 py,
276 py,
277 PyBytes::new(py, &get_bytes_from_path(&file)).into_object(),
277 PyBytes::new(py, &get_bytes_from_path(&file)).into_object(),
278 ),
278 ),
279 }
279 }
280 }
280 }
281
281
282 Ok(PyTuple::new(
282 Ok(PyTuple::new(
283 py,
283 py,
284 &[
284 &[
285 lookup.into_object(),
285 lookup.into_object(),
286 modified.into_object(),
286 modified.into_object(),
287 added.into_object(),
287 added.into_object(),
288 removed.into_object(),
288 removed.into_object(),
289 deleted.into_object(),
289 deleted.into_object(),
290 clean.into_object(),
290 clean.into_object(),
291 ignored.into_object(),
291 ignored.into_object(),
292 unknown.into_object(),
292 unknown.into_object(),
293 py_warnings.into_object(),
293 py_warnings.into_object(),
294 bad.into_object(),
294 bad.into_object(),
295 ][..],
295 ][..],
296 ))
296 ))
297 }
297 }
General Comments 0
You need to be logged in to leave comments. Login now