##// END OF EJS Templates
rust-status: check for '.hg' regardless of file type (issue6300)...
Raphaël Gomès -
r45250:a467416c stable
parent child Browse files
Show More
@@ -1,910 +1,913 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, PathBuf},
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
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".
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>;
99 type IgnoreFnType<'a> = Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>;
100
100
101 /// 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
102 /// 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
103 /// 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
104 /// changes. (issue2608)
104 /// changes. (issue2608)
105 /// 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>`
106 /// 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
107 /// caller to cast `b` as `i32`.
107 /// caller to cast `b` as `i32`.
108 fn mod_compare(a: i32, b: i32) -> bool {
108 fn mod_compare(a: i32, b: i32) -> bool {
109 a & i32::max_value() != b & i32::max_value()
109 a & i32::max_value() != b & i32::max_value()
110 }
110 }
111
111
112 /// Return a sorted list containing information about the entries
112 /// Return a sorted list containing information about the entries
113 /// in the directory.
113 /// in the directory.
114 ///
114 ///
115 /// * `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
116 fn list_directory(
116 fn list_directory(
117 path: impl AsRef<Path>,
117 path: impl AsRef<Path>,
118 skip_dot_hg: bool,
118 skip_dot_hg: bool,
119 ) -> std::io::Result<Vec<(HgPathBuf, DirEntry)>> {
119 ) -> std::io::Result<Vec<(HgPathBuf, DirEntry)>> {
120 let mut results = vec![];
120 let mut results = vec![];
121 let entries = read_dir(path.as_ref())?;
121 let entries = read_dir(path.as_ref())?;
122
122
123 for entry in entries {
123 for entry in entries {
124 let entry = entry?;
124 let entry = entry?;
125 let filename = os_string_to_hg_path_buf(entry.file_name())?;
125 let filename = os_string_to_hg_path_buf(entry.file_name())?;
126 let file_type = entry.file_type()?;
126 let file_type = entry.file_type()?;
127 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() {
128 return Ok(vec![]);
128 return Ok(vec![]);
129 } else {
129 } else {
130 results.push((HgPathBuf::from(filename), entry))
130 results.push((HgPathBuf::from(filename), entry))
131 }
131 }
132 }
132 }
133
133
134 results.sort_unstable_by_key(|e| e.0.clone());
134 results.sort_unstable_by_key(|e| e.0.clone());
135 Ok(results)
135 Ok(results)
136 }
136 }
137
137
138 /// 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.
139 fn dispatch_found(
139 fn dispatch_found(
140 filename: impl AsRef<HgPath>,
140 filename: impl AsRef<HgPath>,
141 entry: DirstateEntry,
141 entry: DirstateEntry,
142 metadata: HgMetadata,
142 metadata: HgMetadata,
143 copy_map: &CopyMap,
143 copy_map: &CopyMap,
144 options: StatusOptions,
144 options: StatusOptions,
145 ) -> Dispatch {
145 ) -> Dispatch {
146 let DirstateEntry {
146 let DirstateEntry {
147 state,
147 state,
148 mode,
148 mode,
149 mtime,
149 mtime,
150 size,
150 size,
151 } = entry;
151 } = entry;
152
152
153 let HgMetadata {
153 let HgMetadata {
154 st_mode,
154 st_mode,
155 st_size,
155 st_size,
156 st_mtime,
156 st_mtime,
157 ..
157 ..
158 } = metadata;
158 } = metadata;
159
159
160 match state {
160 match state {
161 EntryState::Normal => {
161 EntryState::Normal => {
162 let size_changed = mod_compare(size, st_size as i32);
162 let size_changed = mod_compare(size, st_size as i32);
163 let mode_changed =
163 let mode_changed =
164 (mode ^ st_mode as i32) & 0o100 != 0o000 && options.check_exec;
164 (mode ^ st_mode as i32) & 0o100 != 0o000 && options.check_exec;
165 let metadata_changed = size >= 0 && (size_changed || mode_changed);
165 let metadata_changed = size >= 0 && (size_changed || mode_changed);
166 let other_parent = size == SIZE_FROM_OTHER_PARENT;
166 let other_parent = size == SIZE_FROM_OTHER_PARENT;
167 if metadata_changed
167 if metadata_changed
168 || other_parent
168 || other_parent
169 || copy_map.contains_key(filename.as_ref())
169 || copy_map.contains_key(filename.as_ref())
170 {
170 {
171 Dispatch::Modified
171 Dispatch::Modified
172 } else if mod_compare(mtime, st_mtime as i32) {
172 } else if mod_compare(mtime, st_mtime as i32) {
173 Dispatch::Unsure
173 Dispatch::Unsure
174 } else if st_mtime == options.last_normal_time {
174 } else if st_mtime == options.last_normal_time {
175 // the file may have just been marked as normal and
175 // the file may have just been marked as normal and
176 // it may have changed in the same second without
176 // it may have changed in the same second without
177 // changing its size. This can happen if we quickly
177 // changing its size. This can happen if we quickly
178 // do multiple commits. Force lookup, so we don't
178 // do multiple commits. Force lookup, so we don't
179 // miss such a racy file change.
179 // miss such a racy file change.
180 Dispatch::Unsure
180 Dispatch::Unsure
181 } else if options.list_clean {
181 } else if options.list_clean {
182 Dispatch::Clean
182 Dispatch::Clean
183 } else {
183 } else {
184 Dispatch::None
184 Dispatch::None
185 }
185 }
186 }
186 }
187 EntryState::Merged => Dispatch::Modified,
187 EntryState::Merged => Dispatch::Modified,
188 EntryState::Added => Dispatch::Added,
188 EntryState::Added => Dispatch::Added,
189 EntryState::Removed => Dispatch::Removed,
189 EntryState::Removed => Dispatch::Removed,
190 EntryState::Unknown => Dispatch::Unknown,
190 EntryState::Unknown => Dispatch::Unknown,
191 }
191 }
192 }
192 }
193
193
194 /// The file corresponding to this Dirstate entry is missing.
194 /// The file corresponding to this Dirstate entry is missing.
195 fn dispatch_missing(state: EntryState) -> Dispatch {
195 fn dispatch_missing(state: EntryState) -> Dispatch {
196 match state {
196 match state {
197 // File was removed from the filesystem during commands
197 // File was removed from the filesystem during commands
198 EntryState::Normal | EntryState::Merged | EntryState::Added => {
198 EntryState::Normal | EntryState::Merged | EntryState::Added => {
199 Dispatch::Deleted
199 Dispatch::Deleted
200 }
200 }
201 // File was removed, everything is normal
201 // File was removed, everything is normal
202 EntryState::Removed => Dispatch::Removed,
202 EntryState::Removed => Dispatch::Removed,
203 // File is unknown to Mercurial, everything is normal
203 // File is unknown to Mercurial, everything is normal
204 EntryState::Unknown => Dispatch::Unknown,
204 EntryState::Unknown => Dispatch::Unknown,
205 }
205 }
206 }
206 }
207
207
208 lazy_static! {
208 lazy_static! {
209 static ref DEFAULT_WORK: HashSet<&'static HgPath> = {
209 static ref DEFAULT_WORK: HashSet<&'static HgPath> = {
210 let mut h = HashSet::new();
210 let mut h = HashSet::new();
211 h.insert(HgPath::new(b""));
211 h.insert(HgPath::new(b""));
212 h
212 h
213 };
213 };
214 }
214 }
215
215
216 /// Get stat data about the files explicitly specified by match.
216 /// Get stat data about the files explicitly specified by match.
217 /// TODO subrepos
217 /// TODO subrepos
218 #[timed]
218 #[timed]
219 fn walk_explicit<'a>(
219 fn walk_explicit<'a>(
220 files: Option<&'a HashSet<&HgPath>>,
220 files: Option<&'a HashSet<&HgPath>>,
221 dmap: &'a DirstateMap,
221 dmap: &'a DirstateMap,
222 root_dir: impl AsRef<Path> + Sync + Send + 'a,
222 root_dir: impl AsRef<Path> + Sync + Send + 'a,
223 options: StatusOptions,
223 options: StatusOptions,
224 ) -> impl ParallelIterator<Item = IoResult<(&'a HgPath, Dispatch)>> {
224 ) -> impl ParallelIterator<Item = IoResult<(&'a HgPath, Dispatch)>> {
225 files
225 files
226 .unwrap_or(&DEFAULT_WORK)
226 .unwrap_or(&DEFAULT_WORK)
227 .par_iter()
227 .par_iter()
228 .map(move |filename| {
228 .map(move |filename| {
229 // TODO normalization
229 // TODO normalization
230 let normalized = filename.as_ref();
230 let normalized = filename.as_ref();
231
231
232 let buf = match hg_path_to_path_buf(normalized) {
232 let buf = match hg_path_to_path_buf(normalized) {
233 Ok(x) => x,
233 Ok(x) => x,
234 Err(e) => return Some(Err(e.into())),
234 Err(e) => return Some(Err(e.into())),
235 };
235 };
236 let target = root_dir.as_ref().join(buf);
236 let target = root_dir.as_ref().join(buf);
237 let st = target.symlink_metadata();
237 let st = target.symlink_metadata();
238 let in_dmap = dmap.get(normalized);
238 let in_dmap = dmap.get(normalized);
239 match st {
239 match st {
240 Ok(meta) => {
240 Ok(meta) => {
241 let file_type = meta.file_type();
241 let file_type = meta.file_type();
242 return if file_type.is_file() || file_type.is_symlink() {
242 return if file_type.is_file() || file_type.is_symlink() {
243 if let Some(entry) = in_dmap {
243 if let Some(entry) = in_dmap {
244 return Some(Ok((
244 return Some(Ok((
245 normalized,
245 normalized,
246 dispatch_found(
246 dispatch_found(
247 &normalized,
247 &normalized,
248 *entry,
248 *entry,
249 HgMetadata::from_metadata(meta),
249 HgMetadata::from_metadata(meta),
250 &dmap.copy_map,
250 &dmap.copy_map,
251 options,
251 options,
252 ),
252 ),
253 )));
253 )));
254 }
254 }
255 Some(Ok((normalized, Dispatch::Unknown)))
255 Some(Ok((normalized, Dispatch::Unknown)))
256 } else {
256 } else {
257 if file_type.is_dir() {
257 if file_type.is_dir() {
258 Some(Ok((
258 Some(Ok((
259 normalized,
259 normalized,
260 Dispatch::Directory {
260 Dispatch::Directory {
261 was_file: in_dmap.is_some(),
261 was_file: in_dmap.is_some(),
262 },
262 },
263 )))
263 )))
264 } else {
264 } else {
265 Some(Ok((
265 Some(Ok((
266 normalized,
266 normalized,
267 Dispatch::Bad(BadMatch::BadType(
267 Dispatch::Bad(BadMatch::BadType(
268 // TODO do more than unknown
268 // TODO do more than unknown
269 // Support for all `BadType` variant
269 // Support for all `BadType` variant
270 // varies greatly between platforms.
270 // varies greatly between platforms.
271 // So far, no tests check the type and
271 // So far, no tests check the type and
272 // this should be good enough for most
272 // this should be good enough for most
273 // users.
273 // users.
274 BadType::Unknown,
274 BadType::Unknown,
275 )),
275 )),
276 )))
276 )))
277 }
277 }
278 };
278 };
279 }
279 }
280 Err(_) => {
280 Err(_) => {
281 if let Some(entry) = in_dmap {
281 if let Some(entry) = in_dmap {
282 return Some(Ok((
282 return Some(Ok((
283 normalized,
283 normalized,
284 dispatch_missing(entry.state),
284 dispatch_missing(entry.state),
285 )));
285 )));
286 }
286 }
287 }
287 }
288 };
288 };
289 None
289 None
290 })
290 })
291 .flatten()
291 .flatten()
292 }
292 }
293
293
294 #[derive(Debug, Copy, Clone)]
294 #[derive(Debug, Copy, Clone)]
295 pub struct StatusOptions {
295 pub struct StatusOptions {
296 /// Remember the most recent modification timeslot for status, to make
296 /// Remember the most recent modification timeslot for status, to make
297 /// sure we won't miss future size-preserving file content modifications
297 /// sure we won't miss future size-preserving file content modifications
298 /// that happen within the same timeslot.
298 /// that happen within the same timeslot.
299 pub last_normal_time: i64,
299 pub last_normal_time: i64,
300 /// Whether we are on a filesystem with UNIX-like exec flags
300 /// Whether we are on a filesystem with UNIX-like exec flags
301 pub check_exec: bool,
301 pub check_exec: bool,
302 pub list_clean: bool,
302 pub list_clean: bool,
303 pub list_unknown: bool,
303 pub list_unknown: bool,
304 pub list_ignored: bool,
304 pub list_ignored: bool,
305 }
305 }
306
306
307 /// Dispatch a single entry (file, folder, symlink...) found during `traverse`.
307 /// Dispatch a single entry (file, folder, symlink...) found during `traverse`.
308 /// 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
309 /// in a separate thread.
309 /// in a separate thread.
310 fn handle_traversed_entry<'a>(
310 fn handle_traversed_entry<'a>(
311 scope: &rayon::Scope<'a>,
311 scope: &rayon::Scope<'a>,
312 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
312 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
313 matcher: &'a (impl Matcher + Sync),
313 matcher: &'a (impl Matcher + Sync),
314 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
314 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
315 dmap: &'a DirstateMap,
315 dmap: &'a DirstateMap,
316 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
316 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
317 ignore_fn: &'a IgnoreFnType,
317 ignore_fn: &'a IgnoreFnType,
318 dir_ignore_fn: &'a IgnoreFnType,
318 dir_ignore_fn: &'a IgnoreFnType,
319 options: StatusOptions,
319 options: StatusOptions,
320 filename: HgPathBuf,
320 filename: HgPathBuf,
321 dir_entry: DirEntry,
321 dir_entry: DirEntry,
322 ) -> IoResult<()> {
322 ) -> IoResult<()> {
323 let file_type = dir_entry.file_type()?;
323 let file_type = dir_entry.file_type()?;
324 let entry_option = dmap.get(&filename);
324 let entry_option = dmap.get(&filename);
325
325
326 if filename.as_bytes() == b".hg" {
327 // Could be a directory or a symlink
328 return Ok(());
329 }
330
326 if file_type.is_dir() {
331 if file_type.is_dir() {
327 handle_traversed_dir(
332 handle_traversed_dir(
328 scope,
333 scope,
329 files_sender,
334 files_sender,
330 matcher,
335 matcher,
331 root_dir,
336 root_dir,
332 dmap,
337 dmap,
333 old_results,
338 old_results,
334 ignore_fn,
339 ignore_fn,
335 dir_ignore_fn,
340 dir_ignore_fn,
336 options,
341 options,
337 entry_option,
342 entry_option,
338 filename,
343 filename,
339 );
344 );
340 } else if file_type.is_file() || file_type.is_symlink() {
345 } else if file_type.is_file() || file_type.is_symlink() {
341 if let Some(entry) = entry_option {
346 if let Some(entry) = entry_option {
342 if matcher.matches_everything() || matcher.matches(&filename) {
347 if matcher.matches_everything() || matcher.matches(&filename) {
343 let metadata = dir_entry.metadata()?;
348 let metadata = dir_entry.metadata()?;
344 files_sender
349 files_sender
345 .send(Ok((
350 .send(Ok((
346 filename.to_owned(),
351 filename.to_owned(),
347 dispatch_found(
352 dispatch_found(
348 &filename,
353 &filename,
349 *entry,
354 *entry,
350 HgMetadata::from_metadata(metadata),
355 HgMetadata::from_metadata(metadata),
351 &dmap.copy_map,
356 &dmap.copy_map,
352 options,
357 options,
353 ),
358 ),
354 )))
359 )))
355 .unwrap();
360 .unwrap();
356 }
361 }
357 } else if (matcher.matches_everything() || matcher.matches(&filename))
362 } else if (matcher.matches_everything() || matcher.matches(&filename))
358 && !ignore_fn(&filename)
363 && !ignore_fn(&filename)
359 {
364 {
360 if (options.list_ignored || matcher.exact_match(&filename))
365 if (options.list_ignored || matcher.exact_match(&filename))
361 && dir_ignore_fn(&filename)
366 && dir_ignore_fn(&filename)
362 {
367 {
363 if options.list_ignored {
368 if options.list_ignored {
364 files_sender
369 files_sender
365 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
370 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
366 .unwrap();
371 .unwrap();
367 }
372 }
368 } else {
373 } else {
369 files_sender
374 files_sender
370 .send(Ok((filename.to_owned(), Dispatch::Unknown)))
375 .send(Ok((filename.to_owned(), Dispatch::Unknown)))
371 .unwrap();
376 .unwrap();
372 }
377 }
373 } else if ignore_fn(&filename) && options.list_ignored {
378 } else if ignore_fn(&filename) && options.list_ignored {
374 files_sender
379 files_sender
375 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
380 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
376 .unwrap();
381 .unwrap();
377 }
382 }
378 } else if let Some(entry) = entry_option {
383 } else if let Some(entry) = entry_option {
379 // Used to be a file or a folder, now something else.
384 // Used to be a file or a folder, now something else.
380 if matcher.matches_everything() || matcher.matches(&filename) {
385 if matcher.matches_everything() || matcher.matches(&filename) {
381 files_sender
386 files_sender
382 .send(Ok((filename.to_owned(), dispatch_missing(entry.state))))
387 .send(Ok((filename.to_owned(), dispatch_missing(entry.state))))
383 .unwrap();
388 .unwrap();
384 }
389 }
385 }
390 }
386
391
387 Ok(())
392 Ok(())
388 }
393 }
389
394
390 /// A directory was found in the filesystem and needs to be traversed
395 /// A directory was found in the filesystem and needs to be traversed
391 fn handle_traversed_dir<'a>(
396 fn handle_traversed_dir<'a>(
392 scope: &rayon::Scope<'a>,
397 scope: &rayon::Scope<'a>,
393 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
398 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
394 matcher: &'a (impl Matcher + Sync),
399 matcher: &'a (impl Matcher + Sync),
395 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
400 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
396 dmap: &'a DirstateMap,
401 dmap: &'a DirstateMap,
397 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
402 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
398 ignore_fn: &'a IgnoreFnType,
403 ignore_fn: &'a IgnoreFnType,
399 dir_ignore_fn: &'a IgnoreFnType,
404 dir_ignore_fn: &'a IgnoreFnType,
400 options: StatusOptions,
405 options: StatusOptions,
401 entry_option: Option<&'a DirstateEntry>,
406 entry_option: Option<&'a DirstateEntry>,
402 directory: HgPathBuf,
407 directory: HgPathBuf,
403 ) {
408 ) {
404 scope.spawn(move |_| {
409 scope.spawn(move |_| {
405 // Nested `if` until `rust-lang/rust#53668` is stable
410 // Nested `if` until `rust-lang/rust#53668` is stable
406 if let Some(entry) = entry_option {
411 if let Some(entry) = entry_option {
407 // Used to be a file, is now a folder
412 // Used to be a file, is now a folder
408 if matcher.matches_everything() || matcher.matches(&directory) {
413 if matcher.matches_everything() || matcher.matches(&directory) {
409 files_sender
414 files_sender
410 .send(Ok((
415 .send(Ok((
411 directory.to_owned(),
416 directory.to_owned(),
412 dispatch_missing(entry.state),
417 dispatch_missing(entry.state),
413 )))
418 )))
414 .unwrap();
419 .unwrap();
415 }
420 }
416 }
421 }
417 // Do we need to traverse it?
422 // Do we need to traverse it?
418 if !ignore_fn(&directory) || options.list_ignored {
423 if !ignore_fn(&directory) || options.list_ignored {
419 traverse_dir(
424 traverse_dir(
420 files_sender,
425 files_sender,
421 matcher,
426 matcher,
422 root_dir,
427 root_dir,
423 dmap,
428 dmap,
424 directory,
429 directory,
425 &old_results,
430 &old_results,
426 ignore_fn,
431 ignore_fn,
427 dir_ignore_fn,
432 dir_ignore_fn,
428 options,
433 options,
429 )
434 )
430 .unwrap_or_else(|e| files_sender.send(Err(e)).unwrap())
435 .unwrap_or_else(|e| files_sender.send(Err(e)).unwrap())
431 }
436 }
432 });
437 });
433 }
438 }
434
439
435 /// Decides whether the directory needs to be listed, and if so handles the
440 /// Decides whether the directory needs to be listed, and if so handles the
436 /// entries in a separate thread.
441 /// entries in a separate thread.
437 fn traverse_dir<'a>(
442 fn traverse_dir<'a>(
438 files_sender: &crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
443 files_sender: &crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
439 matcher: &'a (impl Matcher + Sync),
444 matcher: &'a (impl Matcher + Sync),
440 root_dir: impl AsRef<Path> + Sync + Send + Copy,
445 root_dir: impl AsRef<Path> + Sync + Send + Copy,
441 dmap: &'a DirstateMap,
446 dmap: &'a DirstateMap,
442 directory: impl AsRef<HgPath>,
447 directory: impl AsRef<HgPath>,
443 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
448 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
444 ignore_fn: &IgnoreFnType,
449 ignore_fn: &IgnoreFnType,
445 dir_ignore_fn: &IgnoreFnType,
450 dir_ignore_fn: &IgnoreFnType,
446 options: StatusOptions,
451 options: StatusOptions,
447 ) -> IoResult<()> {
452 ) -> IoResult<()> {
448 let directory = directory.as_ref();
453 let directory = directory.as_ref();
449 if directory.as_bytes() == b".hg" {
454
450 return Ok(());
451 }
452 let visit_entries = match matcher.visit_children_set(directory) {
455 let visit_entries = match matcher.visit_children_set(directory) {
453 VisitChildrenSet::Empty => return Ok(()),
456 VisitChildrenSet::Empty => return Ok(()),
454 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
457 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
455 VisitChildrenSet::Set(set) => Some(set),
458 VisitChildrenSet::Set(set) => Some(set),
456 };
459 };
457 let buf = hg_path_to_path_buf(directory)?;
460 let buf = hg_path_to_path_buf(directory)?;
458 let dir_path = root_dir.as_ref().join(buf);
461 let dir_path = root_dir.as_ref().join(buf);
459
462
460 let skip_dot_hg = !directory.as_bytes().is_empty();
463 let skip_dot_hg = !directory.as_bytes().is_empty();
461 let entries = match list_directory(dir_path, skip_dot_hg) {
464 let entries = match list_directory(dir_path, skip_dot_hg) {
462 Err(e) => match e.kind() {
465 Err(e) => match e.kind() {
463 ErrorKind::NotFound | ErrorKind::PermissionDenied => {
466 ErrorKind::NotFound | ErrorKind::PermissionDenied => {
464 files_sender
467 files_sender
465 .send(Ok((
468 .send(Ok((
466 directory.to_owned(),
469 directory.to_owned(),
467 Dispatch::Bad(BadMatch::OsError(
470 Dispatch::Bad(BadMatch::OsError(
468 // Unwrapping here is OK because the error always
471 // Unwrapping here is OK because the error always
469 // is a real os error
472 // is a real os error
470 e.raw_os_error().unwrap(),
473 e.raw_os_error().unwrap(),
471 )),
474 )),
472 )))
475 )))
473 .unwrap();
476 .unwrap();
474 return Ok(());
477 return Ok(());
475 }
478 }
476 _ => return Err(e),
479 _ => return Err(e),
477 },
480 },
478 Ok(entries) => entries,
481 Ok(entries) => entries,
479 };
482 };
480
483
481 rayon::scope(|scope| -> IoResult<()> {
484 rayon::scope(|scope| -> IoResult<()> {
482 for (filename, dir_entry) in entries {
485 for (filename, dir_entry) in entries {
483 if let Some(ref set) = visit_entries {
486 if let Some(ref set) = visit_entries {
484 if !set.contains(filename.deref()) {
487 if !set.contains(filename.deref()) {
485 continue;
488 continue;
486 }
489 }
487 }
490 }
488 // TODO normalize
491 // TODO normalize
489 let filename = if directory.is_empty() {
492 let filename = if directory.is_empty() {
490 filename.to_owned()
493 filename.to_owned()
491 } else {
494 } else {
492 directory.join(&filename)
495 directory.join(&filename)
493 };
496 };
494
497
495 if !old_results.contains_key(filename.deref()) {
498 if !old_results.contains_key(filename.deref()) {
496 handle_traversed_entry(
499 handle_traversed_entry(
497 scope,
500 scope,
498 files_sender,
501 files_sender,
499 matcher,
502 matcher,
500 root_dir,
503 root_dir,
501 dmap,
504 dmap,
502 old_results,
505 old_results,
503 ignore_fn,
506 ignore_fn,
504 dir_ignore_fn,
507 dir_ignore_fn,
505 options,
508 options,
506 filename,
509 filename,
507 dir_entry,
510 dir_entry,
508 )?;
511 )?;
509 }
512 }
510 }
513 }
511 Ok(())
514 Ok(())
512 })
515 })
513 }
516 }
514
517
515 /// Walk the working directory recursively to look for changes compared to the
518 /// Walk the working directory recursively to look for changes compared to the
516 /// current `DirstateMap`.
519 /// current `DirstateMap`.
517 ///
520 ///
518 /// This takes a mutable reference to the results to account for the `extend`
521 /// This takes a mutable reference to the results to account for the `extend`
519 /// in timings
522 /// in timings
520 #[timed]
523 #[timed]
521 fn traverse<'a>(
524 fn traverse<'a>(
522 matcher: &'a (impl Matcher + Sync),
525 matcher: &'a (impl Matcher + Sync),
523 root_dir: impl AsRef<Path> + Sync + Send + Copy,
526 root_dir: impl AsRef<Path> + Sync + Send + Copy,
524 dmap: &'a DirstateMap,
527 dmap: &'a DirstateMap,
525 path: impl AsRef<HgPath>,
528 path: impl AsRef<HgPath>,
526 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
529 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
527 ignore_fn: &IgnoreFnType,
530 ignore_fn: &IgnoreFnType,
528 dir_ignore_fn: &IgnoreFnType,
531 dir_ignore_fn: &IgnoreFnType,
529 options: StatusOptions,
532 options: StatusOptions,
530 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
533 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
531 ) -> IoResult<()> {
534 ) -> IoResult<()> {
532 let root_dir = root_dir.as_ref();
535 let root_dir = root_dir.as_ref();
533
536
534 // The traversal is done in parallel, so use a channel to gather entries.
537 // The traversal is done in parallel, so use a channel to gather entries.
535 // `crossbeam::Sender` is `Send`, while `mpsc::Sender` is not.
538 // `crossbeam::Sender` is `Send`, while `mpsc::Sender` is not.
536 let (files_transmitter, files_receiver) = crossbeam::channel::unbounded();
539 let (files_transmitter, files_receiver) = crossbeam::channel::unbounded();
537
540
538 traverse_dir(
541 traverse_dir(
539 &files_transmitter,
542 &files_transmitter,
540 matcher,
543 matcher,
541 root_dir,
544 root_dir,
542 &dmap,
545 &dmap,
543 path,
546 path,
544 &old_results,
547 &old_results,
545 &ignore_fn,
548 &ignore_fn,
546 &dir_ignore_fn,
549 &dir_ignore_fn,
547 options,
550 options,
548 )?;
551 )?;
549
552
550 // Disconnect the channel so the receiver stops waiting
553 // Disconnect the channel so the receiver stops waiting
551 drop(files_transmitter);
554 drop(files_transmitter);
552
555
553 // TODO don't collect. Find a way of replicating the behavior of
556 // TODO don't collect. Find a way of replicating the behavior of
554 // `itertools::process_results`, but for `rayon::ParallelIterator`
557 // `itertools::process_results`, but for `rayon::ParallelIterator`
555 let new_results: IoResult<Vec<(Cow<'a, HgPath>, Dispatch)>> =
558 let new_results: IoResult<Vec<(Cow<'a, HgPath>, Dispatch)>> =
556 files_receiver
559 files_receiver
557 .into_iter()
560 .into_iter()
558 .map(|item| {
561 .map(|item| {
559 let (f, d) = item?;
562 let (f, d) = item?;
560 Ok((Cow::Owned(f), d))
563 Ok((Cow::Owned(f), d))
561 })
564 })
562 .collect();
565 .collect();
563
566
564 results.par_extend(new_results?);
567 results.par_extend(new_results?);
565
568
566 Ok(())
569 Ok(())
567 }
570 }
568
571
569 /// Stat all entries in the `DirstateMap` and mark them for dispatch.
572 /// Stat all entries in the `DirstateMap` and mark them for dispatch.
570 fn stat_dmap_entries(
573 fn stat_dmap_entries(
571 dmap: &DirstateMap,
574 dmap: &DirstateMap,
572 root_dir: impl AsRef<Path> + Sync + Send,
575 root_dir: impl AsRef<Path> + Sync + Send,
573 options: StatusOptions,
576 options: StatusOptions,
574 ) -> impl ParallelIterator<Item = IoResult<(&HgPath, Dispatch)>> {
577 ) -> impl ParallelIterator<Item = IoResult<(&HgPath, Dispatch)>> {
575 dmap.par_iter().map(move |(filename, entry)| {
578 dmap.par_iter().map(move |(filename, entry)| {
576 let filename: &HgPath = filename;
579 let filename: &HgPath = filename;
577 let filename_as_path = hg_path_to_path_buf(filename)?;
580 let filename_as_path = hg_path_to_path_buf(filename)?;
578 let meta = root_dir.as_ref().join(filename_as_path).symlink_metadata();
581 let meta = root_dir.as_ref().join(filename_as_path).symlink_metadata();
579
582
580 match meta {
583 match meta {
581 Ok(ref m)
584 Ok(ref m)
582 if !(m.file_type().is_file()
585 if !(m.file_type().is_file()
583 || m.file_type().is_symlink()) =>
586 || m.file_type().is_symlink()) =>
584 {
587 {
585 Ok((filename, dispatch_missing(entry.state)))
588 Ok((filename, dispatch_missing(entry.state)))
586 }
589 }
587 Ok(m) => Ok((
590 Ok(m) => Ok((
588 filename,
591 filename,
589 dispatch_found(
592 dispatch_found(
590 filename,
593 filename,
591 *entry,
594 *entry,
592 HgMetadata::from_metadata(m),
595 HgMetadata::from_metadata(m),
593 &dmap.copy_map,
596 &dmap.copy_map,
594 options,
597 options,
595 ),
598 ),
596 )),
599 )),
597 Err(ref e)
600 Err(ref e)
598 if e.kind() == ErrorKind::NotFound
601 if e.kind() == ErrorKind::NotFound
599 || e.raw_os_error() == Some(20) =>
602 || e.raw_os_error() == Some(20) =>
600 {
603 {
601 // Rust does not yet have an `ErrorKind` for
604 // Rust does not yet have an `ErrorKind` for
602 // `NotADirectory` (errno 20)
605 // `NotADirectory` (errno 20)
603 // It happens if the dirstate contains `foo/bar` and
606 // It happens if the dirstate contains `foo/bar` and
604 // foo is not a directory
607 // foo is not a directory
605 Ok((filename, dispatch_missing(entry.state)))
608 Ok((filename, dispatch_missing(entry.state)))
606 }
609 }
607 Err(e) => Err(e),
610 Err(e) => Err(e),
608 }
611 }
609 })
612 })
610 }
613 }
611
614
612 /// This takes a mutable reference to the results to account for the `extend`
615 /// This takes a mutable reference to the results to account for the `extend`
613 /// in timings
616 /// in timings
614 #[timed]
617 #[timed]
615 fn extend_from_dmap<'a>(
618 fn extend_from_dmap<'a>(
616 dmap: &'a DirstateMap,
619 dmap: &'a DirstateMap,
617 root_dir: impl AsRef<Path> + Sync + Send,
620 root_dir: impl AsRef<Path> + Sync + Send,
618 options: StatusOptions,
621 options: StatusOptions,
619 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
622 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
620 ) {
623 ) {
621 results.par_extend(
624 results.par_extend(
622 stat_dmap_entries(dmap, root_dir, options)
625 stat_dmap_entries(dmap, root_dir, options)
623 .flatten()
626 .flatten()
624 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch)),
627 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch)),
625 );
628 );
626 }
629 }
627
630
628 #[derive(Debug)]
631 #[derive(Debug)]
629 pub struct DirstateStatus<'a> {
632 pub struct DirstateStatus<'a> {
630 pub modified: Vec<Cow<'a, HgPath>>,
633 pub modified: Vec<Cow<'a, HgPath>>,
631 pub added: Vec<Cow<'a, HgPath>>,
634 pub added: Vec<Cow<'a, HgPath>>,
632 pub removed: Vec<Cow<'a, HgPath>>,
635 pub removed: Vec<Cow<'a, HgPath>>,
633 pub deleted: Vec<Cow<'a, HgPath>>,
636 pub deleted: Vec<Cow<'a, HgPath>>,
634 pub clean: Vec<Cow<'a, HgPath>>,
637 pub clean: Vec<Cow<'a, HgPath>>,
635 pub ignored: Vec<Cow<'a, HgPath>>,
638 pub ignored: Vec<Cow<'a, HgPath>>,
636 pub unknown: Vec<Cow<'a, HgPath>>,
639 pub unknown: Vec<Cow<'a, HgPath>>,
637 pub bad: Vec<(Cow<'a, HgPath>, BadMatch)>,
640 pub bad: Vec<(Cow<'a, HgPath>, BadMatch)>,
638 }
641 }
639
642
640 #[timed]
643 #[timed]
641 fn build_response<'a>(
644 fn build_response<'a>(
642 results: impl IntoIterator<Item = (Cow<'a, HgPath>, Dispatch)>,
645 results: impl IntoIterator<Item = (Cow<'a, HgPath>, Dispatch)>,
643 ) -> (Vec<Cow<'a, HgPath>>, DirstateStatus<'a>) {
646 ) -> (Vec<Cow<'a, HgPath>>, DirstateStatus<'a>) {
644 let mut lookup = vec![];
647 let mut lookup = vec![];
645 let mut modified = vec![];
648 let mut modified = vec![];
646 let mut added = vec![];
649 let mut added = vec![];
647 let mut removed = vec![];
650 let mut removed = vec![];
648 let mut deleted = vec![];
651 let mut deleted = vec![];
649 let mut clean = vec![];
652 let mut clean = vec![];
650 let mut ignored = vec![];
653 let mut ignored = vec![];
651 let mut unknown = vec![];
654 let mut unknown = vec![];
652 let mut bad = vec![];
655 let mut bad = vec![];
653
656
654 for (filename, dispatch) in results.into_iter() {
657 for (filename, dispatch) in results.into_iter() {
655 match dispatch {
658 match dispatch {
656 Dispatch::Unknown => unknown.push(filename),
659 Dispatch::Unknown => unknown.push(filename),
657 Dispatch::Unsure => lookup.push(filename),
660 Dispatch::Unsure => lookup.push(filename),
658 Dispatch::Modified => modified.push(filename),
661 Dispatch::Modified => modified.push(filename),
659 Dispatch::Added => added.push(filename),
662 Dispatch::Added => added.push(filename),
660 Dispatch::Removed => removed.push(filename),
663 Dispatch::Removed => removed.push(filename),
661 Dispatch::Deleted => deleted.push(filename),
664 Dispatch::Deleted => deleted.push(filename),
662 Dispatch::Clean => clean.push(filename),
665 Dispatch::Clean => clean.push(filename),
663 Dispatch::Ignored => ignored.push(filename),
666 Dispatch::Ignored => ignored.push(filename),
664 Dispatch::None => {}
667 Dispatch::None => {}
665 Dispatch::Bad(reason) => bad.push((filename, reason)),
668 Dispatch::Bad(reason) => bad.push((filename, reason)),
666 Dispatch::Directory { .. } => {}
669 Dispatch::Directory { .. } => {}
667 }
670 }
668 }
671 }
669
672
670 (
673 (
671 lookup,
674 lookup,
672 DirstateStatus {
675 DirstateStatus {
673 modified,
676 modified,
674 added,
677 added,
675 removed,
678 removed,
676 deleted,
679 deleted,
677 clean,
680 clean,
678 ignored,
681 ignored,
679 unknown,
682 unknown,
680 bad,
683 bad,
681 },
684 },
682 )
685 )
683 }
686 }
684
687
685 #[derive(Debug)]
688 #[derive(Debug)]
686 pub enum StatusError {
689 pub enum StatusError {
687 IO(std::io::Error),
690 IO(std::io::Error),
688 Path(HgPathError),
691 Path(HgPathError),
689 Pattern(PatternError),
692 Pattern(PatternError),
690 }
693 }
691
694
692 pub type StatusResult<T> = Result<T, StatusError>;
695 pub type StatusResult<T> = Result<T, StatusError>;
693
696
694 impl From<PatternError> for StatusError {
697 impl From<PatternError> for StatusError {
695 fn from(e: PatternError) -> Self {
698 fn from(e: PatternError) -> Self {
696 StatusError::Pattern(e)
699 StatusError::Pattern(e)
697 }
700 }
698 }
701 }
699 impl From<HgPathError> for StatusError {
702 impl From<HgPathError> for StatusError {
700 fn from(e: HgPathError) -> Self {
703 fn from(e: HgPathError) -> Self {
701 StatusError::Path(e)
704 StatusError::Path(e)
702 }
705 }
703 }
706 }
704 impl From<std::io::Error> for StatusError {
707 impl From<std::io::Error> for StatusError {
705 fn from(e: std::io::Error) -> Self {
708 fn from(e: std::io::Error) -> Self {
706 StatusError::IO(e)
709 StatusError::IO(e)
707 }
710 }
708 }
711 }
709
712
710 impl ToString for StatusError {
713 impl ToString for StatusError {
711 fn to_string(&self) -> String {
714 fn to_string(&self) -> String {
712 match self {
715 match self {
713 StatusError::IO(e) => e.to_string(),
716 StatusError::IO(e) => e.to_string(),
714 StatusError::Path(e) => e.to_string(),
717 StatusError::Path(e) => e.to_string(),
715 StatusError::Pattern(e) => e.to_string(),
718 StatusError::Pattern(e) => e.to_string(),
716 }
719 }
717 }
720 }
718 }
721 }
719
722
720 /// This takes a mutable reference to the results to account for the `extend`
723 /// This takes a mutable reference to the results to account for the `extend`
721 /// in timings
724 /// in timings
722 #[timed]
725 #[timed]
723 fn handle_unknowns<'a>(
726 fn handle_unknowns<'a>(
724 dmap: &'a DirstateMap,
727 dmap: &'a DirstateMap,
725 matcher: &(impl Matcher + Sync),
728 matcher: &(impl Matcher + Sync),
726 root_dir: impl AsRef<Path> + Sync + Send + Copy,
729 root_dir: impl AsRef<Path> + Sync + Send + Copy,
727 options: StatusOptions,
730 options: StatusOptions,
728 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
731 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
729 ) -> IoResult<()> {
732 ) -> IoResult<()> {
730 let to_visit: Vec<(&HgPath, &DirstateEntry)> = if results.is_empty()
733 let to_visit: Vec<(&HgPath, &DirstateEntry)> = if results.is_empty()
731 && matcher.matches_everything()
734 && matcher.matches_everything()
732 {
735 {
733 dmap.iter().map(|(f, e)| (f.deref(), e)).collect()
736 dmap.iter().map(|(f, e)| (f.deref(), e)).collect()
734 } else {
737 } else {
735 // Only convert to a hashmap if needed.
738 // Only convert to a hashmap if needed.
736 let old_results: FastHashMap<_, _> = results.iter().cloned().collect();
739 let old_results: FastHashMap<_, _> = results.iter().cloned().collect();
737 dmap.iter()
740 dmap.iter()
738 .filter_map(move |(f, e)| {
741 .filter_map(move |(f, e)| {
739 if !old_results.contains_key(f.deref()) && matcher.matches(f) {
742 if !old_results.contains_key(f.deref()) && matcher.matches(f) {
740 Some((f.deref(), e))
743 Some((f.deref(), e))
741 } else {
744 } else {
742 None
745 None
743 }
746 }
744 })
747 })
745 .collect()
748 .collect()
746 };
749 };
747
750
748 // We walked all dirs under the roots that weren't ignored, and
751 // We walked all dirs under the roots that weren't ignored, and
749 // everything that matched was stat'ed and is already in results.
752 // everything that matched was stat'ed and is already in results.
750 // The rest must thus be ignored or under a symlink.
753 // The rest must thus be ignored or under a symlink.
751 let path_auditor = PathAuditor::new(root_dir);
754 let path_auditor = PathAuditor::new(root_dir);
752
755
753 // TODO don't collect. Find a way of replicating the behavior of
756 // TODO don't collect. Find a way of replicating the behavior of
754 // `itertools::process_results`, but for `rayon::ParallelIterator`
757 // `itertools::process_results`, but for `rayon::ParallelIterator`
755 let new_results: IoResult<Vec<_>> = to_visit
758 let new_results: IoResult<Vec<_>> = to_visit
756 .into_par_iter()
759 .into_par_iter()
757 .filter_map(|(filename, entry)| -> Option<IoResult<_>> {
760 .filter_map(|(filename, entry)| -> Option<IoResult<_>> {
758 // Report ignored items in the dmap as long as they are not
761 // Report ignored items in the dmap as long as they are not
759 // under a symlink directory.
762 // under a symlink directory.
760 if path_auditor.check(filename) {
763 if path_auditor.check(filename) {
761 // TODO normalize for case-insensitive filesystems
764 // TODO normalize for case-insensitive filesystems
762 let buf = match hg_path_to_path_buf(filename) {
765 let buf = match hg_path_to_path_buf(filename) {
763 Ok(x) => x,
766 Ok(x) => x,
764 Err(e) => return Some(Err(e.into())),
767 Err(e) => return Some(Err(e.into())),
765 };
768 };
766 Some(Ok((
769 Some(Ok((
767 Cow::Borrowed(filename),
770 Cow::Borrowed(filename),
768 match root_dir.as_ref().join(&buf).symlink_metadata() {
771 match root_dir.as_ref().join(&buf).symlink_metadata() {
769 // File was just ignored, no links, and exists
772 // File was just ignored, no links, and exists
770 Ok(meta) => {
773 Ok(meta) => {
771 let metadata = HgMetadata::from_metadata(meta);
774 let metadata = HgMetadata::from_metadata(meta);
772 dispatch_found(
775 dispatch_found(
773 filename,
776 filename,
774 *entry,
777 *entry,
775 metadata,
778 metadata,
776 &dmap.copy_map,
779 &dmap.copy_map,
777 options,
780 options,
778 )
781 )
779 }
782 }
780 // File doesn't exist
783 // File doesn't exist
781 Err(_) => dispatch_missing(entry.state),
784 Err(_) => dispatch_missing(entry.state),
782 },
785 },
783 )))
786 )))
784 } else {
787 } else {
785 // It's either missing or under a symlink directory which
788 // It's either missing or under a symlink directory which
786 // we, in this case, report as missing.
789 // we, in this case, report as missing.
787 Some(Ok((
790 Some(Ok((
788 Cow::Borrowed(filename),
791 Cow::Borrowed(filename),
789 dispatch_missing(entry.state),
792 dispatch_missing(entry.state),
790 )))
793 )))
791 }
794 }
792 })
795 })
793 .collect();
796 .collect();
794
797
795 results.par_extend(new_results?);
798 results.par_extend(new_results?);
796
799
797 Ok(())
800 Ok(())
798 }
801 }
799
802
800 /// Get the status of files in the working directory.
803 /// Get the status of files in the working directory.
801 ///
804 ///
802 /// This is the current entry-point for `hg-core` and is realistically unusable
805 /// This is the current entry-point for `hg-core` and is realistically unusable
803 /// outside of a Python context because its arguments need to provide a lot of
806 /// outside of a Python context because its arguments need to provide a lot of
804 /// information that will not be necessary in the future.
807 /// information that will not be necessary in the future.
805 #[timed]
808 #[timed]
806 pub fn status<'a: 'c, 'b: 'c, 'c>(
809 pub fn status<'a: 'c, 'b: 'c, 'c>(
807 dmap: &'a DirstateMap,
810 dmap: &'a DirstateMap,
808 matcher: &'b (impl Matcher + Sync),
811 matcher: &'b (impl Matcher + Sync),
809 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'c,
812 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'c,
810 ignore_files: Vec<PathBuf>,
813 ignore_files: Vec<PathBuf>,
811 options: StatusOptions,
814 options: StatusOptions,
812 ) -> StatusResult<(
815 ) -> StatusResult<(
813 (Vec<Cow<'c, HgPath>>, DirstateStatus<'c>),
816 (Vec<Cow<'c, HgPath>>, DirstateStatus<'c>),
814 Vec<PatternFileWarning>,
817 Vec<PatternFileWarning>,
815 )> {
818 )> {
816 // Needs to outlive `dir_ignore_fn` since it's captured.
819 // Needs to outlive `dir_ignore_fn` since it's captured.
817 let mut ignore_fn: IgnoreFnType;
820 let mut ignore_fn: IgnoreFnType;
818
821
819 // Only involve real ignore mechanism if we're listing unknowns or ignored.
822 // Only involve real ignore mechanism if we're listing unknowns or ignored.
820 let (dir_ignore_fn, warnings): (IgnoreFnType, _) = if options.list_ignored
823 let (dir_ignore_fn, warnings): (IgnoreFnType, _) = if options.list_ignored
821 || options.list_unknown
824 || options.list_unknown
822 {
825 {
823 let (ignore, warnings) = get_ignore_function(ignore_files, root_dir)?;
826 let (ignore, warnings) = get_ignore_function(ignore_files, root_dir)?;
824
827
825 ignore_fn = ignore;
828 ignore_fn = ignore;
826 let dir_ignore_fn = Box::new(|dir: &_| {
829 let dir_ignore_fn = Box::new(|dir: &_| {
827 // Is the path or one of its ancestors ignored?
830 // Is the path or one of its ancestors ignored?
828 if ignore_fn(dir) {
831 if ignore_fn(dir) {
829 true
832 true
830 } else {
833 } else {
831 for p in find_dirs(dir) {
834 for p in find_dirs(dir) {
832 if ignore_fn(p) {
835 if ignore_fn(p) {
833 return true;
836 return true;
834 }
837 }
835 }
838 }
836 false
839 false
837 }
840 }
838 });
841 });
839 (dir_ignore_fn, warnings)
842 (dir_ignore_fn, warnings)
840 } else {
843 } else {
841 ignore_fn = Box::new(|&_| true);
844 ignore_fn = Box::new(|&_| true);
842 (Box::new(|&_| true), vec![])
845 (Box::new(|&_| true), vec![])
843 };
846 };
844
847
845 let files = matcher.file_set();
848 let files = matcher.file_set();
846
849
847 // Step 1: check the files explicitly mentioned by the user
850 // Step 1: check the files explicitly mentioned by the user
848 let explicit = walk_explicit(files, &dmap, root_dir, options);
851 let explicit = walk_explicit(files, &dmap, root_dir, options);
849
852
850 // Collect results into a `Vec` because we do very few lookups in most
853 // Collect results into a `Vec` because we do very few lookups in most
851 // cases.
854 // cases.
852 let (work, mut results): (Vec<_>, Vec<_>) = explicit
855 let (work, mut results): (Vec<_>, Vec<_>) = explicit
853 .filter_map(Result::ok)
856 .filter_map(Result::ok)
854 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch))
857 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch))
855 .partition(|(_, dispatch)| match dispatch {
858 .partition(|(_, dispatch)| match dispatch {
856 Dispatch::Directory { .. } => true,
859 Dispatch::Directory { .. } => true,
857 _ => false,
860 _ => false,
858 });
861 });
859
862
860 if !work.is_empty() {
863 if !work.is_empty() {
861 // Hashmaps are quite a bit slower to build than vecs, so only build it
864 // Hashmaps are quite a bit slower to build than vecs, so only build it
862 // if needed.
865 // if needed.
863 let old_results = results.iter().cloned().collect();
866 let old_results = results.iter().cloned().collect();
864
867
865 // Step 2: recursively check the working directory for changes if
868 // Step 2: recursively check the working directory for changes if
866 // needed
869 // needed
867 for (dir, dispatch) in work {
870 for (dir, dispatch) in work {
868 match dispatch {
871 match dispatch {
869 Dispatch::Directory { was_file } => {
872 Dispatch::Directory { was_file } => {
870 if was_file {
873 if was_file {
871 results.push((dir.to_owned(), Dispatch::Removed));
874 results.push((dir.to_owned(), Dispatch::Removed));
872 }
875 }
873 if options.list_ignored
876 if options.list_ignored
874 || options.list_unknown && !dir_ignore_fn(&dir)
877 || options.list_unknown && !dir_ignore_fn(&dir)
875 {
878 {
876 traverse(
879 traverse(
877 matcher,
880 matcher,
878 root_dir,
881 root_dir,
879 &dmap,
882 &dmap,
880 &dir,
883 &dir,
881 &old_results,
884 &old_results,
882 &ignore_fn,
885 &ignore_fn,
883 &dir_ignore_fn,
886 &dir_ignore_fn,
884 options,
887 options,
885 &mut results,
888 &mut results,
886 )?;
889 )?;
887 }
890 }
888 }
891 }
889 _ => unreachable!("There can only be directories in `work`"),
892 _ => unreachable!("There can only be directories in `work`"),
890 }
893 }
891 }
894 }
892 }
895 }
893
896
894 if !matcher.is_exact() {
897 if !matcher.is_exact() {
895 // Step 3: Check the remaining files from the dmap.
898 // Step 3: Check the remaining files from the dmap.
896 // If a dmap file is not in results yet, it was either
899 // If a dmap file is not in results yet, it was either
897 // a) not matched b) ignored, c) missing, or d) under a
900 // a) not matched b) ignored, c) missing, or d) under a
898 // symlink directory.
901 // symlink directory.
899
902
900 if options.list_unknown {
903 if options.list_unknown {
901 handle_unknowns(dmap, matcher, root_dir, options, &mut results)?;
904 handle_unknowns(dmap, matcher, root_dir, options, &mut results)?;
902 } else {
905 } else {
903 // We may not have walked the full directory tree above, so stat
906 // We may not have walked the full directory tree above, so stat
904 // and check everything we missed.
907 // and check everything we missed.
905 extend_from_dmap(&dmap, root_dir, options, &mut results);
908 extend_from_dmap(&dmap, root_dir, options, &mut results);
906 }
909 }
907 }
910 }
908
911
909 Ok((build_response(results), warnings))
912 Ok((build_response(results), warnings))
910 }
913 }
@@ -1,674 +1,682 b''
1 $ hg init repo1
1 $ hg init repo1
2 $ cd repo1
2 $ cd repo1
3 $ mkdir a b a/1 b/1 b/2
3 $ mkdir a b a/1 b/1 b/2
4 $ touch in_root a/in_a b/in_b a/1/in_a_1 b/1/in_b_1 b/2/in_b_2
4 $ touch in_root a/in_a b/in_b a/1/in_a_1 b/1/in_b_1 b/2/in_b_2
5
5
6 hg status in repo root:
6 hg status in repo root:
7
7
8 $ hg status
8 $ hg status
9 ? a/1/in_a_1
9 ? a/1/in_a_1
10 ? a/in_a
10 ? a/in_a
11 ? b/1/in_b_1
11 ? b/1/in_b_1
12 ? b/2/in_b_2
12 ? b/2/in_b_2
13 ? b/in_b
13 ? b/in_b
14 ? in_root
14 ? in_root
15
15
16 hg status . in repo root:
16 hg status . in repo root:
17
17
18 $ hg status .
18 $ hg status .
19 ? a/1/in_a_1
19 ? a/1/in_a_1
20 ? a/in_a
20 ? a/in_a
21 ? b/1/in_b_1
21 ? b/1/in_b_1
22 ? b/2/in_b_2
22 ? b/2/in_b_2
23 ? b/in_b
23 ? b/in_b
24 ? in_root
24 ? in_root
25
25
26 $ hg status --cwd a
26 $ hg status --cwd a
27 ? a/1/in_a_1
27 ? a/1/in_a_1
28 ? a/in_a
28 ? a/in_a
29 ? b/1/in_b_1
29 ? b/1/in_b_1
30 ? b/2/in_b_2
30 ? b/2/in_b_2
31 ? b/in_b
31 ? b/in_b
32 ? in_root
32 ? in_root
33 $ hg status --cwd a .
33 $ hg status --cwd a .
34 ? 1/in_a_1
34 ? 1/in_a_1
35 ? in_a
35 ? in_a
36 $ hg status --cwd a ..
36 $ hg status --cwd a ..
37 ? 1/in_a_1
37 ? 1/in_a_1
38 ? in_a
38 ? in_a
39 ? ../b/1/in_b_1
39 ? ../b/1/in_b_1
40 ? ../b/2/in_b_2
40 ? ../b/2/in_b_2
41 ? ../b/in_b
41 ? ../b/in_b
42 ? ../in_root
42 ? ../in_root
43
43
44 $ hg status --cwd b
44 $ hg status --cwd b
45 ? a/1/in_a_1
45 ? a/1/in_a_1
46 ? a/in_a
46 ? a/in_a
47 ? b/1/in_b_1
47 ? b/1/in_b_1
48 ? b/2/in_b_2
48 ? b/2/in_b_2
49 ? b/in_b
49 ? b/in_b
50 ? in_root
50 ? in_root
51 $ hg status --cwd b .
51 $ hg status --cwd b .
52 ? 1/in_b_1
52 ? 1/in_b_1
53 ? 2/in_b_2
53 ? 2/in_b_2
54 ? in_b
54 ? in_b
55 $ hg status --cwd b ..
55 $ hg status --cwd b ..
56 ? ../a/1/in_a_1
56 ? ../a/1/in_a_1
57 ? ../a/in_a
57 ? ../a/in_a
58 ? 1/in_b_1
58 ? 1/in_b_1
59 ? 2/in_b_2
59 ? 2/in_b_2
60 ? in_b
60 ? in_b
61 ? ../in_root
61 ? ../in_root
62
62
63 $ hg status --cwd a/1
63 $ hg status --cwd a/1
64 ? a/1/in_a_1
64 ? a/1/in_a_1
65 ? a/in_a
65 ? a/in_a
66 ? b/1/in_b_1
66 ? b/1/in_b_1
67 ? b/2/in_b_2
67 ? b/2/in_b_2
68 ? b/in_b
68 ? b/in_b
69 ? in_root
69 ? in_root
70 $ hg status --cwd a/1 .
70 $ hg status --cwd a/1 .
71 ? in_a_1
71 ? in_a_1
72 $ hg status --cwd a/1 ..
72 $ hg status --cwd a/1 ..
73 ? in_a_1
73 ? in_a_1
74 ? ../in_a
74 ? ../in_a
75
75
76 $ hg status --cwd b/1
76 $ hg status --cwd b/1
77 ? a/1/in_a_1
77 ? a/1/in_a_1
78 ? a/in_a
78 ? a/in_a
79 ? b/1/in_b_1
79 ? b/1/in_b_1
80 ? b/2/in_b_2
80 ? b/2/in_b_2
81 ? b/in_b
81 ? b/in_b
82 ? in_root
82 ? in_root
83 $ hg status --cwd b/1 .
83 $ hg status --cwd b/1 .
84 ? in_b_1
84 ? in_b_1
85 $ hg status --cwd b/1 ..
85 $ hg status --cwd b/1 ..
86 ? in_b_1
86 ? in_b_1
87 ? ../2/in_b_2
87 ? ../2/in_b_2
88 ? ../in_b
88 ? ../in_b
89
89
90 $ hg status --cwd b/2
90 $ hg status --cwd b/2
91 ? a/1/in_a_1
91 ? a/1/in_a_1
92 ? a/in_a
92 ? a/in_a
93 ? b/1/in_b_1
93 ? b/1/in_b_1
94 ? b/2/in_b_2
94 ? b/2/in_b_2
95 ? b/in_b
95 ? b/in_b
96 ? in_root
96 ? in_root
97 $ hg status --cwd b/2 .
97 $ hg status --cwd b/2 .
98 ? in_b_2
98 ? in_b_2
99 $ hg status --cwd b/2 ..
99 $ hg status --cwd b/2 ..
100 ? ../1/in_b_1
100 ? ../1/in_b_1
101 ? in_b_2
101 ? in_b_2
102 ? ../in_b
102 ? ../in_b
103
103
104 combining patterns with root and patterns without a root works
104 combining patterns with root and patterns without a root works
105
105
106 $ hg st a/in_a re:.*b$
106 $ hg st a/in_a re:.*b$
107 ? a/in_a
107 ? a/in_a
108 ? b/in_b
108 ? b/in_b
109
109
110 tweaking defaults works
110 tweaking defaults works
111 $ hg status --cwd a --config ui.tweakdefaults=yes
111 $ hg status --cwd a --config ui.tweakdefaults=yes
112 ? 1/in_a_1
112 ? 1/in_a_1
113 ? in_a
113 ? in_a
114 ? ../b/1/in_b_1
114 ? ../b/1/in_b_1
115 ? ../b/2/in_b_2
115 ? ../b/2/in_b_2
116 ? ../b/in_b
116 ? ../b/in_b
117 ? ../in_root
117 ? ../in_root
118 $ HGPLAIN=1 hg status --cwd a --config ui.tweakdefaults=yes
118 $ HGPLAIN=1 hg status --cwd a --config ui.tweakdefaults=yes
119 ? a/1/in_a_1 (glob)
119 ? a/1/in_a_1 (glob)
120 ? a/in_a (glob)
120 ? a/in_a (glob)
121 ? b/1/in_b_1 (glob)
121 ? b/1/in_b_1 (glob)
122 ? b/2/in_b_2 (glob)
122 ? b/2/in_b_2 (glob)
123 ? b/in_b (glob)
123 ? b/in_b (glob)
124 ? in_root
124 ? in_root
125 $ HGPLAINEXCEPT=tweakdefaults hg status --cwd a --config ui.tweakdefaults=yes
125 $ HGPLAINEXCEPT=tweakdefaults hg status --cwd a --config ui.tweakdefaults=yes
126 ? 1/in_a_1
126 ? 1/in_a_1
127 ? in_a
127 ? in_a
128 ? ../b/1/in_b_1
128 ? ../b/1/in_b_1
129 ? ../b/2/in_b_2
129 ? ../b/2/in_b_2
130 ? ../b/in_b
130 ? ../b/in_b
131 ? ../in_root (glob)
131 ? ../in_root (glob)
132
132
133 relative paths can be requested
133 relative paths can be requested
134
134
135 $ hg status --cwd a --config ui.relative-paths=yes
135 $ hg status --cwd a --config ui.relative-paths=yes
136 ? 1/in_a_1
136 ? 1/in_a_1
137 ? in_a
137 ? in_a
138 ? ../b/1/in_b_1
138 ? ../b/1/in_b_1
139 ? ../b/2/in_b_2
139 ? ../b/2/in_b_2
140 ? ../b/in_b
140 ? ../b/in_b
141 ? ../in_root
141 ? ../in_root
142
142
143 $ hg status --cwd a . --config ui.relative-paths=legacy
143 $ hg status --cwd a . --config ui.relative-paths=legacy
144 ? 1/in_a_1
144 ? 1/in_a_1
145 ? in_a
145 ? in_a
146 $ hg status --cwd a . --config ui.relative-paths=no
146 $ hg status --cwd a . --config ui.relative-paths=no
147 ? a/1/in_a_1
147 ? a/1/in_a_1
148 ? a/in_a
148 ? a/in_a
149
149
150 commands.status.relative overrides ui.relative-paths
150 commands.status.relative overrides ui.relative-paths
151
151
152 $ cat >> $HGRCPATH <<EOF
152 $ cat >> $HGRCPATH <<EOF
153 > [ui]
153 > [ui]
154 > relative-paths = False
154 > relative-paths = False
155 > [commands]
155 > [commands]
156 > status.relative = True
156 > status.relative = True
157 > EOF
157 > EOF
158 $ hg status --cwd a
158 $ hg status --cwd a
159 ? 1/in_a_1
159 ? 1/in_a_1
160 ? in_a
160 ? in_a
161 ? ../b/1/in_b_1
161 ? ../b/1/in_b_1
162 ? ../b/2/in_b_2
162 ? ../b/2/in_b_2
163 ? ../b/in_b
163 ? ../b/in_b
164 ? ../in_root
164 ? ../in_root
165 $ HGPLAIN=1 hg status --cwd a
165 $ HGPLAIN=1 hg status --cwd a
166 ? a/1/in_a_1 (glob)
166 ? a/1/in_a_1 (glob)
167 ? a/in_a (glob)
167 ? a/in_a (glob)
168 ? b/1/in_b_1 (glob)
168 ? b/1/in_b_1 (glob)
169 ? b/2/in_b_2 (glob)
169 ? b/2/in_b_2 (glob)
170 ? b/in_b (glob)
170 ? b/in_b (glob)
171 ? in_root
171 ? in_root
172
172
173 if relative paths are explicitly off, tweakdefaults doesn't change it
173 if relative paths are explicitly off, tweakdefaults doesn't change it
174 $ cat >> $HGRCPATH <<EOF
174 $ cat >> $HGRCPATH <<EOF
175 > [commands]
175 > [commands]
176 > status.relative = False
176 > status.relative = False
177 > EOF
177 > EOF
178 $ hg status --cwd a --config ui.tweakdefaults=yes
178 $ hg status --cwd a --config ui.tweakdefaults=yes
179 ? a/1/in_a_1
179 ? a/1/in_a_1
180 ? a/in_a
180 ? a/in_a
181 ? b/1/in_b_1
181 ? b/1/in_b_1
182 ? b/2/in_b_2
182 ? b/2/in_b_2
183 ? b/in_b
183 ? b/in_b
184 ? in_root
184 ? in_root
185
185
186 $ cd ..
186 $ cd ..
187
187
188 $ hg init repo2
188 $ hg init repo2
189 $ cd repo2
189 $ cd repo2
190 $ touch modified removed deleted ignored
190 $ touch modified removed deleted ignored
191 $ echo "^ignored$" > .hgignore
191 $ echo "^ignored$" > .hgignore
192 $ hg ci -A -m 'initial checkin'
192 $ hg ci -A -m 'initial checkin'
193 adding .hgignore
193 adding .hgignore
194 adding deleted
194 adding deleted
195 adding modified
195 adding modified
196 adding removed
196 adding removed
197 $ touch modified added unknown ignored
197 $ touch modified added unknown ignored
198 $ hg add added
198 $ hg add added
199 $ hg remove removed
199 $ hg remove removed
200 $ rm deleted
200 $ rm deleted
201
201
202 hg status:
202 hg status:
203
203
204 $ hg status
204 $ hg status
205 A added
205 A added
206 R removed
206 R removed
207 ! deleted
207 ! deleted
208 ? unknown
208 ? unknown
209
209
210 hg status modified added removed deleted unknown never-existed ignored:
210 hg status modified added removed deleted unknown never-existed ignored:
211
211
212 $ hg status modified added removed deleted unknown never-existed ignored
212 $ hg status modified added removed deleted unknown never-existed ignored
213 never-existed: * (glob)
213 never-existed: * (glob)
214 A added
214 A added
215 R removed
215 R removed
216 ! deleted
216 ! deleted
217 ? unknown
217 ? unknown
218
218
219 $ hg copy modified copied
219 $ hg copy modified copied
220
220
221 hg status -C:
221 hg status -C:
222
222
223 $ hg status -C
223 $ hg status -C
224 A added
224 A added
225 A copied
225 A copied
226 modified
226 modified
227 R removed
227 R removed
228 ! deleted
228 ! deleted
229 ? unknown
229 ? unknown
230
230
231 hg status -A:
231 hg status -A:
232
232
233 $ hg status -A
233 $ hg status -A
234 A added
234 A added
235 A copied
235 A copied
236 modified
236 modified
237 R removed
237 R removed
238 ! deleted
238 ! deleted
239 ? unknown
239 ? unknown
240 I ignored
240 I ignored
241 C .hgignore
241 C .hgignore
242 C modified
242 C modified
243
243
244 $ hg status -A -T '{status} {path} {node|shortest}\n'
244 $ hg status -A -T '{status} {path} {node|shortest}\n'
245 A added ffff
245 A added ffff
246 A copied ffff
246 A copied ffff
247 R removed ffff
247 R removed ffff
248 ! deleted ffff
248 ! deleted ffff
249 ? unknown ffff
249 ? unknown ffff
250 I ignored ffff
250 I ignored ffff
251 C .hgignore ffff
251 C .hgignore ffff
252 C modified ffff
252 C modified ffff
253
253
254 $ hg status -A -Tjson
254 $ hg status -A -Tjson
255 [
255 [
256 {
256 {
257 "itemtype": "file",
257 "itemtype": "file",
258 "path": "added",
258 "path": "added",
259 "status": "A"
259 "status": "A"
260 },
260 },
261 {
261 {
262 "itemtype": "file",
262 "itemtype": "file",
263 "path": "copied",
263 "path": "copied",
264 "source": "modified",
264 "source": "modified",
265 "status": "A"
265 "status": "A"
266 },
266 },
267 {
267 {
268 "itemtype": "file",
268 "itemtype": "file",
269 "path": "removed",
269 "path": "removed",
270 "status": "R"
270 "status": "R"
271 },
271 },
272 {
272 {
273 "itemtype": "file",
273 "itemtype": "file",
274 "path": "deleted",
274 "path": "deleted",
275 "status": "!"
275 "status": "!"
276 },
276 },
277 {
277 {
278 "itemtype": "file",
278 "itemtype": "file",
279 "path": "unknown",
279 "path": "unknown",
280 "status": "?"
280 "status": "?"
281 },
281 },
282 {
282 {
283 "itemtype": "file",
283 "itemtype": "file",
284 "path": "ignored",
284 "path": "ignored",
285 "status": "I"
285 "status": "I"
286 },
286 },
287 {
287 {
288 "itemtype": "file",
288 "itemtype": "file",
289 "path": ".hgignore",
289 "path": ".hgignore",
290 "status": "C"
290 "status": "C"
291 },
291 },
292 {
292 {
293 "itemtype": "file",
293 "itemtype": "file",
294 "path": "modified",
294 "path": "modified",
295 "status": "C"
295 "status": "C"
296 }
296 }
297 ]
297 ]
298
298
299 $ hg status -A -Tpickle > pickle
299 $ hg status -A -Tpickle > pickle
300 >>> from __future__ import print_function
300 >>> from __future__ import print_function
301 >>> from mercurial import util
301 >>> from mercurial import util
302 >>> pickle = util.pickle
302 >>> pickle = util.pickle
303 >>> data = sorted((x[b'status'].decode(), x[b'path'].decode()) for x in pickle.load(open("pickle", r"rb")))
303 >>> data = sorted((x[b'status'].decode(), x[b'path'].decode()) for x in pickle.load(open("pickle", r"rb")))
304 >>> for s, p in data: print("%s %s" % (s, p))
304 >>> for s, p in data: print("%s %s" % (s, p))
305 ! deleted
305 ! deleted
306 ? pickle
306 ? pickle
307 ? unknown
307 ? unknown
308 A added
308 A added
309 A copied
309 A copied
310 C .hgignore
310 C .hgignore
311 C modified
311 C modified
312 I ignored
312 I ignored
313 R removed
313 R removed
314 $ rm pickle
314 $ rm pickle
315
315
316 $ echo "^ignoreddir$" > .hgignore
316 $ echo "^ignoreddir$" > .hgignore
317 $ mkdir ignoreddir
317 $ mkdir ignoreddir
318 $ touch ignoreddir/file
318 $ touch ignoreddir/file
319
319
320 Test templater support:
320 Test templater support:
321
321
322 $ hg status -AT "[{status}]\t{if(source, '{source} -> ')}{path}\n"
322 $ hg status -AT "[{status}]\t{if(source, '{source} -> ')}{path}\n"
323 [M] .hgignore
323 [M] .hgignore
324 [A] added
324 [A] added
325 [A] modified -> copied
325 [A] modified -> copied
326 [R] removed
326 [R] removed
327 [!] deleted
327 [!] deleted
328 [?] ignored
328 [?] ignored
329 [?] unknown
329 [?] unknown
330 [I] ignoreddir/file
330 [I] ignoreddir/file
331 [C] modified
331 [C] modified
332 $ hg status -AT default
332 $ hg status -AT default
333 M .hgignore
333 M .hgignore
334 A added
334 A added
335 A copied
335 A copied
336 modified
336 modified
337 R removed
337 R removed
338 ! deleted
338 ! deleted
339 ? ignored
339 ? ignored
340 ? unknown
340 ? unknown
341 I ignoreddir/file
341 I ignoreddir/file
342 C modified
342 C modified
343 $ hg status -T compact
343 $ hg status -T compact
344 abort: "status" not in template map
344 abort: "status" not in template map
345 [255]
345 [255]
346
346
347 hg status ignoreddir/file:
347 hg status ignoreddir/file:
348
348
349 $ hg status ignoreddir/file
349 $ hg status ignoreddir/file
350
350
351 hg status -i ignoreddir/file:
351 hg status -i ignoreddir/file:
352
352
353 $ hg status -i ignoreddir/file
353 $ hg status -i ignoreddir/file
354 I ignoreddir/file
354 I ignoreddir/file
355 $ cd ..
355 $ cd ..
356
356
357 Check 'status -q' and some combinations
357 Check 'status -q' and some combinations
358
358
359 $ hg init repo3
359 $ hg init repo3
360 $ cd repo3
360 $ cd repo3
361 $ touch modified removed deleted ignored
361 $ touch modified removed deleted ignored
362 $ echo "^ignored$" > .hgignore
362 $ echo "^ignored$" > .hgignore
363 $ hg commit -A -m 'initial checkin'
363 $ hg commit -A -m 'initial checkin'
364 adding .hgignore
364 adding .hgignore
365 adding deleted
365 adding deleted
366 adding modified
366 adding modified
367 adding removed
367 adding removed
368 $ touch added unknown ignored
368 $ touch added unknown ignored
369 $ hg add added
369 $ hg add added
370 $ echo "test" >> modified
370 $ echo "test" >> modified
371 $ hg remove removed
371 $ hg remove removed
372 $ rm deleted
372 $ rm deleted
373 $ hg copy modified copied
373 $ hg copy modified copied
374
374
375 Specify working directory revision explicitly, that should be the same as
375 Specify working directory revision explicitly, that should be the same as
376 "hg status"
376 "hg status"
377
377
378 $ hg status --change "wdir()"
378 $ hg status --change "wdir()"
379 M modified
379 M modified
380 A added
380 A added
381 A copied
381 A copied
382 R removed
382 R removed
383 ! deleted
383 ! deleted
384 ? unknown
384 ? unknown
385
385
386 Run status with 2 different flags.
386 Run status with 2 different flags.
387 Check if result is the same or different.
387 Check if result is the same or different.
388 If result is not as expected, raise error
388 If result is not as expected, raise error
389
389
390 $ assert() {
390 $ assert() {
391 > hg status $1 > ../a
391 > hg status $1 > ../a
392 > hg status $2 > ../b
392 > hg status $2 > ../b
393 > if diff ../a ../b > /dev/null; then
393 > if diff ../a ../b > /dev/null; then
394 > out=0
394 > out=0
395 > else
395 > else
396 > out=1
396 > out=1
397 > fi
397 > fi
398 > if [ $3 -eq 0 ]; then
398 > if [ $3 -eq 0 ]; then
399 > df="same"
399 > df="same"
400 > else
400 > else
401 > df="different"
401 > df="different"
402 > fi
402 > fi
403 > if [ $out -ne $3 ]; then
403 > if [ $out -ne $3 ]; then
404 > echo "Error on $1 and $2, should be $df."
404 > echo "Error on $1 and $2, should be $df."
405 > fi
405 > fi
406 > }
406 > }
407
407
408 Assert flag1 flag2 [0-same | 1-different]
408 Assert flag1 flag2 [0-same | 1-different]
409
409
410 $ assert "-q" "-mard" 0
410 $ assert "-q" "-mard" 0
411 $ assert "-A" "-marduicC" 0
411 $ assert "-A" "-marduicC" 0
412 $ assert "-qA" "-mardcC" 0
412 $ assert "-qA" "-mardcC" 0
413 $ assert "-qAui" "-A" 0
413 $ assert "-qAui" "-A" 0
414 $ assert "-qAu" "-marducC" 0
414 $ assert "-qAu" "-marducC" 0
415 $ assert "-qAi" "-mardicC" 0
415 $ assert "-qAi" "-mardicC" 0
416 $ assert "-qu" "-u" 0
416 $ assert "-qu" "-u" 0
417 $ assert "-q" "-u" 1
417 $ assert "-q" "-u" 1
418 $ assert "-m" "-a" 1
418 $ assert "-m" "-a" 1
419 $ assert "-r" "-d" 1
419 $ assert "-r" "-d" 1
420 $ cd ..
420 $ cd ..
421
421
422 $ hg init repo4
422 $ hg init repo4
423 $ cd repo4
423 $ cd repo4
424 $ touch modified removed deleted
424 $ touch modified removed deleted
425 $ hg ci -q -A -m 'initial checkin'
425 $ hg ci -q -A -m 'initial checkin'
426 $ touch added unknown
426 $ touch added unknown
427 $ hg add added
427 $ hg add added
428 $ hg remove removed
428 $ hg remove removed
429 $ rm deleted
429 $ rm deleted
430 $ echo x > modified
430 $ echo x > modified
431 $ hg copy modified copied
431 $ hg copy modified copied
432 $ hg ci -m 'test checkin' -d "1000001 0"
432 $ hg ci -m 'test checkin' -d "1000001 0"
433 $ rm *
433 $ rm *
434 $ touch unrelated
434 $ touch unrelated
435 $ hg ci -q -A -m 'unrelated checkin' -d "1000002 0"
435 $ hg ci -q -A -m 'unrelated checkin' -d "1000002 0"
436
436
437 hg status --change 1:
437 hg status --change 1:
438
438
439 $ hg status --change 1
439 $ hg status --change 1
440 M modified
440 M modified
441 A added
441 A added
442 A copied
442 A copied
443 R removed
443 R removed
444
444
445 hg status --change 1 unrelated:
445 hg status --change 1 unrelated:
446
446
447 $ hg status --change 1 unrelated
447 $ hg status --change 1 unrelated
448
448
449 hg status -C --change 1 added modified copied removed deleted:
449 hg status -C --change 1 added modified copied removed deleted:
450
450
451 $ hg status -C --change 1 added modified copied removed deleted
451 $ hg status -C --change 1 added modified copied removed deleted
452 M modified
452 M modified
453 A added
453 A added
454 A copied
454 A copied
455 modified
455 modified
456 R removed
456 R removed
457
457
458 hg status -A --change 1 and revset:
458 hg status -A --change 1 and revset:
459
459
460 $ hg status -A --change '1|1'
460 $ hg status -A --change '1|1'
461 M modified
461 M modified
462 A added
462 A added
463 A copied
463 A copied
464 modified
464 modified
465 R removed
465 R removed
466 C deleted
466 C deleted
467
467
468 $ cd ..
468 $ cd ..
469
469
470 hg status with --rev and reverted changes:
470 hg status with --rev and reverted changes:
471
471
472 $ hg init reverted-changes-repo
472 $ hg init reverted-changes-repo
473 $ cd reverted-changes-repo
473 $ cd reverted-changes-repo
474 $ echo a > file
474 $ echo a > file
475 $ hg add file
475 $ hg add file
476 $ hg ci -m a
476 $ hg ci -m a
477 $ echo b > file
477 $ echo b > file
478 $ hg ci -m b
478 $ hg ci -m b
479
479
480 reverted file should appear clean
480 reverted file should appear clean
481
481
482 $ hg revert -r 0 .
482 $ hg revert -r 0 .
483 reverting file
483 reverting file
484 $ hg status -A --rev 0
484 $ hg status -A --rev 0
485 C file
485 C file
486
486
487 #if execbit
487 #if execbit
488 reverted file with changed flag should appear modified
488 reverted file with changed flag should appear modified
489
489
490 $ chmod +x file
490 $ chmod +x file
491 $ hg status -A --rev 0
491 $ hg status -A --rev 0
492 M file
492 M file
493
493
494 $ hg revert -r 0 .
494 $ hg revert -r 0 .
495 reverting file
495 reverting file
496
496
497 reverted and committed file with changed flag should appear modified
497 reverted and committed file with changed flag should appear modified
498
498
499 $ hg co -C .
499 $ hg co -C .
500 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
500 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
501 $ chmod +x file
501 $ chmod +x file
502 $ hg ci -m 'change flag'
502 $ hg ci -m 'change flag'
503 $ hg status -A --rev 1 --rev 2
503 $ hg status -A --rev 1 --rev 2
504 M file
504 M file
505 $ hg diff -r 1 -r 2
505 $ hg diff -r 1 -r 2
506
506
507 #endif
507 #endif
508
508
509 $ cd ..
509 $ cd ..
510
510
511 hg status of binary file starting with '\1\n', a separator for metadata:
511 hg status of binary file starting with '\1\n', a separator for metadata:
512
512
513 $ hg init repo5
513 $ hg init repo5
514 $ cd repo5
514 $ cd repo5
515 >>> open("010a", r"wb").write(b"\1\nfoo") and None
515 >>> open("010a", r"wb").write(b"\1\nfoo") and None
516 $ hg ci -q -A -m 'initial checkin'
516 $ hg ci -q -A -m 'initial checkin'
517 $ hg status -A
517 $ hg status -A
518 C 010a
518 C 010a
519
519
520 >>> open("010a", r"wb").write(b"\1\nbar") and None
520 >>> open("010a", r"wb").write(b"\1\nbar") and None
521 $ hg status -A
521 $ hg status -A
522 M 010a
522 M 010a
523 $ hg ci -q -m 'modify 010a'
523 $ hg ci -q -m 'modify 010a'
524 $ hg status -A --rev 0:1
524 $ hg status -A --rev 0:1
525 M 010a
525 M 010a
526
526
527 $ touch empty
527 $ touch empty
528 $ hg ci -q -A -m 'add another file'
528 $ hg ci -q -A -m 'add another file'
529 $ hg status -A --rev 1:2 010a
529 $ hg status -A --rev 1:2 010a
530 C 010a
530 C 010a
531
531
532 $ cd ..
532 $ cd ..
533
533
534 test "hg status" with "directory pattern" which matches against files
534 test "hg status" with "directory pattern" which matches against files
535 only known on target revision.
535 only known on target revision.
536
536
537 $ hg init repo6
537 $ hg init repo6
538 $ cd repo6
538 $ cd repo6
539
539
540 $ echo a > a.txt
540 $ echo a > a.txt
541 $ hg add a.txt
541 $ hg add a.txt
542 $ hg commit -m '#0'
542 $ hg commit -m '#0'
543 $ mkdir -p 1/2/3/4/5
543 $ mkdir -p 1/2/3/4/5
544 $ echo b > 1/2/3/4/5/b.txt
544 $ echo b > 1/2/3/4/5/b.txt
545 $ hg add 1/2/3/4/5/b.txt
545 $ hg add 1/2/3/4/5/b.txt
546 $ hg commit -m '#1'
546 $ hg commit -m '#1'
547
547
548 $ hg update -C 0 > /dev/null
548 $ hg update -C 0 > /dev/null
549 $ hg status -A
549 $ hg status -A
550 C a.txt
550 C a.txt
551
551
552 the directory matching against specified pattern should be removed,
552 the directory matching against specified pattern should be removed,
553 because directory existence prevents 'dirstate.walk()' from showing
553 because directory existence prevents 'dirstate.walk()' from showing
554 warning message about such pattern.
554 warning message about such pattern.
555
555
556 $ test ! -d 1
556 $ test ! -d 1
557 $ hg status -A --rev 1 1/2/3/4/5/b.txt
557 $ hg status -A --rev 1 1/2/3/4/5/b.txt
558 R 1/2/3/4/5/b.txt
558 R 1/2/3/4/5/b.txt
559 $ hg status -A --rev 1 1/2/3/4/5
559 $ hg status -A --rev 1 1/2/3/4/5
560 R 1/2/3/4/5/b.txt
560 R 1/2/3/4/5/b.txt
561 $ hg status -A --rev 1 1/2/3
561 $ hg status -A --rev 1 1/2/3
562 R 1/2/3/4/5/b.txt
562 R 1/2/3/4/5/b.txt
563 $ hg status -A --rev 1 1
563 $ hg status -A --rev 1 1
564 R 1/2/3/4/5/b.txt
564 R 1/2/3/4/5/b.txt
565
565
566 $ hg status --config ui.formatdebug=True --rev 1 1
566 $ hg status --config ui.formatdebug=True --rev 1 1
567 status = [
567 status = [
568 {
568 {
569 'itemtype': 'file',
569 'itemtype': 'file',
570 'path': '1/2/3/4/5/b.txt',
570 'path': '1/2/3/4/5/b.txt',
571 'status': 'R'
571 'status': 'R'
572 },
572 },
573 ]
573 ]
574
574
575 #if windows
575 #if windows
576 $ hg --config ui.slash=false status -A --rev 1 1
576 $ hg --config ui.slash=false status -A --rev 1 1
577 R 1\2\3\4\5\b.txt
577 R 1\2\3\4\5\b.txt
578 #endif
578 #endif
579
579
580 $ cd ..
580 $ cd ..
581
581
582 Status after move overwriting a file (issue4458)
582 Status after move overwriting a file (issue4458)
583 =================================================
583 =================================================
584
584
585
585
586 $ hg init issue4458
586 $ hg init issue4458
587 $ cd issue4458
587 $ cd issue4458
588 $ echo a > a
588 $ echo a > a
589 $ echo b > b
589 $ echo b > b
590 $ hg commit -Am base
590 $ hg commit -Am base
591 adding a
591 adding a
592 adding b
592 adding b
593
593
594
594
595 with --force
595 with --force
596
596
597 $ hg mv b --force a
597 $ hg mv b --force a
598 $ hg st --copies
598 $ hg st --copies
599 M a
599 M a
600 b
600 b
601 R b
601 R b
602 $ hg revert --all
602 $ hg revert --all
603 reverting a
603 reverting a
604 undeleting b
604 undeleting b
605 $ rm *.orig
605 $ rm *.orig
606
606
607 without force
607 without force
608
608
609 $ hg rm a
609 $ hg rm a
610 $ hg st --copies
610 $ hg st --copies
611 R a
611 R a
612 $ hg mv b a
612 $ hg mv b a
613 $ hg st --copies
613 $ hg st --copies
614 M a
614 M a
615 b
615 b
616 R b
616 R b
617
617
618 using ui.statuscopies setting
618 using ui.statuscopies setting
619 $ hg st --config ui.statuscopies=true
619 $ hg st --config ui.statuscopies=true
620 M a
620 M a
621 b
621 b
622 R b
622 R b
623 $ hg st --config ui.statuscopies=false
623 $ hg st --config ui.statuscopies=false
624 M a
624 M a
625 R b
625 R b
626 $ hg st --config ui.tweakdefaults=yes
626 $ hg st --config ui.tweakdefaults=yes
627 M a
627 M a
628 b
628 b
629 R b
629 R b
630
630
631 using log status template (issue5155)
631 using log status template (issue5155)
632 $ hg log -Tstatus -r 'wdir()' -C
632 $ hg log -Tstatus -r 'wdir()' -C
633 changeset: 2147483647:ffffffffffff
633 changeset: 2147483647:ffffffffffff
634 parent: 0:8c55c58b4c0e
634 parent: 0:8c55c58b4c0e
635 user: test
635 user: test
636 date: * (glob)
636 date: * (glob)
637 files:
637 files:
638 M a
638 M a
639 b
639 b
640 R b
640 R b
641
641
642 $ hg log -GTstatus -r 'wdir()' -C
642 $ hg log -GTstatus -r 'wdir()' -C
643 o changeset: 2147483647:ffffffffffff
643 o changeset: 2147483647:ffffffffffff
644 | parent: 0:8c55c58b4c0e
644 | parent: 0:8c55c58b4c0e
645 ~ user: test
645 ~ user: test
646 date: * (glob)
646 date: * (glob)
647 files:
647 files:
648 M a
648 M a
649 b
649 b
650 R b
650 R b
651
651
652
652
653 Other "bug" highlight, the revision status does not report the copy information.
653 Other "bug" highlight, the revision status does not report the copy information.
654 This is buggy behavior.
654 This is buggy behavior.
655
655
656 $ hg commit -m 'blah'
656 $ hg commit -m 'blah'
657 $ hg st --copies --change .
657 $ hg st --copies --change .
658 M a
658 M a
659 R b
659 R b
660
660
661 using log status template, the copy information is displayed correctly.
661 using log status template, the copy information is displayed correctly.
662 $ hg log -Tstatus -r. -C
662 $ hg log -Tstatus -r. -C
663 changeset: 1:6685fde43d21
663 changeset: 1:6685fde43d21
664 tag: tip
664 tag: tip
665 user: test
665 user: test
666 date: * (glob)
666 date: * (glob)
667 summary: blah
667 summary: blah
668 files:
668 files:
669 M a
669 M a
670 b
670 b
671 R b
671 R b
672
672
673
673
674 $ cd ..
674 $ cd ..
675
676 Make sure .hg doesn't show up even as a symlink
677
678 $ hg init repo0
679 $ mkdir symlink-repo0
680 $ cd symlink-repo0
681 $ ln -s ../repo0/.hg
682 $ hg status
General Comments 0
You need to be logged in to leave comments. Login now