##// END OF EJS Templates
rust-status: fix issue6456 for the Rust implementation also...
Raphaël Gomès -
r47491:3c9ddb19 default
parent child Browse files
Show More
@@ -1,988 +1,994 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 #[cfg(feature = "dirstate-tree")]
12 #[cfg(feature = "dirstate-tree")]
13 use crate::dirstate::dirstate_tree::iter::StatusShortcut;
13 use crate::dirstate::dirstate_tree::iter::StatusShortcut;
14 #[cfg(not(feature = "dirstate-tree"))]
14 #[cfg(not(feature = "dirstate-tree"))]
15 use crate::utils::path_auditor::PathAuditor;
15 use crate::utils::path_auditor::PathAuditor;
16 use crate::{
16 use crate::{
17 dirstate::SIZE_FROM_OTHER_PARENT,
17 dirstate::SIZE_FROM_OTHER_PARENT,
18 filepatterns::PatternFileWarning,
18 filepatterns::PatternFileWarning,
19 matchers::{get_ignore_function, Matcher, VisitChildrenSet},
19 matchers::{get_ignore_function, Matcher, VisitChildrenSet},
20 utils::{
20 utils::{
21 files::{find_dirs, HgMetadata},
21 files::{find_dirs, HgMetadata},
22 hg_path::{
22 hg_path::{
23 hg_path_to_path_buf, os_string_to_hg_path_buf, HgPath, HgPathBuf,
23 hg_path_to_path_buf, os_string_to_hg_path_buf, HgPath, HgPathBuf,
24 HgPathError,
24 HgPathError,
25 },
25 },
26 },
26 },
27 CopyMap, DirstateEntry, DirstateMap, EntryState, FastHashMap,
27 CopyMap, DirstateEntry, DirstateMap, EntryState, FastHashMap,
28 PatternError,
28 PatternError,
29 };
29 };
30 use lazy_static::lazy_static;
30 use lazy_static::lazy_static;
31 use micro_timer::timed;
31 use micro_timer::timed;
32 use rayon::prelude::*;
32 use rayon::prelude::*;
33 use std::{
33 use std::{
34 borrow::Cow,
34 borrow::Cow,
35 collections::HashSet,
35 collections::HashSet,
36 fmt,
36 fmt,
37 fs::{read_dir, DirEntry},
37 fs::{read_dir, DirEntry},
38 io::ErrorKind,
38 io::ErrorKind,
39 ops::Deref,
39 ops::Deref,
40 path::{Path, PathBuf},
40 path::{Path, PathBuf},
41 };
41 };
42
42
43 /// Wrong type of file from a `BadMatch`
43 /// Wrong type of file from a `BadMatch`
44 /// Note: a lot of those don't exist on all platforms.
44 /// Note: a lot of those don't exist on all platforms.
45 #[derive(Debug, Copy, Clone)]
45 #[derive(Debug, Copy, Clone)]
46 pub enum BadType {
46 pub enum BadType {
47 CharacterDevice,
47 CharacterDevice,
48 BlockDevice,
48 BlockDevice,
49 FIFO,
49 FIFO,
50 Socket,
50 Socket,
51 Directory,
51 Directory,
52 Unknown,
52 Unknown,
53 }
53 }
54
54
55 impl fmt::Display for BadType {
55 impl fmt::Display for BadType {
56 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
56 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
57 f.write_str(match self {
57 f.write_str(match self {
58 BadType::CharacterDevice => "character device",
58 BadType::CharacterDevice => "character device",
59 BadType::BlockDevice => "block device",
59 BadType::BlockDevice => "block device",
60 BadType::FIFO => "fifo",
60 BadType::FIFO => "fifo",
61 BadType::Socket => "socket",
61 BadType::Socket => "socket",
62 BadType::Directory => "directory",
62 BadType::Directory => "directory",
63 BadType::Unknown => "unknown",
63 BadType::Unknown => "unknown",
64 })
64 })
65 }
65 }
66 }
66 }
67
67
68 /// Was explicitly matched but cannot be found/accessed
68 /// Was explicitly matched but cannot be found/accessed
69 #[derive(Debug, Copy, Clone)]
69 #[derive(Debug, Copy, Clone)]
70 pub enum BadMatch {
70 pub enum BadMatch {
71 OsError(i32),
71 OsError(i32),
72 BadType(BadType),
72 BadType(BadType),
73 }
73 }
74
74
75 /// Enum used to dispatch new status entries into the right collections.
75 /// Enum used to dispatch new status entries into the right collections.
76 /// Is similar to `crate::EntryState`, but represents the transient state of
76 /// Is similar to `crate::EntryState`, but represents the transient state of
77 /// entries during the lifetime of a command.
77 /// entries during the lifetime of a command.
78 #[derive(Debug, Copy, Clone)]
78 #[derive(Debug, Copy, Clone)]
79 pub enum Dispatch {
79 pub enum Dispatch {
80 Unsure,
80 Unsure,
81 Modified,
81 Modified,
82 Added,
82 Added,
83 Removed,
83 Removed,
84 Deleted,
84 Deleted,
85 Clean,
85 Clean,
86 Unknown,
86 Unknown,
87 Ignored,
87 Ignored,
88 /// Empty dispatch, the file is not worth listing
88 /// Empty dispatch, the file is not worth listing
89 None,
89 None,
90 /// Was explicitly matched but cannot be found/accessed
90 /// Was explicitly matched but cannot be found/accessed
91 Bad(BadMatch),
91 Bad(BadMatch),
92 Directory {
92 Directory {
93 /// True if the directory used to be a file in the dmap so we can say
93 /// True if the directory used to be a file in the dmap so we can say
94 /// that it's been removed.
94 /// that it's been removed.
95 was_file: bool,
95 was_file: bool,
96 },
96 },
97 }
97 }
98
98
99 type IoResult<T> = std::io::Result<T>;
99 type IoResult<T> = std::io::Result<T>;
100
100
101 /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait, 'static>`, so add
101 /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait, 'static>`, so add
102 /// an explicit lifetime here to not fight `'static` bounds "out of nowhere".
102 /// an explicit lifetime here to not fight `'static` bounds "out of nowhere".
103 type IgnoreFnType<'a> = Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>;
103 type IgnoreFnType<'a> = Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>;
104
104
105 /// We have a good mix of owned (from directory traversal) and borrowed (from
105 /// We have a good mix of owned (from directory traversal) and borrowed (from
106 /// the dirstate/explicit) paths, this comes up a lot.
106 /// the dirstate/explicit) paths, this comes up a lot.
107 pub type HgPathCow<'a> = Cow<'a, HgPath>;
107 pub type HgPathCow<'a> = Cow<'a, HgPath>;
108
108
109 /// A path with its computed ``Dispatch`` information
109 /// A path with its computed ``Dispatch`` information
110 type DispatchedPath<'a> = (HgPathCow<'a>, Dispatch);
110 type DispatchedPath<'a> = (HgPathCow<'a>, Dispatch);
111
111
112 /// The conversion from `HgPath` to a real fs path failed.
112 /// The conversion from `HgPath` to a real fs path failed.
113 /// `22` is the error code for "Invalid argument"
113 /// `22` is the error code for "Invalid argument"
114 const INVALID_PATH_DISPATCH: Dispatch = Dispatch::Bad(BadMatch::OsError(22));
114 const INVALID_PATH_DISPATCH: Dispatch = Dispatch::Bad(BadMatch::OsError(22));
115
115
116 /// Dates and times that are outside the 31-bit signed range are compared
116 /// Dates and times that are outside the 31-bit signed range are compared
117 /// modulo 2^31. This should prevent hg from behaving badly with very large
117 /// modulo 2^31. This should prevent hg from behaving badly with very large
118 /// files or corrupt dates while still having a high probability of detecting
118 /// files or corrupt dates while still having a high probability of detecting
119 /// changes. (issue2608)
119 /// changes. (issue2608)
120 /// TODO I haven't found a way of having `b` be `Into<i32>`, since `From<u64>`
120 /// TODO I haven't found a way of having `b` be `Into<i32>`, since `From<u64>`
121 /// is not defined for `i32`, and there is no `As` trait. This forces the
121 /// is not defined for `i32`, and there is no `As` trait. This forces the
122 /// caller to cast `b` as `i32`.
122 /// caller to cast `b` as `i32`.
123 fn mod_compare(a: i32, b: i32) -> bool {
123 fn mod_compare(a: i32, b: i32) -> bool {
124 a & i32::max_value() != b & i32::max_value()
124 a & i32::max_value() != b & i32::max_value()
125 }
125 }
126
126
127 /// Return a sorted list containing information about the entries
127 /// Return a sorted list containing information about the entries
128 /// in the directory.
128 /// in the directory.
129 ///
129 ///
130 /// * `skip_dot_hg` - Return an empty vec if `path` contains a `.hg` directory
130 /// * `skip_dot_hg` - Return an empty vec if `path` contains a `.hg` directory
131 fn list_directory(
131 fn list_directory(
132 path: impl AsRef<Path>,
132 path: impl AsRef<Path>,
133 skip_dot_hg: bool,
133 skip_dot_hg: bool,
134 ) -> std::io::Result<Vec<(HgPathBuf, DirEntry)>> {
134 ) -> std::io::Result<Vec<(HgPathBuf, DirEntry)>> {
135 let mut results = vec![];
135 let mut results = vec![];
136 let entries = read_dir(path.as_ref())?;
136 let entries = read_dir(path.as_ref())?;
137
137
138 for entry in entries {
138 for entry in entries {
139 let entry = entry?;
139 let entry = entry?;
140 let filename = os_string_to_hg_path_buf(entry.file_name())?;
140 let filename = os_string_to_hg_path_buf(entry.file_name())?;
141 let file_type = entry.file_type()?;
141 let file_type = entry.file_type()?;
142 if skip_dot_hg && filename.as_bytes() == b".hg" && file_type.is_dir() {
142 if skip_dot_hg && filename.as_bytes() == b".hg" && file_type.is_dir() {
143 return Ok(vec![]);
143 return Ok(vec![]);
144 } else {
144 } else {
145 results.push((filename, entry))
145 results.push((filename, entry))
146 }
146 }
147 }
147 }
148
148
149 results.sort_unstable_by_key(|e| e.0.clone());
149 results.sort_unstable_by_key(|e| e.0.clone());
150 Ok(results)
150 Ok(results)
151 }
151 }
152
152
153 /// The file corresponding to the dirstate entry was found on the filesystem.
153 /// The file corresponding to the dirstate entry was found on the filesystem.
154 fn dispatch_found(
154 fn dispatch_found(
155 filename: impl AsRef<HgPath>,
155 filename: impl AsRef<HgPath>,
156 entry: DirstateEntry,
156 entry: DirstateEntry,
157 metadata: HgMetadata,
157 metadata: HgMetadata,
158 copy_map: &CopyMap,
158 copy_map: &CopyMap,
159 options: StatusOptions,
159 options: StatusOptions,
160 ) -> Dispatch {
160 ) -> Dispatch {
161 let DirstateEntry {
161 let DirstateEntry {
162 state,
162 state,
163 mode,
163 mode,
164 mtime,
164 mtime,
165 size,
165 size,
166 } = entry;
166 } = entry;
167
167
168 let HgMetadata {
168 let HgMetadata {
169 st_mode,
169 st_mode,
170 st_size,
170 st_size,
171 st_mtime,
171 st_mtime,
172 ..
172 ..
173 } = metadata;
173 } = metadata;
174
174
175 match state {
175 match state {
176 EntryState::Normal => {
176 EntryState::Normal => {
177 let size_changed = mod_compare(size, st_size as i32);
177 let size_changed = mod_compare(size, st_size as i32);
178 let mode_changed =
178 let mode_changed =
179 (mode ^ st_mode as i32) & 0o100 != 0o000 && options.check_exec;
179 (mode ^ st_mode as i32) & 0o100 != 0o000 && options.check_exec;
180 let metadata_changed = size >= 0 && (size_changed || mode_changed);
180 let metadata_changed = size >= 0 && (size_changed || mode_changed);
181 let other_parent = size == SIZE_FROM_OTHER_PARENT;
181 let other_parent = size == SIZE_FROM_OTHER_PARENT;
182
182
183 if metadata_changed
183 if metadata_changed
184 || other_parent
184 || other_parent
185 || copy_map.contains_key(filename.as_ref())
185 || copy_map.contains_key(filename.as_ref())
186 {
186 {
187 Dispatch::Modified
187 if metadata.is_symlink() && size_changed {
188 // issue6456: Size returned may be longer due to encryption
189 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
190 Dispatch::Unsure
191 } else {
192 Dispatch::Modified
193 }
188 } else if mod_compare(mtime, st_mtime as i32)
194 } else if mod_compare(mtime, st_mtime as i32)
189 || st_mtime == options.last_normal_time
195 || st_mtime == options.last_normal_time
190 {
196 {
191 // the file may have just been marked as normal and
197 // the file may have just been marked as normal and
192 // it may have changed in the same second without
198 // it may have changed in the same second without
193 // changing its size. This can happen if we quickly
199 // changing its size. This can happen if we quickly
194 // do multiple commits. Force lookup, so we don't
200 // do multiple commits. Force lookup, so we don't
195 // miss such a racy file change.
201 // miss such a racy file change.
196 Dispatch::Unsure
202 Dispatch::Unsure
197 } else if options.list_clean {
203 } else if options.list_clean {
198 Dispatch::Clean
204 Dispatch::Clean
199 } else {
205 } else {
200 Dispatch::None
206 Dispatch::None
201 }
207 }
202 }
208 }
203 EntryState::Merged => Dispatch::Modified,
209 EntryState::Merged => Dispatch::Modified,
204 EntryState::Added => Dispatch::Added,
210 EntryState::Added => Dispatch::Added,
205 EntryState::Removed => Dispatch::Removed,
211 EntryState::Removed => Dispatch::Removed,
206 EntryState::Unknown => Dispatch::Unknown,
212 EntryState::Unknown => Dispatch::Unknown,
207 }
213 }
208 }
214 }
209
215
210 /// The file corresponding to this Dirstate entry is missing.
216 /// The file corresponding to this Dirstate entry is missing.
211 fn dispatch_missing(state: EntryState) -> Dispatch {
217 fn dispatch_missing(state: EntryState) -> Dispatch {
212 match state {
218 match state {
213 // File was removed from the filesystem during commands
219 // File was removed from the filesystem during commands
214 EntryState::Normal | EntryState::Merged | EntryState::Added => {
220 EntryState::Normal | EntryState::Merged | EntryState::Added => {
215 Dispatch::Deleted
221 Dispatch::Deleted
216 }
222 }
217 // File was removed, everything is normal
223 // File was removed, everything is normal
218 EntryState::Removed => Dispatch::Removed,
224 EntryState::Removed => Dispatch::Removed,
219 // File is unknown to Mercurial, everything is normal
225 // File is unknown to Mercurial, everything is normal
220 EntryState::Unknown => Dispatch::Unknown,
226 EntryState::Unknown => Dispatch::Unknown,
221 }
227 }
222 }
228 }
223
229
224 fn dispatch_os_error(e: &std::io::Error) -> Dispatch {
230 fn dispatch_os_error(e: &std::io::Error) -> Dispatch {
225 Dispatch::Bad(BadMatch::OsError(
231 Dispatch::Bad(BadMatch::OsError(
226 e.raw_os_error().expect("expected real OS error"),
232 e.raw_os_error().expect("expected real OS error"),
227 ))
233 ))
228 }
234 }
229
235
230 lazy_static! {
236 lazy_static! {
231 static ref DEFAULT_WORK: HashSet<&'static HgPath> = {
237 static ref DEFAULT_WORK: HashSet<&'static HgPath> = {
232 let mut h = HashSet::new();
238 let mut h = HashSet::new();
233 h.insert(HgPath::new(b""));
239 h.insert(HgPath::new(b""));
234 h
240 h
235 };
241 };
236 }
242 }
237
243
238 #[derive(Debug, Copy, Clone)]
244 #[derive(Debug, Copy, Clone)]
239 pub struct StatusOptions {
245 pub struct StatusOptions {
240 /// Remember the most recent modification timeslot for status, to make
246 /// Remember the most recent modification timeslot for status, to make
241 /// sure we won't miss future size-preserving file content modifications
247 /// sure we won't miss future size-preserving file content modifications
242 /// that happen within the same timeslot.
248 /// that happen within the same timeslot.
243 pub last_normal_time: i64,
249 pub last_normal_time: i64,
244 /// Whether we are on a filesystem with UNIX-like exec flags
250 /// Whether we are on a filesystem with UNIX-like exec flags
245 pub check_exec: bool,
251 pub check_exec: bool,
246 pub list_clean: bool,
252 pub list_clean: bool,
247 pub list_unknown: bool,
253 pub list_unknown: bool,
248 pub list_ignored: bool,
254 pub list_ignored: bool,
249 /// Whether to collect traversed dirs for applying a callback later.
255 /// Whether to collect traversed dirs for applying a callback later.
250 /// Used by `hg purge` for example.
256 /// Used by `hg purge` for example.
251 pub collect_traversed_dirs: bool,
257 pub collect_traversed_dirs: bool,
252 }
258 }
253
259
254 #[derive(Debug)]
260 #[derive(Debug)]
255 pub struct DirstateStatus<'a> {
261 pub struct DirstateStatus<'a> {
256 pub modified: Vec<HgPathCow<'a>>,
262 pub modified: Vec<HgPathCow<'a>>,
257 pub added: Vec<HgPathCow<'a>>,
263 pub added: Vec<HgPathCow<'a>>,
258 pub removed: Vec<HgPathCow<'a>>,
264 pub removed: Vec<HgPathCow<'a>>,
259 pub deleted: Vec<HgPathCow<'a>>,
265 pub deleted: Vec<HgPathCow<'a>>,
260 pub clean: Vec<HgPathCow<'a>>,
266 pub clean: Vec<HgPathCow<'a>>,
261 pub ignored: Vec<HgPathCow<'a>>,
267 pub ignored: Vec<HgPathCow<'a>>,
262 pub unknown: Vec<HgPathCow<'a>>,
268 pub unknown: Vec<HgPathCow<'a>>,
263 pub bad: Vec<(HgPathCow<'a>, BadMatch)>,
269 pub bad: Vec<(HgPathCow<'a>, BadMatch)>,
264 /// Only filled if `collect_traversed_dirs` is `true`
270 /// Only filled if `collect_traversed_dirs` is `true`
265 pub traversed: Vec<HgPathBuf>,
271 pub traversed: Vec<HgPathBuf>,
266 }
272 }
267
273
268 #[derive(Debug, derive_more::From)]
274 #[derive(Debug, derive_more::From)]
269 pub enum StatusError {
275 pub enum StatusError {
270 /// Generic IO error
276 /// Generic IO error
271 IO(std::io::Error),
277 IO(std::io::Error),
272 /// An invalid path that cannot be represented in Mercurial was found
278 /// An invalid path that cannot be represented in Mercurial was found
273 Path(HgPathError),
279 Path(HgPathError),
274 /// An invalid "ignore" pattern was found
280 /// An invalid "ignore" pattern was found
275 Pattern(PatternError),
281 Pattern(PatternError),
276 }
282 }
277
283
278 pub type StatusResult<T> = Result<T, StatusError>;
284 pub type StatusResult<T> = Result<T, StatusError>;
279
285
280 impl fmt::Display for StatusError {
286 impl fmt::Display for StatusError {
281 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
287 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
282 match self {
288 match self {
283 StatusError::IO(error) => error.fmt(f),
289 StatusError::IO(error) => error.fmt(f),
284 StatusError::Path(error) => error.fmt(f),
290 StatusError::Path(error) => error.fmt(f),
285 StatusError::Pattern(error) => error.fmt(f),
291 StatusError::Pattern(error) => error.fmt(f),
286 }
292 }
287 }
293 }
288 }
294 }
289
295
290 /// Gives information about which files are changed in the working directory
296 /// Gives information about which files are changed in the working directory
291 /// and how, compared to the revision we're based on
297 /// and how, compared to the revision we're based on
292 pub struct Status<'a, M: Matcher + Sync> {
298 pub struct Status<'a, M: Matcher + Sync> {
293 dmap: &'a DirstateMap,
299 dmap: &'a DirstateMap,
294 pub(crate) matcher: &'a M,
300 pub(crate) matcher: &'a M,
295 root_dir: PathBuf,
301 root_dir: PathBuf,
296 pub(crate) options: StatusOptions,
302 pub(crate) options: StatusOptions,
297 ignore_fn: IgnoreFnType<'a>,
303 ignore_fn: IgnoreFnType<'a>,
298 }
304 }
299
305
300 impl<'a, M> Status<'a, M>
306 impl<'a, M> Status<'a, M>
301 where
307 where
302 M: Matcher + Sync,
308 M: Matcher + Sync,
303 {
309 {
304 pub fn new(
310 pub fn new(
305 dmap: &'a DirstateMap,
311 dmap: &'a DirstateMap,
306 matcher: &'a M,
312 matcher: &'a M,
307 root_dir: PathBuf,
313 root_dir: PathBuf,
308 ignore_files: Vec<PathBuf>,
314 ignore_files: Vec<PathBuf>,
309 options: StatusOptions,
315 options: StatusOptions,
310 ) -> StatusResult<(Self, Vec<PatternFileWarning>)> {
316 ) -> StatusResult<(Self, Vec<PatternFileWarning>)> {
311 // Needs to outlive `dir_ignore_fn` since it's captured.
317 // Needs to outlive `dir_ignore_fn` since it's captured.
312
318
313 let (ignore_fn, warnings): (IgnoreFnType, _) =
319 let (ignore_fn, warnings): (IgnoreFnType, _) =
314 if options.list_ignored || options.list_unknown {
320 if options.list_ignored || options.list_unknown {
315 get_ignore_function(ignore_files, &root_dir)?
321 get_ignore_function(ignore_files, &root_dir)?
316 } else {
322 } else {
317 (Box::new(|&_| true), vec![])
323 (Box::new(|&_| true), vec![])
318 };
324 };
319
325
320 Ok((
326 Ok((
321 Self {
327 Self {
322 dmap,
328 dmap,
323 matcher,
329 matcher,
324 root_dir,
330 root_dir,
325 options,
331 options,
326 ignore_fn,
332 ignore_fn,
327 },
333 },
328 warnings,
334 warnings,
329 ))
335 ))
330 }
336 }
331
337
332 /// Is the path ignored?
338 /// Is the path ignored?
333 pub fn is_ignored(&self, path: impl AsRef<HgPath>) -> bool {
339 pub fn is_ignored(&self, path: impl AsRef<HgPath>) -> bool {
334 (self.ignore_fn)(path.as_ref())
340 (self.ignore_fn)(path.as_ref())
335 }
341 }
336
342
337 /// Is the path or one of its ancestors ignored?
343 /// Is the path or one of its ancestors ignored?
338 pub fn dir_ignore(&self, dir: impl AsRef<HgPath>) -> bool {
344 pub fn dir_ignore(&self, dir: impl AsRef<HgPath>) -> bool {
339 // Only involve ignore mechanism if we're listing unknowns or ignored.
345 // Only involve ignore mechanism if we're listing unknowns or ignored.
340 if self.options.list_ignored || self.options.list_unknown {
346 if self.options.list_ignored || self.options.list_unknown {
341 if self.is_ignored(&dir) {
347 if self.is_ignored(&dir) {
342 true
348 true
343 } else {
349 } else {
344 for p in find_dirs(dir.as_ref()) {
350 for p in find_dirs(dir.as_ref()) {
345 if self.is_ignored(p) {
351 if self.is_ignored(p) {
346 return true;
352 return true;
347 }
353 }
348 }
354 }
349 false
355 false
350 }
356 }
351 } else {
357 } else {
352 true
358 true
353 }
359 }
354 }
360 }
355
361
356 /// Get stat data about the files explicitly specified by the matcher.
362 /// Get stat data about the files explicitly specified by the matcher.
357 /// Returns a tuple of the directories that need to be traversed and the
363 /// Returns a tuple of the directories that need to be traversed and the
358 /// files with their corresponding `Dispatch`.
364 /// files with their corresponding `Dispatch`.
359 /// TODO subrepos
365 /// TODO subrepos
360 #[timed]
366 #[timed]
361 pub fn walk_explicit(
367 pub fn walk_explicit(
362 &self,
368 &self,
363 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
369 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
364 ) -> (Vec<DispatchedPath<'a>>, Vec<DispatchedPath<'a>>) {
370 ) -> (Vec<DispatchedPath<'a>>, Vec<DispatchedPath<'a>>) {
365 self.matcher
371 self.matcher
366 .file_set()
372 .file_set()
367 .unwrap_or(&DEFAULT_WORK)
373 .unwrap_or(&DEFAULT_WORK)
368 .par_iter()
374 .par_iter()
369 .flat_map(|&filename| -> Option<_> {
375 .flat_map(|&filename| -> Option<_> {
370 // TODO normalization
376 // TODO normalization
371 let normalized = filename;
377 let normalized = filename;
372
378
373 let buf = match hg_path_to_path_buf(normalized) {
379 let buf = match hg_path_to_path_buf(normalized) {
374 Ok(x) => x,
380 Ok(x) => x,
375 Err(_) => {
381 Err(_) => {
376 return Some((
382 return Some((
377 Cow::Borrowed(normalized),
383 Cow::Borrowed(normalized),
378 INVALID_PATH_DISPATCH,
384 INVALID_PATH_DISPATCH,
379 ))
385 ))
380 }
386 }
381 };
387 };
382 let target = self.root_dir.join(buf);
388 let target = self.root_dir.join(buf);
383 let st = target.symlink_metadata();
389 let st = target.symlink_metadata();
384 let in_dmap = self.dmap.get(normalized);
390 let in_dmap = self.dmap.get(normalized);
385 match st {
391 match st {
386 Ok(meta) => {
392 Ok(meta) => {
387 let file_type = meta.file_type();
393 let file_type = meta.file_type();
388 return if file_type.is_file() || file_type.is_symlink()
394 return if file_type.is_file() || file_type.is_symlink()
389 {
395 {
390 if let Some(entry) = in_dmap {
396 if let Some(entry) = in_dmap {
391 return Some((
397 return Some((
392 Cow::Borrowed(normalized),
398 Cow::Borrowed(normalized),
393 dispatch_found(
399 dispatch_found(
394 &normalized,
400 &normalized,
395 *entry,
401 *entry,
396 HgMetadata::from_metadata(meta),
402 HgMetadata::from_metadata(meta),
397 &self.dmap.copy_map,
403 &self.dmap.copy_map,
398 self.options,
404 self.options,
399 ),
405 ),
400 ));
406 ));
401 }
407 }
402 Some((
408 Some((
403 Cow::Borrowed(normalized),
409 Cow::Borrowed(normalized),
404 Dispatch::Unknown,
410 Dispatch::Unknown,
405 ))
411 ))
406 } else if file_type.is_dir() {
412 } else if file_type.is_dir() {
407 if self.options.collect_traversed_dirs {
413 if self.options.collect_traversed_dirs {
408 traversed_sender
414 traversed_sender
409 .send(normalized.to_owned())
415 .send(normalized.to_owned())
410 .expect("receiver should outlive sender");
416 .expect("receiver should outlive sender");
411 }
417 }
412 Some((
418 Some((
413 Cow::Borrowed(normalized),
419 Cow::Borrowed(normalized),
414 Dispatch::Directory {
420 Dispatch::Directory {
415 was_file: in_dmap.is_some(),
421 was_file: in_dmap.is_some(),
416 },
422 },
417 ))
423 ))
418 } else {
424 } else {
419 Some((
425 Some((
420 Cow::Borrowed(normalized),
426 Cow::Borrowed(normalized),
421 Dispatch::Bad(BadMatch::BadType(
427 Dispatch::Bad(BadMatch::BadType(
422 // TODO do more than unknown
428 // TODO do more than unknown
423 // Support for all `BadType` variant
429 // Support for all `BadType` variant
424 // varies greatly between platforms.
430 // varies greatly between platforms.
425 // So far, no tests check the type and
431 // So far, no tests check the type and
426 // this should be good enough for most
432 // this should be good enough for most
427 // users.
433 // users.
428 BadType::Unknown,
434 BadType::Unknown,
429 )),
435 )),
430 ))
436 ))
431 };
437 };
432 }
438 }
433 Err(_) => {
439 Err(_) => {
434 if let Some(entry) = in_dmap {
440 if let Some(entry) = in_dmap {
435 return Some((
441 return Some((
436 Cow::Borrowed(normalized),
442 Cow::Borrowed(normalized),
437 dispatch_missing(entry.state),
443 dispatch_missing(entry.state),
438 ));
444 ));
439 }
445 }
440 }
446 }
441 };
447 };
442 None
448 None
443 })
449 })
444 .partition(|(_, dispatch)| match dispatch {
450 .partition(|(_, dispatch)| match dispatch {
445 Dispatch::Directory { .. } => true,
451 Dispatch::Directory { .. } => true,
446 _ => false,
452 _ => false,
447 })
453 })
448 }
454 }
449
455
450 /// Walk the working directory recursively to look for changes compared to
456 /// Walk the working directory recursively to look for changes compared to
451 /// the current `DirstateMap`.
457 /// the current `DirstateMap`.
452 ///
458 ///
453 /// This takes a mutable reference to the results to account for the
459 /// This takes a mutable reference to the results to account for the
454 /// `extend` in timings
460 /// `extend` in timings
455 #[timed]
461 #[timed]
456 pub fn traverse(
462 pub fn traverse(
457 &self,
463 &self,
458 path: impl AsRef<HgPath>,
464 path: impl AsRef<HgPath>,
459 old_results: &FastHashMap<HgPathCow<'a>, Dispatch>,
465 old_results: &FastHashMap<HgPathCow<'a>, Dispatch>,
460 results: &mut Vec<DispatchedPath<'a>>,
466 results: &mut Vec<DispatchedPath<'a>>,
461 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
467 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
462 ) {
468 ) {
463 // The traversal is done in parallel, so use a channel to gather
469 // The traversal is done in parallel, so use a channel to gather
464 // entries. `crossbeam_channel::Sender` is `Sync`, while `mpsc::Sender`
470 // entries. `crossbeam_channel::Sender` is `Sync`, while `mpsc::Sender`
465 // is not.
471 // is not.
466 let (files_transmitter, files_receiver) =
472 let (files_transmitter, files_receiver) =
467 crossbeam_channel::unbounded();
473 crossbeam_channel::unbounded();
468
474
469 self.traverse_dir(
475 self.traverse_dir(
470 &files_transmitter,
476 &files_transmitter,
471 path,
477 path,
472 &old_results,
478 &old_results,
473 traversed_sender,
479 traversed_sender,
474 );
480 );
475
481
476 // Disconnect the channel so the receiver stops waiting
482 // Disconnect the channel so the receiver stops waiting
477 drop(files_transmitter);
483 drop(files_transmitter);
478
484
479 let new_results = files_receiver
485 let new_results = files_receiver
480 .into_iter()
486 .into_iter()
481 .par_bridge()
487 .par_bridge()
482 .map(|(f, d)| (Cow::Owned(f), d));
488 .map(|(f, d)| (Cow::Owned(f), d));
483
489
484 results.par_extend(new_results);
490 results.par_extend(new_results);
485 }
491 }
486
492
487 /// Dispatch a single entry (file, folder, symlink...) found during
493 /// Dispatch a single entry (file, folder, symlink...) found during
488 /// `traverse`. If the entry is a folder that needs to be traversed, it
494 /// `traverse`. If the entry is a folder that needs to be traversed, it
489 /// will be handled in a separate thread.
495 /// will be handled in a separate thread.
490 fn handle_traversed_entry<'b>(
496 fn handle_traversed_entry<'b>(
491 &'a self,
497 &'a self,
492 scope: &rayon::Scope<'b>,
498 scope: &rayon::Scope<'b>,
493 files_sender: &'b crossbeam_channel::Sender<(HgPathBuf, Dispatch)>,
499 files_sender: &'b crossbeam_channel::Sender<(HgPathBuf, Dispatch)>,
494 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
500 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
495 filename: HgPathBuf,
501 filename: HgPathBuf,
496 dir_entry: DirEntry,
502 dir_entry: DirEntry,
497 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
503 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
498 ) -> IoResult<()>
504 ) -> IoResult<()>
499 where
505 where
500 'a: 'b,
506 'a: 'b,
501 {
507 {
502 let file_type = dir_entry.file_type()?;
508 let file_type = dir_entry.file_type()?;
503 let entry_option = self.dmap.get(&filename);
509 let entry_option = self.dmap.get(&filename);
504
510
505 if filename.as_bytes() == b".hg" {
511 if filename.as_bytes() == b".hg" {
506 // Could be a directory or a symlink
512 // Could be a directory or a symlink
507 return Ok(());
513 return Ok(());
508 }
514 }
509
515
510 if file_type.is_dir() {
516 if file_type.is_dir() {
511 self.handle_traversed_dir(
517 self.handle_traversed_dir(
512 scope,
518 scope,
513 files_sender,
519 files_sender,
514 old_results,
520 old_results,
515 entry_option,
521 entry_option,
516 filename,
522 filename,
517 traversed_sender,
523 traversed_sender,
518 );
524 );
519 } else if file_type.is_file() || file_type.is_symlink() {
525 } else if file_type.is_file() || file_type.is_symlink() {
520 if let Some(entry) = entry_option {
526 if let Some(entry) = entry_option {
521 if self.matcher.matches_everything()
527 if self.matcher.matches_everything()
522 || self.matcher.matches(&filename)
528 || self.matcher.matches(&filename)
523 {
529 {
524 let metadata = dir_entry.metadata()?;
530 let metadata = dir_entry.metadata()?;
525 files_sender
531 files_sender
526 .send((
532 .send((
527 filename.to_owned(),
533 filename.to_owned(),
528 dispatch_found(
534 dispatch_found(
529 &filename,
535 &filename,
530 *entry,
536 *entry,
531 HgMetadata::from_metadata(metadata),
537 HgMetadata::from_metadata(metadata),
532 &self.dmap.copy_map,
538 &self.dmap.copy_map,
533 self.options,
539 self.options,
534 ),
540 ),
535 ))
541 ))
536 .unwrap();
542 .unwrap();
537 }
543 }
538 } else if (self.matcher.matches_everything()
544 } else if (self.matcher.matches_everything()
539 || self.matcher.matches(&filename))
545 || self.matcher.matches(&filename))
540 && !self.is_ignored(&filename)
546 && !self.is_ignored(&filename)
541 {
547 {
542 if (self.options.list_ignored
548 if (self.options.list_ignored
543 || self.matcher.exact_match(&filename))
549 || self.matcher.exact_match(&filename))
544 && self.dir_ignore(&filename)
550 && self.dir_ignore(&filename)
545 {
551 {
546 if self.options.list_ignored {
552 if self.options.list_ignored {
547 files_sender
553 files_sender
548 .send((filename.to_owned(), Dispatch::Ignored))
554 .send((filename.to_owned(), Dispatch::Ignored))
549 .unwrap();
555 .unwrap();
550 }
556 }
551 } else if self.options.list_unknown {
557 } else if self.options.list_unknown {
552 files_sender
558 files_sender
553 .send((filename.to_owned(), Dispatch::Unknown))
559 .send((filename.to_owned(), Dispatch::Unknown))
554 .unwrap();
560 .unwrap();
555 }
561 }
556 } else if self.is_ignored(&filename) && self.options.list_ignored {
562 } else if self.is_ignored(&filename) && self.options.list_ignored {
557 files_sender
563 files_sender
558 .send((filename.to_owned(), Dispatch::Ignored))
564 .send((filename.to_owned(), Dispatch::Ignored))
559 .unwrap();
565 .unwrap();
560 }
566 }
561 } else if let Some(entry) = entry_option {
567 } else if let Some(entry) = entry_option {
562 // Used to be a file or a folder, now something else.
568 // Used to be a file or a folder, now something else.
563 if self.matcher.matches_everything()
569 if self.matcher.matches_everything()
564 || self.matcher.matches(&filename)
570 || self.matcher.matches(&filename)
565 {
571 {
566 files_sender
572 files_sender
567 .send((filename.to_owned(), dispatch_missing(entry.state)))
573 .send((filename.to_owned(), dispatch_missing(entry.state)))
568 .unwrap();
574 .unwrap();
569 }
575 }
570 }
576 }
571
577
572 Ok(())
578 Ok(())
573 }
579 }
574
580
575 /// A directory was found in the filesystem and needs to be traversed
581 /// A directory was found in the filesystem and needs to be traversed
576 fn handle_traversed_dir<'b>(
582 fn handle_traversed_dir<'b>(
577 &'a self,
583 &'a self,
578 scope: &rayon::Scope<'b>,
584 scope: &rayon::Scope<'b>,
579 files_sender: &'b crossbeam_channel::Sender<(HgPathBuf, Dispatch)>,
585 files_sender: &'b crossbeam_channel::Sender<(HgPathBuf, Dispatch)>,
580 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
586 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
581 entry_option: Option<&'a DirstateEntry>,
587 entry_option: Option<&'a DirstateEntry>,
582 directory: HgPathBuf,
588 directory: HgPathBuf,
583 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
589 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
584 ) where
590 ) where
585 'a: 'b,
591 'a: 'b,
586 {
592 {
587 scope.spawn(move |_| {
593 scope.spawn(move |_| {
588 // Nested `if` until `rust-lang/rust#53668` is stable
594 // Nested `if` until `rust-lang/rust#53668` is stable
589 if let Some(entry) = entry_option {
595 if let Some(entry) = entry_option {
590 // Used to be a file, is now a folder
596 // Used to be a file, is now a folder
591 if self.matcher.matches_everything()
597 if self.matcher.matches_everything()
592 || self.matcher.matches(&directory)
598 || self.matcher.matches(&directory)
593 {
599 {
594 files_sender
600 files_sender
595 .send((
601 .send((
596 directory.to_owned(),
602 directory.to_owned(),
597 dispatch_missing(entry.state),
603 dispatch_missing(entry.state),
598 ))
604 ))
599 .unwrap();
605 .unwrap();
600 }
606 }
601 }
607 }
602 // Do we need to traverse it?
608 // Do we need to traverse it?
603 if !self.is_ignored(&directory) || self.options.list_ignored {
609 if !self.is_ignored(&directory) || self.options.list_ignored {
604 self.traverse_dir(
610 self.traverse_dir(
605 files_sender,
611 files_sender,
606 directory,
612 directory,
607 &old_results,
613 &old_results,
608 traversed_sender,
614 traversed_sender,
609 )
615 )
610 }
616 }
611 });
617 });
612 }
618 }
613
619
614 /// Decides whether the directory needs to be listed, and if so handles the
620 /// Decides whether the directory needs to be listed, and if so handles the
615 /// entries in a separate thread.
621 /// entries in a separate thread.
616 fn traverse_dir(
622 fn traverse_dir(
617 &self,
623 &self,
618 files_sender: &crossbeam_channel::Sender<(HgPathBuf, Dispatch)>,
624 files_sender: &crossbeam_channel::Sender<(HgPathBuf, Dispatch)>,
619 directory: impl AsRef<HgPath>,
625 directory: impl AsRef<HgPath>,
620 old_results: &FastHashMap<Cow<HgPath>, Dispatch>,
626 old_results: &FastHashMap<Cow<HgPath>, Dispatch>,
621 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
627 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
622 ) {
628 ) {
623 let directory = directory.as_ref();
629 let directory = directory.as_ref();
624
630
625 if self.options.collect_traversed_dirs {
631 if self.options.collect_traversed_dirs {
626 traversed_sender
632 traversed_sender
627 .send(directory.to_owned())
633 .send(directory.to_owned())
628 .expect("receiver should outlive sender");
634 .expect("receiver should outlive sender");
629 }
635 }
630
636
631 let visit_entries = match self.matcher.visit_children_set(directory) {
637 let visit_entries = match self.matcher.visit_children_set(directory) {
632 VisitChildrenSet::Empty => return,
638 VisitChildrenSet::Empty => return,
633 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
639 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
634 VisitChildrenSet::Set(set) => Some(set),
640 VisitChildrenSet::Set(set) => Some(set),
635 };
641 };
636 let buf = match hg_path_to_path_buf(directory) {
642 let buf = match hg_path_to_path_buf(directory) {
637 Ok(b) => b,
643 Ok(b) => b,
638 Err(_) => {
644 Err(_) => {
639 files_sender
645 files_sender
640 .send((directory.to_owned(), INVALID_PATH_DISPATCH))
646 .send((directory.to_owned(), INVALID_PATH_DISPATCH))
641 .expect("receiver should outlive sender");
647 .expect("receiver should outlive sender");
642 return;
648 return;
643 }
649 }
644 };
650 };
645 let dir_path = self.root_dir.join(buf);
651 let dir_path = self.root_dir.join(buf);
646
652
647 let skip_dot_hg = !directory.as_bytes().is_empty();
653 let skip_dot_hg = !directory.as_bytes().is_empty();
648 let entries = match list_directory(dir_path, skip_dot_hg) {
654 let entries = match list_directory(dir_path, skip_dot_hg) {
649 Err(e) => {
655 Err(e) => {
650 files_sender
656 files_sender
651 .send((directory.to_owned(), dispatch_os_error(&e)))
657 .send((directory.to_owned(), dispatch_os_error(&e)))
652 .expect("receiver should outlive sender");
658 .expect("receiver should outlive sender");
653 return;
659 return;
654 }
660 }
655 Ok(entries) => entries,
661 Ok(entries) => entries,
656 };
662 };
657
663
658 rayon::scope(|scope| {
664 rayon::scope(|scope| {
659 for (filename, dir_entry) in entries {
665 for (filename, dir_entry) in entries {
660 if let Some(ref set) = visit_entries {
666 if let Some(ref set) = visit_entries {
661 if !set.contains(filename.deref()) {
667 if !set.contains(filename.deref()) {
662 continue;
668 continue;
663 }
669 }
664 }
670 }
665 // TODO normalize
671 // TODO normalize
666 let filename = if directory.is_empty() {
672 let filename = if directory.is_empty() {
667 filename.to_owned()
673 filename.to_owned()
668 } else {
674 } else {
669 directory.join(&filename)
675 directory.join(&filename)
670 };
676 };
671
677
672 if !old_results.contains_key(filename.deref()) {
678 if !old_results.contains_key(filename.deref()) {
673 match self.handle_traversed_entry(
679 match self.handle_traversed_entry(
674 scope,
680 scope,
675 files_sender,
681 files_sender,
676 old_results,
682 old_results,
677 filename,
683 filename,
678 dir_entry,
684 dir_entry,
679 traversed_sender.clone(),
685 traversed_sender.clone(),
680 ) {
686 ) {
681 Err(e) => {
687 Err(e) => {
682 files_sender
688 files_sender
683 .send((
689 .send((
684 directory.to_owned(),
690 directory.to_owned(),
685 dispatch_os_error(&e),
691 dispatch_os_error(&e),
686 ))
692 ))
687 .expect("receiver should outlive sender");
693 .expect("receiver should outlive sender");
688 }
694 }
689 Ok(_) => {}
695 Ok(_) => {}
690 }
696 }
691 }
697 }
692 }
698 }
693 })
699 })
694 }
700 }
695
701
696 /// Add the files in the dirstate to the results.
702 /// Add the files in the dirstate to the results.
697 ///
703 ///
698 /// This takes a mutable reference to the results to account for the
704 /// This takes a mutable reference to the results to account for the
699 /// `extend` in timings
705 /// `extend` in timings
700 #[cfg(feature = "dirstate-tree")]
706 #[cfg(feature = "dirstate-tree")]
701 #[timed]
707 #[timed]
702 pub fn extend_from_dmap(&self, results: &mut Vec<DispatchedPath<'a>>) {
708 pub fn extend_from_dmap(&self, results: &mut Vec<DispatchedPath<'a>>) {
703 results.par_extend(
709 results.par_extend(
704 self.dmap
710 self.dmap
705 .fs_iter(self.root_dir.clone())
711 .fs_iter(self.root_dir.clone())
706 .par_bridge()
712 .par_bridge()
707 .filter(|(path, _)| self.matcher.matches(path))
713 .filter(|(path, _)| self.matcher.matches(path))
708 .map(move |(filename, shortcut)| {
714 .map(move |(filename, shortcut)| {
709 let entry = match shortcut {
715 let entry = match shortcut {
710 StatusShortcut::Entry(e) => e,
716 StatusShortcut::Entry(e) => e,
711 StatusShortcut::Dispatch(d) => {
717 StatusShortcut::Dispatch(d) => {
712 return (Cow::Owned(filename), d)
718 return (Cow::Owned(filename), d)
713 }
719 }
714 };
720 };
715 let filename_as_path = match hg_path_to_path_buf(&filename)
721 let filename_as_path = match hg_path_to_path_buf(&filename)
716 {
722 {
717 Ok(f) => f,
723 Ok(f) => f,
718 Err(_) => {
724 Err(_) => {
719 return (
725 return (
720 Cow::Owned(filename),
726 Cow::Owned(filename),
721 INVALID_PATH_DISPATCH,
727 INVALID_PATH_DISPATCH,
722 )
728 )
723 }
729 }
724 };
730 };
725 let meta = self
731 let meta = self
726 .root_dir
732 .root_dir
727 .join(filename_as_path)
733 .join(filename_as_path)
728 .symlink_metadata();
734 .symlink_metadata();
729
735
730 match meta {
736 match meta {
731 Ok(m)
737 Ok(m)
732 if !(m.file_type().is_file()
738 if !(m.file_type().is_file()
733 || m.file_type().is_symlink()) =>
739 || m.file_type().is_symlink()) =>
734 {
740 {
735 (
741 (
736 Cow::Owned(filename),
742 Cow::Owned(filename),
737 dispatch_missing(entry.state),
743 dispatch_missing(entry.state),
738 )
744 )
739 }
745 }
740 Ok(m) => {
746 Ok(m) => {
741 let dispatch = dispatch_found(
747 let dispatch = dispatch_found(
742 &filename,
748 &filename,
743 entry,
749 entry,
744 HgMetadata::from_metadata(m),
750 HgMetadata::from_metadata(m),
745 &self.dmap.copy_map,
751 &self.dmap.copy_map,
746 self.options,
752 self.options,
747 );
753 );
748 (Cow::Owned(filename), dispatch)
754 (Cow::Owned(filename), dispatch)
749 }
755 }
750 Err(e)
756 Err(e)
751 if e.kind() == ErrorKind::NotFound
757 if e.kind() == ErrorKind::NotFound
752 || e.raw_os_error() == Some(20) =>
758 || e.raw_os_error() == Some(20) =>
753 {
759 {
754 // Rust does not yet have an `ErrorKind` for
760 // Rust does not yet have an `ErrorKind` for
755 // `NotADirectory` (errno 20)
761 // `NotADirectory` (errno 20)
756 // It happens if the dirstate contains `foo/bar`
762 // It happens if the dirstate contains `foo/bar`
757 // and foo is not a
763 // and foo is not a
758 // directory
764 // directory
759 (
765 (
760 Cow::Owned(filename),
766 Cow::Owned(filename),
761 dispatch_missing(entry.state),
767 dispatch_missing(entry.state),
762 )
768 )
763 }
769 }
764 Err(e) => {
770 Err(e) => {
765 (Cow::Owned(filename), dispatch_os_error(&e))
771 (Cow::Owned(filename), dispatch_os_error(&e))
766 }
772 }
767 }
773 }
768 }),
774 }),
769 );
775 );
770 }
776 }
771
777
772 /// Add the files in the dirstate to the results.
778 /// Add the files in the dirstate to the results.
773 ///
779 ///
774 /// This takes a mutable reference to the results to account for the
780 /// This takes a mutable reference to the results to account for the
775 /// `extend` in timings
781 /// `extend` in timings
776 #[cfg(not(feature = "dirstate-tree"))]
782 #[cfg(not(feature = "dirstate-tree"))]
777 #[timed]
783 #[timed]
778 pub fn extend_from_dmap(&self, results: &mut Vec<DispatchedPath<'a>>) {
784 pub fn extend_from_dmap(&self, results: &mut Vec<DispatchedPath<'a>>) {
779 results.par_extend(
785 results.par_extend(
780 self.dmap
786 self.dmap
781 .par_iter()
787 .par_iter()
782 .filter(|(path, _)| self.matcher.matches(path))
788 .filter(|(path, _)| self.matcher.matches(path))
783 .map(move |(filename, entry)| {
789 .map(move |(filename, entry)| {
784 let filename: &HgPath = filename;
790 let filename: &HgPath = filename;
785 let filename_as_path = match hg_path_to_path_buf(filename)
791 let filename_as_path = match hg_path_to_path_buf(filename)
786 {
792 {
787 Ok(f) => f,
793 Ok(f) => f,
788 Err(_) => {
794 Err(_) => {
789 return (
795 return (
790 Cow::Borrowed(filename),
796 Cow::Borrowed(filename),
791 INVALID_PATH_DISPATCH,
797 INVALID_PATH_DISPATCH,
792 )
798 )
793 }
799 }
794 };
800 };
795 let meta = self
801 let meta = self
796 .root_dir
802 .root_dir
797 .join(filename_as_path)
803 .join(filename_as_path)
798 .symlink_metadata();
804 .symlink_metadata();
799 match meta {
805 match meta {
800 Ok(m)
806 Ok(m)
801 if !(m.file_type().is_file()
807 if !(m.file_type().is_file()
802 || m.file_type().is_symlink()) =>
808 || m.file_type().is_symlink()) =>
803 {
809 {
804 (
810 (
805 Cow::Borrowed(filename),
811 Cow::Borrowed(filename),
806 dispatch_missing(entry.state),
812 dispatch_missing(entry.state),
807 )
813 )
808 }
814 }
809 Ok(m) => (
815 Ok(m) => (
810 Cow::Borrowed(filename),
816 Cow::Borrowed(filename),
811 dispatch_found(
817 dispatch_found(
812 filename,
818 filename,
813 *entry,
819 *entry,
814 HgMetadata::from_metadata(m),
820 HgMetadata::from_metadata(m),
815 &self.dmap.copy_map,
821 &self.dmap.copy_map,
816 self.options,
822 self.options,
817 ),
823 ),
818 ),
824 ),
819 Err(e)
825 Err(e)
820 if e.kind() == ErrorKind::NotFound
826 if e.kind() == ErrorKind::NotFound
821 || e.raw_os_error() == Some(20) =>
827 || e.raw_os_error() == Some(20) =>
822 {
828 {
823 // Rust does not yet have an `ErrorKind` for
829 // Rust does not yet have an `ErrorKind` for
824 // `NotADirectory` (errno 20)
830 // `NotADirectory` (errno 20)
825 // It happens if the dirstate contains `foo/bar`
831 // It happens if the dirstate contains `foo/bar`
826 // and foo is not a
832 // and foo is not a
827 // directory
833 // directory
828 (
834 (
829 Cow::Borrowed(filename),
835 Cow::Borrowed(filename),
830 dispatch_missing(entry.state),
836 dispatch_missing(entry.state),
831 )
837 )
832 }
838 }
833 Err(e) => {
839 Err(e) => {
834 (Cow::Borrowed(filename), dispatch_os_error(&e))
840 (Cow::Borrowed(filename), dispatch_os_error(&e))
835 }
841 }
836 }
842 }
837 }),
843 }),
838 );
844 );
839 }
845 }
840
846
841 /// Checks all files that are in the dirstate but were not found during the
847 /// Checks all files that are in the dirstate but were not found during the
842 /// working directory traversal. This means that the rest must
848 /// working directory traversal. This means that the rest must
843 /// be either ignored, under a symlink or under a new nested repo.
849 /// be either ignored, under a symlink or under a new nested repo.
844 ///
850 ///
845 /// This takes a mutable reference to the results to account for the
851 /// This takes a mutable reference to the results to account for the
846 /// `extend` in timings
852 /// `extend` in timings
847 #[cfg(not(feature = "dirstate-tree"))]
853 #[cfg(not(feature = "dirstate-tree"))]
848 #[timed]
854 #[timed]
849 pub fn handle_unknowns(&self, results: &mut Vec<DispatchedPath<'a>>) {
855 pub fn handle_unknowns(&self, results: &mut Vec<DispatchedPath<'a>>) {
850 let to_visit: Vec<(&HgPath, &DirstateEntry)> =
856 let to_visit: Vec<(&HgPath, &DirstateEntry)> =
851 if results.is_empty() && self.matcher.matches_everything() {
857 if results.is_empty() && self.matcher.matches_everything() {
852 self.dmap.iter().map(|(f, e)| (f.deref(), e)).collect()
858 self.dmap.iter().map(|(f, e)| (f.deref(), e)).collect()
853 } else {
859 } else {
854 // Only convert to a hashmap if needed.
860 // Only convert to a hashmap if needed.
855 let old_results: FastHashMap<_, _> =
861 let old_results: FastHashMap<_, _> =
856 results.iter().cloned().collect();
862 results.iter().cloned().collect();
857 self.dmap
863 self.dmap
858 .iter()
864 .iter()
859 .filter_map(move |(f, e)| {
865 .filter_map(move |(f, e)| {
860 if !old_results.contains_key(f.deref())
866 if !old_results.contains_key(f.deref())
861 && self.matcher.matches(f)
867 && self.matcher.matches(f)
862 {
868 {
863 Some((f.deref(), e))
869 Some((f.deref(), e))
864 } else {
870 } else {
865 None
871 None
866 }
872 }
867 })
873 })
868 .collect()
874 .collect()
869 };
875 };
870
876
871 let path_auditor = PathAuditor::new(&self.root_dir);
877 let path_auditor = PathAuditor::new(&self.root_dir);
872
878
873 let new_results = to_visit.into_par_iter().filter_map(
879 let new_results = to_visit.into_par_iter().filter_map(
874 |(filename, entry)| -> Option<_> {
880 |(filename, entry)| -> Option<_> {
875 // Report ignored items in the dmap as long as they are not
881 // Report ignored items in the dmap as long as they are not
876 // under a symlink directory.
882 // under a symlink directory.
877 if path_auditor.check(filename) {
883 if path_auditor.check(filename) {
878 // TODO normalize for case-insensitive filesystems
884 // TODO normalize for case-insensitive filesystems
879 let buf = match hg_path_to_path_buf(filename) {
885 let buf = match hg_path_to_path_buf(filename) {
880 Ok(x) => x,
886 Ok(x) => x,
881 Err(_) => {
887 Err(_) => {
882 return Some((
888 return Some((
883 Cow::Owned(filename.to_owned()),
889 Cow::Owned(filename.to_owned()),
884 INVALID_PATH_DISPATCH,
890 INVALID_PATH_DISPATCH,
885 ));
891 ));
886 }
892 }
887 };
893 };
888 Some((
894 Some((
889 Cow::Owned(filename.to_owned()),
895 Cow::Owned(filename.to_owned()),
890 match self.root_dir.join(&buf).symlink_metadata() {
896 match self.root_dir.join(&buf).symlink_metadata() {
891 // File was just ignored, no links, and exists
897 // File was just ignored, no links, and exists
892 Ok(meta) => {
898 Ok(meta) => {
893 let metadata = HgMetadata::from_metadata(meta);
899 let metadata = HgMetadata::from_metadata(meta);
894 dispatch_found(
900 dispatch_found(
895 filename,
901 filename,
896 *entry,
902 *entry,
897 metadata,
903 metadata,
898 &self.dmap.copy_map,
904 &self.dmap.copy_map,
899 self.options,
905 self.options,
900 )
906 )
901 }
907 }
902 // File doesn't exist
908 // File doesn't exist
903 Err(_) => dispatch_missing(entry.state),
909 Err(_) => dispatch_missing(entry.state),
904 },
910 },
905 ))
911 ))
906 } else {
912 } else {
907 // It's either missing or under a symlink directory which
913 // It's either missing or under a symlink directory which
908 // we, in this case, report as missing.
914 // we, in this case, report as missing.
909 Some((
915 Some((
910 Cow::Owned(filename.to_owned()),
916 Cow::Owned(filename.to_owned()),
911 dispatch_missing(entry.state),
917 dispatch_missing(entry.state),
912 ))
918 ))
913 }
919 }
914 },
920 },
915 );
921 );
916
922
917 results.par_extend(new_results);
923 results.par_extend(new_results);
918 }
924 }
919 }
925 }
920
926
921 #[timed]
927 #[timed]
922 pub fn build_response<'a>(
928 pub fn build_response<'a>(
923 results: impl IntoIterator<Item = DispatchedPath<'a>>,
929 results: impl IntoIterator<Item = DispatchedPath<'a>>,
924 traversed: Vec<HgPathBuf>,
930 traversed: Vec<HgPathBuf>,
925 ) -> (Vec<HgPathCow<'a>>, DirstateStatus<'a>) {
931 ) -> (Vec<HgPathCow<'a>>, DirstateStatus<'a>) {
926 let mut lookup = vec![];
932 let mut lookup = vec![];
927 let mut modified = vec![];
933 let mut modified = vec![];
928 let mut added = vec![];
934 let mut added = vec![];
929 let mut removed = vec![];
935 let mut removed = vec![];
930 let mut deleted = vec![];
936 let mut deleted = vec![];
931 let mut clean = vec![];
937 let mut clean = vec![];
932 let mut ignored = vec![];
938 let mut ignored = vec![];
933 let mut unknown = vec![];
939 let mut unknown = vec![];
934 let mut bad = vec![];
940 let mut bad = vec![];
935
941
936 for (filename, dispatch) in results.into_iter() {
942 for (filename, dispatch) in results.into_iter() {
937 match dispatch {
943 match dispatch {
938 Dispatch::Unknown => unknown.push(filename),
944 Dispatch::Unknown => unknown.push(filename),
939 Dispatch::Unsure => lookup.push(filename),
945 Dispatch::Unsure => lookup.push(filename),
940 Dispatch::Modified => modified.push(filename),
946 Dispatch::Modified => modified.push(filename),
941 Dispatch::Added => added.push(filename),
947 Dispatch::Added => added.push(filename),
942 Dispatch::Removed => removed.push(filename),
948 Dispatch::Removed => removed.push(filename),
943 Dispatch::Deleted => deleted.push(filename),
949 Dispatch::Deleted => deleted.push(filename),
944 Dispatch::Clean => clean.push(filename),
950 Dispatch::Clean => clean.push(filename),
945 Dispatch::Ignored => ignored.push(filename),
951 Dispatch::Ignored => ignored.push(filename),
946 Dispatch::None => {}
952 Dispatch::None => {}
947 Dispatch::Bad(reason) => bad.push((filename, reason)),
953 Dispatch::Bad(reason) => bad.push((filename, reason)),
948 Dispatch::Directory { .. } => {}
954 Dispatch::Directory { .. } => {}
949 }
955 }
950 }
956 }
951
957
952 (
958 (
953 lookup,
959 lookup,
954 DirstateStatus {
960 DirstateStatus {
955 modified,
961 modified,
956 added,
962 added,
957 removed,
963 removed,
958 deleted,
964 deleted,
959 clean,
965 clean,
960 ignored,
966 ignored,
961 unknown,
967 unknown,
962 bad,
968 bad,
963 traversed,
969 traversed,
964 },
970 },
965 )
971 )
966 }
972 }
967
973
968 /// Get the status of files in the working directory.
974 /// Get the status of files in the working directory.
969 ///
975 ///
970 /// This is the current entry-point for `hg-core` and is realistically unusable
976 /// This is the current entry-point for `hg-core` and is realistically unusable
971 /// outside of a Python context because its arguments need to provide a lot of
977 /// outside of a Python context because its arguments need to provide a lot of
972 /// information that will not be necessary in the future.
978 /// information that will not be necessary in the future.
973 #[timed]
979 #[timed]
974 pub fn status<'a>(
980 pub fn status<'a>(
975 dmap: &'a DirstateMap,
981 dmap: &'a DirstateMap,
976 matcher: &'a (impl Matcher + Sync),
982 matcher: &'a (impl Matcher + Sync),
977 root_dir: PathBuf,
983 root_dir: PathBuf,
978 ignore_files: Vec<PathBuf>,
984 ignore_files: Vec<PathBuf>,
979 options: StatusOptions,
985 options: StatusOptions,
980 ) -> StatusResult<(
986 ) -> StatusResult<(
981 (Vec<HgPathCow<'a>>, DirstateStatus<'a>),
987 (Vec<HgPathCow<'a>>, DirstateStatus<'a>),
982 Vec<PatternFileWarning>,
988 Vec<PatternFileWarning>,
983 )> {
989 )> {
984 let (status, warnings) =
990 let (status, warnings) =
985 Status::new(dmap, matcher, root_dir, ignore_files, options)?;
991 Status::new(dmap, matcher, root_dir, ignore_files, options)?;
986
992
987 Ok((status.run()?, warnings))
993 Ok((status.run()?, warnings))
988 }
994 }
@@ -1,451 +1,457 b''
1 // files.rs
1 // files.rs
2 //
2 //
3 // Copyright 2019
3 // Copyright 2019
4 // Raphaël Gomès <rgomes@octobus.net>,
4 // Raphaël Gomès <rgomes@octobus.net>,
5 // Yuya Nishihara <yuya@tcha.org>
5 // Yuya Nishihara <yuya@tcha.org>
6 //
6 //
7 // This software may be used and distributed according to the terms of the
7 // This software may be used and distributed according to the terms of the
8 // GNU General Public License version 2 or any later version.
8 // GNU General Public License version 2 or any later version.
9
9
10 //! Functions for fiddling with files.
10 //! Functions for fiddling with files.
11
11
12 use crate::utils::{
12 use crate::utils::{
13 hg_path::{path_to_hg_path_buf, HgPath, HgPathBuf, HgPathError},
13 hg_path::{path_to_hg_path_buf, HgPath, HgPathBuf, HgPathError},
14 path_auditor::PathAuditor,
14 path_auditor::PathAuditor,
15 replace_slice,
15 replace_slice,
16 };
16 };
17 use lazy_static::lazy_static;
17 use lazy_static::lazy_static;
18 use same_file::is_same_file;
18 use same_file::is_same_file;
19 use std::borrow::{Cow, ToOwned};
19 use std::borrow::{Cow, ToOwned};
20 use std::ffi::OsStr;
20 use std::ffi::OsStr;
21 use std::fs::Metadata;
21 use std::fs::Metadata;
22 use std::iter::FusedIterator;
22 use std::iter::FusedIterator;
23 use std::ops::Deref;
23 use std::ops::Deref;
24 use std::path::{Path, PathBuf};
24 use std::path::{Path, PathBuf};
25
25
26 pub fn get_os_str_from_bytes(bytes: &[u8]) -> &OsStr {
26 pub fn get_os_str_from_bytes(bytes: &[u8]) -> &OsStr {
27 let os_str;
27 let os_str;
28 #[cfg(unix)]
28 #[cfg(unix)]
29 {
29 {
30 use std::os::unix::ffi::OsStrExt;
30 use std::os::unix::ffi::OsStrExt;
31 os_str = std::ffi::OsStr::from_bytes(bytes);
31 os_str = std::ffi::OsStr::from_bytes(bytes);
32 }
32 }
33 // TODO Handle other platforms
33 // TODO Handle other platforms
34 // TODO: convert from WTF8 to Windows MBCS (ANSI encoding).
34 // TODO: convert from WTF8 to Windows MBCS (ANSI encoding).
35 // Perhaps, the return type would have to be Result<PathBuf>.
35 // Perhaps, the return type would have to be Result<PathBuf>.
36 os_str
36 os_str
37 }
37 }
38
38
39 pub fn get_path_from_bytes(bytes: &[u8]) -> &Path {
39 pub fn get_path_from_bytes(bytes: &[u8]) -> &Path {
40 Path::new(get_os_str_from_bytes(bytes))
40 Path::new(get_os_str_from_bytes(bytes))
41 }
41 }
42
42
43 // TODO: need to convert from WTF8 to MBCS bytes on Windows.
43 // TODO: need to convert from WTF8 to MBCS bytes on Windows.
44 // that's why Vec<u8> is returned.
44 // that's why Vec<u8> is returned.
45 #[cfg(unix)]
45 #[cfg(unix)]
46 pub fn get_bytes_from_path(path: impl AsRef<Path>) -> Vec<u8> {
46 pub fn get_bytes_from_path(path: impl AsRef<Path>) -> Vec<u8> {
47 get_bytes_from_os_str(path.as_ref())
47 get_bytes_from_os_str(path.as_ref())
48 }
48 }
49
49
50 #[cfg(unix)]
50 #[cfg(unix)]
51 pub fn get_bytes_from_os_str(str: impl AsRef<OsStr>) -> Vec<u8> {
51 pub fn get_bytes_from_os_str(str: impl AsRef<OsStr>) -> Vec<u8> {
52 use std::os::unix::ffi::OsStrExt;
52 use std::os::unix::ffi::OsStrExt;
53 str.as_ref().as_bytes().to_vec()
53 str.as_ref().as_bytes().to_vec()
54 }
54 }
55
55
56 /// An iterator over repository path yielding itself and its ancestors.
56 /// An iterator over repository path yielding itself and its ancestors.
57 #[derive(Copy, Clone, Debug)]
57 #[derive(Copy, Clone, Debug)]
58 pub struct Ancestors<'a> {
58 pub struct Ancestors<'a> {
59 next: Option<&'a HgPath>,
59 next: Option<&'a HgPath>,
60 }
60 }
61
61
62 impl<'a> Iterator for Ancestors<'a> {
62 impl<'a> Iterator for Ancestors<'a> {
63 type Item = &'a HgPath;
63 type Item = &'a HgPath;
64
64
65 fn next(&mut self) -> Option<Self::Item> {
65 fn next(&mut self) -> Option<Self::Item> {
66 let next = self.next;
66 let next = self.next;
67 self.next = match self.next {
67 self.next = match self.next {
68 Some(s) if s.is_empty() => None,
68 Some(s) if s.is_empty() => None,
69 Some(s) => {
69 Some(s) => {
70 let p = s.bytes().rposition(|c| *c == b'/').unwrap_or(0);
70 let p = s.bytes().rposition(|c| *c == b'/').unwrap_or(0);
71 Some(HgPath::new(&s.as_bytes()[..p]))
71 Some(HgPath::new(&s.as_bytes()[..p]))
72 }
72 }
73 None => None,
73 None => None,
74 };
74 };
75 next
75 next
76 }
76 }
77 }
77 }
78
78
79 impl<'a> FusedIterator for Ancestors<'a> {}
79 impl<'a> FusedIterator for Ancestors<'a> {}
80
80
81 /// An iterator over repository path yielding itself and its ancestors.
81 /// An iterator over repository path yielding itself and its ancestors.
82 #[derive(Copy, Clone, Debug)]
82 #[derive(Copy, Clone, Debug)]
83 pub(crate) struct AncestorsWithBase<'a> {
83 pub(crate) struct AncestorsWithBase<'a> {
84 next: Option<(&'a HgPath, &'a HgPath)>,
84 next: Option<(&'a HgPath, &'a HgPath)>,
85 }
85 }
86
86
87 impl<'a> Iterator for AncestorsWithBase<'a> {
87 impl<'a> Iterator for AncestorsWithBase<'a> {
88 type Item = (&'a HgPath, &'a HgPath);
88 type Item = (&'a HgPath, &'a HgPath);
89
89
90 fn next(&mut self) -> Option<Self::Item> {
90 fn next(&mut self) -> Option<Self::Item> {
91 let next = self.next;
91 let next = self.next;
92 self.next = match self.next {
92 self.next = match self.next {
93 Some((s, _)) if s.is_empty() => None,
93 Some((s, _)) if s.is_empty() => None,
94 Some((s, _)) => Some(s.split_filename()),
94 Some((s, _)) => Some(s.split_filename()),
95 None => None,
95 None => None,
96 };
96 };
97 next
97 next
98 }
98 }
99 }
99 }
100
100
101 impl<'a> FusedIterator for AncestorsWithBase<'a> {}
101 impl<'a> FusedIterator for AncestorsWithBase<'a> {}
102
102
103 /// Returns an iterator yielding ancestor directories of the given repository
103 /// Returns an iterator yielding ancestor directories of the given repository
104 /// path.
104 /// path.
105 ///
105 ///
106 /// The path is separated by '/', and must not start with '/'.
106 /// The path is separated by '/', and must not start with '/'.
107 ///
107 ///
108 /// The path itself isn't included unless it is b"" (meaning the root
108 /// The path itself isn't included unless it is b"" (meaning the root
109 /// directory.)
109 /// directory.)
110 pub fn find_dirs(path: &HgPath) -> Ancestors {
110 pub fn find_dirs(path: &HgPath) -> Ancestors {
111 let mut dirs = Ancestors { next: Some(path) };
111 let mut dirs = Ancestors { next: Some(path) };
112 if !path.is_empty() {
112 if !path.is_empty() {
113 dirs.next(); // skip itself
113 dirs.next(); // skip itself
114 }
114 }
115 dirs
115 dirs
116 }
116 }
117
117
118 /// Returns an iterator yielding ancestor directories of the given repository
118 /// Returns an iterator yielding ancestor directories of the given repository
119 /// path.
119 /// path.
120 ///
120 ///
121 /// The path is separated by '/', and must not start with '/'.
121 /// The path is separated by '/', and must not start with '/'.
122 ///
122 ///
123 /// The path itself isn't included unless it is b"" (meaning the root
123 /// The path itself isn't included unless it is b"" (meaning the root
124 /// directory.)
124 /// directory.)
125 pub(crate) fn find_dirs_with_base(path: &HgPath) -> AncestorsWithBase {
125 pub(crate) fn find_dirs_with_base(path: &HgPath) -> AncestorsWithBase {
126 let mut dirs = AncestorsWithBase {
126 let mut dirs = AncestorsWithBase {
127 next: Some((path, HgPath::new(b""))),
127 next: Some((path, HgPath::new(b""))),
128 };
128 };
129 if !path.is_empty() {
129 if !path.is_empty() {
130 dirs.next(); // skip itself
130 dirs.next(); // skip itself
131 }
131 }
132 dirs
132 dirs
133 }
133 }
134
134
135 /// TODO more than ASCII?
135 /// TODO more than ASCII?
136 pub fn normalize_case(path: &HgPath) -> HgPathBuf {
136 pub fn normalize_case(path: &HgPath) -> HgPathBuf {
137 #[cfg(windows)] // NTFS compares via upper()
137 #[cfg(windows)] // NTFS compares via upper()
138 return path.to_ascii_uppercase();
138 return path.to_ascii_uppercase();
139 #[cfg(unix)]
139 #[cfg(unix)]
140 path.to_ascii_lowercase()
140 path.to_ascii_lowercase()
141 }
141 }
142
142
143 lazy_static! {
143 lazy_static! {
144 static ref IGNORED_CHARS: Vec<Vec<u8>> = {
144 static ref IGNORED_CHARS: Vec<Vec<u8>> = {
145 [
145 [
146 0x200c, 0x200d, 0x200e, 0x200f, 0x202a, 0x202b, 0x202c, 0x202d,
146 0x200c, 0x200d, 0x200e, 0x200f, 0x202a, 0x202b, 0x202c, 0x202d,
147 0x202e, 0x206a, 0x206b, 0x206c, 0x206d, 0x206e, 0x206f, 0xfeff,
147 0x202e, 0x206a, 0x206b, 0x206c, 0x206d, 0x206e, 0x206f, 0xfeff,
148 ]
148 ]
149 .iter()
149 .iter()
150 .map(|code| {
150 .map(|code| {
151 std::char::from_u32(*code)
151 std::char::from_u32(*code)
152 .unwrap()
152 .unwrap()
153 .encode_utf8(&mut [0; 3])
153 .encode_utf8(&mut [0; 3])
154 .bytes()
154 .bytes()
155 .collect()
155 .collect()
156 })
156 })
157 .collect()
157 .collect()
158 };
158 };
159 }
159 }
160
160
161 fn hfs_ignore_clean(bytes: &[u8]) -> Vec<u8> {
161 fn hfs_ignore_clean(bytes: &[u8]) -> Vec<u8> {
162 let mut buf = bytes.to_owned();
162 let mut buf = bytes.to_owned();
163 let needs_escaping = bytes.iter().any(|b| *b == b'\xe2' || *b == b'\xef');
163 let needs_escaping = bytes.iter().any(|b| *b == b'\xe2' || *b == b'\xef');
164 if needs_escaping {
164 if needs_escaping {
165 for forbidden in IGNORED_CHARS.iter() {
165 for forbidden in IGNORED_CHARS.iter() {
166 replace_slice(&mut buf, forbidden, &[])
166 replace_slice(&mut buf, forbidden, &[])
167 }
167 }
168 buf
168 buf
169 } else {
169 } else {
170 buf
170 buf
171 }
171 }
172 }
172 }
173
173
174 pub fn lower_clean(bytes: &[u8]) -> Vec<u8> {
174 pub fn lower_clean(bytes: &[u8]) -> Vec<u8> {
175 hfs_ignore_clean(&bytes.to_ascii_lowercase())
175 hfs_ignore_clean(&bytes.to_ascii_lowercase())
176 }
176 }
177
177
178 #[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)]
178 #[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)]
179 pub struct HgMetadata {
179 pub struct HgMetadata {
180 pub st_dev: u64,
180 pub st_dev: u64,
181 pub st_mode: u32,
181 pub st_mode: u32,
182 pub st_nlink: u64,
182 pub st_nlink: u64,
183 pub st_size: u64,
183 pub st_size: u64,
184 pub st_mtime: i64,
184 pub st_mtime: i64,
185 pub st_ctime: i64,
185 pub st_ctime: i64,
186 }
186 }
187
187
188 // TODO support other plaforms
188 // TODO support other plaforms
189 #[cfg(unix)]
189 #[cfg(unix)]
190 impl HgMetadata {
190 impl HgMetadata {
191 pub fn from_metadata(metadata: Metadata) -> Self {
191 pub fn from_metadata(metadata: Metadata) -> Self {
192 use std::os::unix::fs::MetadataExt;
192 use std::os::unix::fs::MetadataExt;
193 Self {
193 Self {
194 st_dev: metadata.dev(),
194 st_dev: metadata.dev(),
195 st_mode: metadata.mode(),
195 st_mode: metadata.mode(),
196 st_nlink: metadata.nlink(),
196 st_nlink: metadata.nlink(),
197 st_size: metadata.size(),
197 st_size: metadata.size(),
198 st_mtime: metadata.mtime(),
198 st_mtime: metadata.mtime(),
199 st_ctime: metadata.ctime(),
199 st_ctime: metadata.ctime(),
200 }
200 }
201 }
201 }
202
203 pub fn is_symlink(&self) -> bool {
204 // This is way too manual, but `HgMetadata` will go away in the
205 // near-future dirstate rewrite anyway.
206 self.st_mode & 0170000 == 0120000
207 }
202 }
208 }
203
209
204 /// Returns the canonical path of `name`, given `cwd` and `root`
210 /// Returns the canonical path of `name`, given `cwd` and `root`
205 pub fn canonical_path(
211 pub fn canonical_path(
206 root: impl AsRef<Path>,
212 root: impl AsRef<Path>,
207 cwd: impl AsRef<Path>,
213 cwd: impl AsRef<Path>,
208 name: impl AsRef<Path>,
214 name: impl AsRef<Path>,
209 ) -> Result<PathBuf, HgPathError> {
215 ) -> Result<PathBuf, HgPathError> {
210 // TODO add missing normalization for other platforms
216 // TODO add missing normalization for other platforms
211 let root = root.as_ref();
217 let root = root.as_ref();
212 let cwd = cwd.as_ref();
218 let cwd = cwd.as_ref();
213 let name = name.as_ref();
219 let name = name.as_ref();
214
220
215 let name = if !name.is_absolute() {
221 let name = if !name.is_absolute() {
216 root.join(&cwd).join(&name)
222 root.join(&cwd).join(&name)
217 } else {
223 } else {
218 name.to_owned()
224 name.to_owned()
219 };
225 };
220 let auditor = PathAuditor::new(&root);
226 let auditor = PathAuditor::new(&root);
221 if name != root && name.starts_with(&root) {
227 if name != root && name.starts_with(&root) {
222 let name = name.strip_prefix(&root).unwrap();
228 let name = name.strip_prefix(&root).unwrap();
223 auditor.audit_path(path_to_hg_path_buf(name)?)?;
229 auditor.audit_path(path_to_hg_path_buf(name)?)?;
224 Ok(name.to_owned())
230 Ok(name.to_owned())
225 } else if name == root {
231 } else if name == root {
226 Ok("".into())
232 Ok("".into())
227 } else {
233 } else {
228 // Determine whether `name' is in the hierarchy at or beneath `root',
234 // Determine whether `name' is in the hierarchy at or beneath `root',
229 // by iterating name=name.parent() until it returns `None` (can't
235 // by iterating name=name.parent() until it returns `None` (can't
230 // check name == '/', because that doesn't work on windows).
236 // check name == '/', because that doesn't work on windows).
231 let mut name = name.deref();
237 let mut name = name.deref();
232 let original_name = name.to_owned();
238 let original_name = name.to_owned();
233 loop {
239 loop {
234 let same = is_same_file(&name, &root).unwrap_or(false);
240 let same = is_same_file(&name, &root).unwrap_or(false);
235 if same {
241 if same {
236 if name == original_name {
242 if name == original_name {
237 // `name` was actually the same as root (maybe a symlink)
243 // `name` was actually the same as root (maybe a symlink)
238 return Ok("".into());
244 return Ok("".into());
239 }
245 }
240 // `name` is a symlink to root, so `original_name` is under
246 // `name` is a symlink to root, so `original_name` is under
241 // root
247 // root
242 let rel_path = original_name.strip_prefix(&name).unwrap();
248 let rel_path = original_name.strip_prefix(&name).unwrap();
243 auditor.audit_path(path_to_hg_path_buf(&rel_path)?)?;
249 auditor.audit_path(path_to_hg_path_buf(&rel_path)?)?;
244 return Ok(rel_path.to_owned());
250 return Ok(rel_path.to_owned());
245 }
251 }
246 name = match name.parent() {
252 name = match name.parent() {
247 None => break,
253 None => break,
248 Some(p) => p,
254 Some(p) => p,
249 };
255 };
250 }
256 }
251 // TODO hint to the user about using --cwd
257 // TODO hint to the user about using --cwd
252 // Bubble up the responsibility to Python for now
258 // Bubble up the responsibility to Python for now
253 Err(HgPathError::NotUnderRoot {
259 Err(HgPathError::NotUnderRoot {
254 path: original_name.to_owned(),
260 path: original_name.to_owned(),
255 root: root.to_owned(),
261 root: root.to_owned(),
256 })
262 })
257 }
263 }
258 }
264 }
259
265
260 /// Returns the representation of the path relative to the current working
266 /// Returns the representation of the path relative to the current working
261 /// directory for display purposes.
267 /// directory for display purposes.
262 ///
268 ///
263 /// `cwd` is a `HgPath`, so it is considered relative to the root directory
269 /// `cwd` is a `HgPath`, so it is considered relative to the root directory
264 /// of the repository.
270 /// of the repository.
265 ///
271 ///
266 /// # Examples
272 /// # Examples
267 ///
273 ///
268 /// ```
274 /// ```
269 /// use hg::utils::hg_path::HgPath;
275 /// use hg::utils::hg_path::HgPath;
270 /// use hg::utils::files::relativize_path;
276 /// use hg::utils::files::relativize_path;
271 /// use std::borrow::Cow;
277 /// use std::borrow::Cow;
272 ///
278 ///
273 /// let file = HgPath::new(b"nested/file");
279 /// let file = HgPath::new(b"nested/file");
274 /// let cwd = HgPath::new(b"");
280 /// let cwd = HgPath::new(b"");
275 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"nested/file"));
281 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"nested/file"));
276 ///
282 ///
277 /// let cwd = HgPath::new(b"nested");
283 /// let cwd = HgPath::new(b"nested");
278 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"file"));
284 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"file"));
279 ///
285 ///
280 /// let cwd = HgPath::new(b"other");
286 /// let cwd = HgPath::new(b"other");
281 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"../nested/file"));
287 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"../nested/file"));
282 /// ```
288 /// ```
283 pub fn relativize_path(path: &HgPath, cwd: impl AsRef<HgPath>) -> Cow<[u8]> {
289 pub fn relativize_path(path: &HgPath, cwd: impl AsRef<HgPath>) -> Cow<[u8]> {
284 if cwd.as_ref().is_empty() {
290 if cwd.as_ref().is_empty() {
285 Cow::Borrowed(path.as_bytes())
291 Cow::Borrowed(path.as_bytes())
286 } else {
292 } else {
287 let mut res: Vec<u8> = Vec::new();
293 let mut res: Vec<u8> = Vec::new();
288 let mut path_iter = path.as_bytes().split(|b| *b == b'/').peekable();
294 let mut path_iter = path.as_bytes().split(|b| *b == b'/').peekable();
289 let mut cwd_iter =
295 let mut cwd_iter =
290 cwd.as_ref().as_bytes().split(|b| *b == b'/').peekable();
296 cwd.as_ref().as_bytes().split(|b| *b == b'/').peekable();
291 loop {
297 loop {
292 match (path_iter.peek(), cwd_iter.peek()) {
298 match (path_iter.peek(), cwd_iter.peek()) {
293 (Some(a), Some(b)) if a == b => (),
299 (Some(a), Some(b)) if a == b => (),
294 _ => break,
300 _ => break,
295 }
301 }
296 path_iter.next();
302 path_iter.next();
297 cwd_iter.next();
303 cwd_iter.next();
298 }
304 }
299 let mut need_sep = false;
305 let mut need_sep = false;
300 for _ in cwd_iter {
306 for _ in cwd_iter {
301 if need_sep {
307 if need_sep {
302 res.extend(b"/")
308 res.extend(b"/")
303 } else {
309 } else {
304 need_sep = true
310 need_sep = true
305 };
311 };
306 res.extend(b"..");
312 res.extend(b"..");
307 }
313 }
308 for c in path_iter {
314 for c in path_iter {
309 if need_sep {
315 if need_sep {
310 res.extend(b"/")
316 res.extend(b"/")
311 } else {
317 } else {
312 need_sep = true
318 need_sep = true
313 };
319 };
314 res.extend(c);
320 res.extend(c);
315 }
321 }
316 Cow::Owned(res)
322 Cow::Owned(res)
317 }
323 }
318 }
324 }
319
325
320 #[cfg(test)]
326 #[cfg(test)]
321 mod tests {
327 mod tests {
322 use super::*;
328 use super::*;
323 use pretty_assertions::assert_eq;
329 use pretty_assertions::assert_eq;
324
330
325 #[test]
331 #[test]
326 fn find_dirs_some() {
332 fn find_dirs_some() {
327 let mut dirs = super::find_dirs(HgPath::new(b"foo/bar/baz"));
333 let mut dirs = super::find_dirs(HgPath::new(b"foo/bar/baz"));
328 assert_eq!(dirs.next(), Some(HgPath::new(b"foo/bar")));
334 assert_eq!(dirs.next(), Some(HgPath::new(b"foo/bar")));
329 assert_eq!(dirs.next(), Some(HgPath::new(b"foo")));
335 assert_eq!(dirs.next(), Some(HgPath::new(b"foo")));
330 assert_eq!(dirs.next(), Some(HgPath::new(b"")));
336 assert_eq!(dirs.next(), Some(HgPath::new(b"")));
331 assert_eq!(dirs.next(), None);
337 assert_eq!(dirs.next(), None);
332 assert_eq!(dirs.next(), None);
338 assert_eq!(dirs.next(), None);
333 }
339 }
334
340
335 #[test]
341 #[test]
336 fn find_dirs_empty() {
342 fn find_dirs_empty() {
337 // looks weird, but mercurial.pathutil.finddirs(b"") yields b""
343 // looks weird, but mercurial.pathutil.finddirs(b"") yields b""
338 let mut dirs = super::find_dirs(HgPath::new(b""));
344 let mut dirs = super::find_dirs(HgPath::new(b""));
339 assert_eq!(dirs.next(), Some(HgPath::new(b"")));
345 assert_eq!(dirs.next(), Some(HgPath::new(b"")));
340 assert_eq!(dirs.next(), None);
346 assert_eq!(dirs.next(), None);
341 assert_eq!(dirs.next(), None);
347 assert_eq!(dirs.next(), None);
342 }
348 }
343
349
344 #[test]
350 #[test]
345 fn test_find_dirs_with_base_some() {
351 fn test_find_dirs_with_base_some() {
346 let mut dirs = super::find_dirs_with_base(HgPath::new(b"foo/bar/baz"));
352 let mut dirs = super::find_dirs_with_base(HgPath::new(b"foo/bar/baz"));
347 assert_eq!(
353 assert_eq!(
348 dirs.next(),
354 dirs.next(),
349 Some((HgPath::new(b"foo/bar"), HgPath::new(b"baz")))
355 Some((HgPath::new(b"foo/bar"), HgPath::new(b"baz")))
350 );
356 );
351 assert_eq!(
357 assert_eq!(
352 dirs.next(),
358 dirs.next(),
353 Some((HgPath::new(b"foo"), HgPath::new(b"bar")))
359 Some((HgPath::new(b"foo"), HgPath::new(b"bar")))
354 );
360 );
355 assert_eq!(dirs.next(), Some((HgPath::new(b""), HgPath::new(b"foo"))));
361 assert_eq!(dirs.next(), Some((HgPath::new(b""), HgPath::new(b"foo"))));
356 assert_eq!(dirs.next(), None);
362 assert_eq!(dirs.next(), None);
357 assert_eq!(dirs.next(), None);
363 assert_eq!(dirs.next(), None);
358 }
364 }
359
365
360 #[test]
366 #[test]
361 fn test_find_dirs_with_base_empty() {
367 fn test_find_dirs_with_base_empty() {
362 let mut dirs = super::find_dirs_with_base(HgPath::new(b""));
368 let mut dirs = super::find_dirs_with_base(HgPath::new(b""));
363 assert_eq!(dirs.next(), Some((HgPath::new(b""), HgPath::new(b""))));
369 assert_eq!(dirs.next(), Some((HgPath::new(b""), HgPath::new(b""))));
364 assert_eq!(dirs.next(), None);
370 assert_eq!(dirs.next(), None);
365 assert_eq!(dirs.next(), None);
371 assert_eq!(dirs.next(), None);
366 }
372 }
367
373
368 #[test]
374 #[test]
369 fn test_canonical_path() {
375 fn test_canonical_path() {
370 let root = Path::new("/repo");
376 let root = Path::new("/repo");
371 let cwd = Path::new("/dir");
377 let cwd = Path::new("/dir");
372 let name = Path::new("filename");
378 let name = Path::new("filename");
373 assert_eq!(
379 assert_eq!(
374 canonical_path(root, cwd, name),
380 canonical_path(root, cwd, name),
375 Err(HgPathError::NotUnderRoot {
381 Err(HgPathError::NotUnderRoot {
376 path: PathBuf::from("/dir/filename"),
382 path: PathBuf::from("/dir/filename"),
377 root: root.to_path_buf()
383 root: root.to_path_buf()
378 })
384 })
379 );
385 );
380
386
381 let root = Path::new("/repo");
387 let root = Path::new("/repo");
382 let cwd = Path::new("/");
388 let cwd = Path::new("/");
383 let name = Path::new("filename");
389 let name = Path::new("filename");
384 assert_eq!(
390 assert_eq!(
385 canonical_path(root, cwd, name),
391 canonical_path(root, cwd, name),
386 Err(HgPathError::NotUnderRoot {
392 Err(HgPathError::NotUnderRoot {
387 path: PathBuf::from("/filename"),
393 path: PathBuf::from("/filename"),
388 root: root.to_path_buf()
394 root: root.to_path_buf()
389 })
395 })
390 );
396 );
391
397
392 let root = Path::new("/repo");
398 let root = Path::new("/repo");
393 let cwd = Path::new("/");
399 let cwd = Path::new("/");
394 let name = Path::new("repo/filename");
400 let name = Path::new("repo/filename");
395 assert_eq!(
401 assert_eq!(
396 canonical_path(root, cwd, name),
402 canonical_path(root, cwd, name),
397 Ok(PathBuf::from("filename"))
403 Ok(PathBuf::from("filename"))
398 );
404 );
399
405
400 let root = Path::new("/repo");
406 let root = Path::new("/repo");
401 let cwd = Path::new("/repo");
407 let cwd = Path::new("/repo");
402 let name = Path::new("filename");
408 let name = Path::new("filename");
403 assert_eq!(
409 assert_eq!(
404 canonical_path(root, cwd, name),
410 canonical_path(root, cwd, name),
405 Ok(PathBuf::from("filename"))
411 Ok(PathBuf::from("filename"))
406 );
412 );
407
413
408 let root = Path::new("/repo");
414 let root = Path::new("/repo");
409 let cwd = Path::new("/repo/subdir");
415 let cwd = Path::new("/repo/subdir");
410 let name = Path::new("filename");
416 let name = Path::new("filename");
411 assert_eq!(
417 assert_eq!(
412 canonical_path(root, cwd, name),
418 canonical_path(root, cwd, name),
413 Ok(PathBuf::from("subdir/filename"))
419 Ok(PathBuf::from("subdir/filename"))
414 );
420 );
415 }
421 }
416
422
417 #[test]
423 #[test]
418 fn test_canonical_path_not_rooted() {
424 fn test_canonical_path_not_rooted() {
419 use std::fs::create_dir;
425 use std::fs::create_dir;
420 use tempfile::tempdir;
426 use tempfile::tempdir;
421
427
422 let base_dir = tempdir().unwrap();
428 let base_dir = tempdir().unwrap();
423 let base_dir_path = base_dir.path();
429 let base_dir_path = base_dir.path();
424 let beneath_repo = base_dir_path.join("a");
430 let beneath_repo = base_dir_path.join("a");
425 let root = base_dir_path.join("a/b");
431 let root = base_dir_path.join("a/b");
426 let out_of_repo = base_dir_path.join("c");
432 let out_of_repo = base_dir_path.join("c");
427 let under_repo_symlink = out_of_repo.join("d");
433 let under_repo_symlink = out_of_repo.join("d");
428
434
429 create_dir(&beneath_repo).unwrap();
435 create_dir(&beneath_repo).unwrap();
430 create_dir(&root).unwrap();
436 create_dir(&root).unwrap();
431
437
432 // TODO make portable
438 // TODO make portable
433 std::os::unix::fs::symlink(&root, &out_of_repo).unwrap();
439 std::os::unix::fs::symlink(&root, &out_of_repo).unwrap();
434
440
435 assert_eq!(
441 assert_eq!(
436 canonical_path(&root, Path::new(""), out_of_repo),
442 canonical_path(&root, Path::new(""), out_of_repo),
437 Ok(PathBuf::from(""))
443 Ok(PathBuf::from(""))
438 );
444 );
439 assert_eq!(
445 assert_eq!(
440 canonical_path(&root, Path::new(""), &beneath_repo),
446 canonical_path(&root, Path::new(""), &beneath_repo),
441 Err(HgPathError::NotUnderRoot {
447 Err(HgPathError::NotUnderRoot {
442 path: beneath_repo.to_owned(),
448 path: beneath_repo.to_owned(),
443 root: root.to_owned()
449 root: root.to_owned()
444 })
450 })
445 );
451 );
446 assert_eq!(
452 assert_eq!(
447 canonical_path(&root, Path::new(""), &under_repo_symlink),
453 canonical_path(&root, Path::new(""), &under_repo_symlink),
448 Ok(PathBuf::from("d"))
454 Ok(PathBuf::from("d"))
449 );
455 );
450 }
456 }
451 }
457 }
General Comments 0
You need to be logged in to leave comments. Login now