##// END OF EJS Templates
rust-status: save new dircache even if just invalidated...
Raphaël Gomès -
r50450:8ee3889b stable
parent child Browse files
Show More
@@ -1,903 +1,913 b''
1 use crate::dirstate::entry::TruncatedTimestamp;
1 use crate::dirstate::entry::TruncatedTimestamp;
2 use crate::dirstate::status::IgnoreFnType;
2 use crate::dirstate::status::IgnoreFnType;
3 use crate::dirstate::status::StatusPath;
3 use crate::dirstate::status::StatusPath;
4 use crate::dirstate_tree::dirstate_map::BorrowedPath;
4 use crate::dirstate_tree::dirstate_map::BorrowedPath;
5 use crate::dirstate_tree::dirstate_map::ChildNodesRef;
5 use crate::dirstate_tree::dirstate_map::ChildNodesRef;
6 use crate::dirstate_tree::dirstate_map::DirstateMap;
6 use crate::dirstate_tree::dirstate_map::DirstateMap;
7 use crate::dirstate_tree::dirstate_map::DirstateVersion;
7 use crate::dirstate_tree::dirstate_map::DirstateVersion;
8 use crate::dirstate_tree::dirstate_map::NodeRef;
8 use crate::dirstate_tree::dirstate_map::NodeRef;
9 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
9 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
10 use crate::matchers::get_ignore_function;
10 use crate::matchers::get_ignore_function;
11 use crate::matchers::Matcher;
11 use crate::matchers::Matcher;
12 use crate::utils::files::get_bytes_from_os_string;
12 use crate::utils::files::get_bytes_from_os_string;
13 use crate::utils::files::get_path_from_bytes;
13 use crate::utils::files::get_path_from_bytes;
14 use crate::utils::hg_path::HgPath;
14 use crate::utils::hg_path::HgPath;
15 use crate::BadMatch;
15 use crate::BadMatch;
16 use crate::DirstateStatus;
16 use crate::DirstateStatus;
17 use crate::HgPathBuf;
17 use crate::HgPathBuf;
18 use crate::HgPathCow;
18 use crate::HgPathCow;
19 use crate::PatternFileWarning;
19 use crate::PatternFileWarning;
20 use crate::StatusError;
20 use crate::StatusError;
21 use crate::StatusOptions;
21 use crate::StatusOptions;
22 use micro_timer::timed;
22 use micro_timer::timed;
23 use once_cell::sync::OnceCell;
23 use once_cell::sync::OnceCell;
24 use rayon::prelude::*;
24 use rayon::prelude::*;
25 use sha1::{Digest, Sha1};
25 use sha1::{Digest, Sha1};
26 use std::borrow::Cow;
26 use std::borrow::Cow;
27 use std::io;
27 use std::io;
28 use std::path::Path;
28 use std::path::Path;
29 use std::path::PathBuf;
29 use std::path::PathBuf;
30 use std::sync::Mutex;
30 use std::sync::Mutex;
31 use std::time::SystemTime;
31 use std::time::SystemTime;
32
32
33 /// Returns the status of the working directory compared to its parent
33 /// Returns the status of the working directory compared to its parent
34 /// changeset.
34 /// changeset.
35 ///
35 ///
36 /// This algorithm is based on traversing the filesystem tree (`fs` in function
36 /// This algorithm is based on traversing the filesystem tree (`fs` in function
37 /// and variable names) and dirstate tree at the same time. The core of this
37 /// and variable names) and dirstate tree at the same time. The core of this
38 /// traversal is the recursive `traverse_fs_directory_and_dirstate` function
38 /// traversal is the recursive `traverse_fs_directory_and_dirstate` function
39 /// and its use of `itertools::merge_join_by`. When reaching a path that only
39 /// and its use of `itertools::merge_join_by`. When reaching a path that only
40 /// exists in one of the two trees, depending on information requested by
40 /// exists in one of the two trees, depending on information requested by
41 /// `options` we may need to traverse the remaining subtree.
41 /// `options` we may need to traverse the remaining subtree.
42 #[timed]
42 #[timed]
43 pub fn status<'dirstate>(
43 pub fn status<'dirstate>(
44 dmap: &'dirstate mut DirstateMap,
44 dmap: &'dirstate mut DirstateMap,
45 matcher: &(dyn Matcher + Sync),
45 matcher: &(dyn Matcher + Sync),
46 root_dir: PathBuf,
46 root_dir: PathBuf,
47 ignore_files: Vec<PathBuf>,
47 ignore_files: Vec<PathBuf>,
48 options: StatusOptions,
48 options: StatusOptions,
49 ) -> Result<(DirstateStatus<'dirstate>, Vec<PatternFileWarning>), StatusError>
49 ) -> Result<(DirstateStatus<'dirstate>, Vec<PatternFileWarning>), StatusError>
50 {
50 {
51 // Force the global rayon threadpool to not exceed 16 concurrent threads.
51 // Force the global rayon threadpool to not exceed 16 concurrent threads.
52 // This is a stop-gap measure until we figure out why using more than 16
52 // This is a stop-gap measure until we figure out why using more than 16
53 // threads makes `status` slower for each additional thread.
53 // threads makes `status` slower for each additional thread.
54 // We use `ok()` in case the global threadpool has already been
54 // We use `ok()` in case the global threadpool has already been
55 // instantiated in `rhg` or some other caller.
55 // instantiated in `rhg` or some other caller.
56 // TODO find the underlying cause and fix it, then remove this.
56 // TODO find the underlying cause and fix it, then remove this.
57 rayon::ThreadPoolBuilder::new()
57 rayon::ThreadPoolBuilder::new()
58 .num_threads(16)
58 .num_threads(16)
59 .build_global()
59 .build_global()
60 .ok();
60 .ok();
61
61
62 let (ignore_fn, warnings, patterns_changed): (IgnoreFnType, _, _) =
62 let (ignore_fn, warnings, patterns_changed): (IgnoreFnType, _, _) =
63 if options.list_ignored || options.list_unknown {
63 if options.list_ignored || options.list_unknown {
64 let (ignore_fn, warnings, changed) = match dmap.dirstate_version {
64 let (ignore_fn, warnings, changed) = match dmap.dirstate_version {
65 DirstateVersion::V1 => {
65 DirstateVersion::V1 => {
66 let (ignore_fn, warnings) = get_ignore_function(
66 let (ignore_fn, warnings) = get_ignore_function(
67 ignore_files,
67 ignore_files,
68 &root_dir,
68 &root_dir,
69 &mut |_pattern_bytes| {},
69 &mut |_pattern_bytes| {},
70 )?;
70 )?;
71 (ignore_fn, warnings, None)
71 (ignore_fn, warnings, None)
72 }
72 }
73 DirstateVersion::V2 => {
73 DirstateVersion::V2 => {
74 let mut hasher = Sha1::new();
74 let mut hasher = Sha1::new();
75 let (ignore_fn, warnings) = get_ignore_function(
75 let (ignore_fn, warnings) = get_ignore_function(
76 ignore_files,
76 ignore_files,
77 &root_dir,
77 &root_dir,
78 &mut |pattern_bytes| hasher.update(pattern_bytes),
78 &mut |pattern_bytes| hasher.update(pattern_bytes),
79 )?;
79 )?;
80 let new_hash = *hasher.finalize().as_ref();
80 let new_hash = *hasher.finalize().as_ref();
81 let changed = new_hash != dmap.ignore_patterns_hash;
81 let changed = new_hash != dmap.ignore_patterns_hash;
82 dmap.ignore_patterns_hash = new_hash;
82 dmap.ignore_patterns_hash = new_hash;
83 (ignore_fn, warnings, Some(changed))
83 (ignore_fn, warnings, Some(changed))
84 }
84 }
85 };
85 };
86 (ignore_fn, warnings, changed)
86 (ignore_fn, warnings, changed)
87 } else {
87 } else {
88 (Box::new(|&_| true), vec![], None)
88 (Box::new(|&_| true), vec![], None)
89 };
89 };
90
90
91 let filesystem_time_at_status_start =
91 let filesystem_time_at_status_start =
92 filesystem_now(&root_dir).ok().map(TruncatedTimestamp::from);
92 filesystem_now(&root_dir).ok().map(TruncatedTimestamp::from);
93
93
94 // If the repository is under the current directory, prefer using a
94 // If the repository is under the current directory, prefer using a
95 // relative path, so the kernel needs to traverse fewer directory in every
95 // relative path, so the kernel needs to traverse fewer directory in every
96 // call to `read_dir` or `symlink_metadata`.
96 // call to `read_dir` or `symlink_metadata`.
97 // This is effective in the common case where the current directory is the
97 // This is effective in the common case where the current directory is the
98 // repository root.
98 // repository root.
99
99
100 // TODO: Better yet would be to use libc functions like `openat` and
100 // TODO: Better yet would be to use libc functions like `openat` and
101 // `fstatat` to remove such repeated traversals entirely, but the standard
101 // `fstatat` to remove such repeated traversals entirely, but the standard
102 // library does not provide APIs based on those.
102 // library does not provide APIs based on those.
103 // Maybe with a crate like https://crates.io/crates/openat instead?
103 // Maybe with a crate like https://crates.io/crates/openat instead?
104 let root_dir = if let Some(relative) = std::env::current_dir()
104 let root_dir = if let Some(relative) = std::env::current_dir()
105 .ok()
105 .ok()
106 .and_then(|cwd| root_dir.strip_prefix(cwd).ok())
106 .and_then(|cwd| root_dir.strip_prefix(cwd).ok())
107 {
107 {
108 relative
108 relative
109 } else {
109 } else {
110 &root_dir
110 &root_dir
111 };
111 };
112
112
113 let outcome = DirstateStatus {
113 let outcome = DirstateStatus {
114 filesystem_time_at_status_start,
114 filesystem_time_at_status_start,
115 ..Default::default()
115 ..Default::default()
116 };
116 };
117 let common = StatusCommon {
117 let common = StatusCommon {
118 dmap,
118 dmap,
119 options,
119 options,
120 matcher,
120 matcher,
121 ignore_fn,
121 ignore_fn,
122 outcome: Mutex::new(outcome),
122 outcome: Mutex::new(outcome),
123 ignore_patterns_have_changed: patterns_changed,
123 ignore_patterns_have_changed: patterns_changed,
124 new_cacheable_directories: Default::default(),
124 new_cacheable_directories: Default::default(),
125 outdated_cached_directories: Default::default(),
125 outdated_cached_directories: Default::default(),
126 filesystem_time_at_status_start,
126 filesystem_time_at_status_start,
127 };
127 };
128 let is_at_repo_root = true;
128 let is_at_repo_root = true;
129 let hg_path = &BorrowedPath::OnDisk(HgPath::new(""));
129 let hg_path = &BorrowedPath::OnDisk(HgPath::new(""));
130 let has_ignored_ancestor = HasIgnoredAncestor::create(None, hg_path);
130 let has_ignored_ancestor = HasIgnoredAncestor::create(None, hg_path);
131 let root_cached_mtime = None;
131 let root_cached_mtime = None;
132 let root_dir_metadata = None;
132 let root_dir_metadata = None;
133 // If the path we have for the repository root is a symlink, do follow it.
133 // If the path we have for the repository root is a symlink, do follow it.
134 // (As opposed to symlinks within the working directory which are not
134 // (As opposed to symlinks within the working directory which are not
135 // followed, using `std::fs::symlink_metadata`.)
135 // followed, using `std::fs::symlink_metadata`.)
136 common.traverse_fs_directory_and_dirstate(
136 common.traverse_fs_directory_and_dirstate(
137 &has_ignored_ancestor,
137 &has_ignored_ancestor,
138 dmap.root.as_ref(),
138 dmap.root.as_ref(),
139 hg_path,
139 hg_path,
140 &root_dir,
140 &root_dir,
141 root_dir_metadata,
141 root_dir_metadata,
142 root_cached_mtime,
142 root_cached_mtime,
143 is_at_repo_root,
143 is_at_repo_root,
144 )?;
144 )?;
145 let mut outcome = common.outcome.into_inner().unwrap();
145 let mut outcome = common.outcome.into_inner().unwrap();
146 let new_cacheable = common.new_cacheable_directories.into_inner().unwrap();
146 let new_cacheable = common.new_cacheable_directories.into_inner().unwrap();
147 let outdated = common.outdated_cached_directories.into_inner().unwrap();
147 let outdated = common.outdated_cached_directories.into_inner().unwrap();
148
148
149 outcome.dirty = common.ignore_patterns_have_changed == Some(true)
149 outcome.dirty = common.ignore_patterns_have_changed == Some(true)
150 || !outdated.is_empty()
150 || !outdated.is_empty()
151 || (!new_cacheable.is_empty()
151 || (!new_cacheable.is_empty()
152 && dmap.dirstate_version == DirstateVersion::V2);
152 && dmap.dirstate_version == DirstateVersion::V2);
153
153
154 // Remove outdated mtimes before adding new mtimes, in case a given
154 // Remove outdated mtimes before adding new mtimes, in case a given
155 // directory is both
155 // directory is both
156 for path in &outdated {
156 for path in &outdated {
157 dmap.clear_cached_mtime(path)?;
157 dmap.clear_cached_mtime(path)?;
158 }
158 }
159 for (path, mtime) in &new_cacheable {
159 for (path, mtime) in &new_cacheable {
160 dmap.set_cached_mtime(path, *mtime)?;
160 dmap.set_cached_mtime(path, *mtime)?;
161 }
161 }
162
162
163 Ok((outcome, warnings))
163 Ok((outcome, warnings))
164 }
164 }
165
165
166 /// Bag of random things needed by various parts of the algorithm. Reduces the
166 /// Bag of random things needed by various parts of the algorithm. Reduces the
167 /// number of parameters passed to functions.
167 /// number of parameters passed to functions.
168 struct StatusCommon<'a, 'tree, 'on_disk: 'tree> {
168 struct StatusCommon<'a, 'tree, 'on_disk: 'tree> {
169 dmap: &'tree DirstateMap<'on_disk>,
169 dmap: &'tree DirstateMap<'on_disk>,
170 options: StatusOptions,
170 options: StatusOptions,
171 matcher: &'a (dyn Matcher + Sync),
171 matcher: &'a (dyn Matcher + Sync),
172 ignore_fn: IgnoreFnType<'a>,
172 ignore_fn: IgnoreFnType<'a>,
173 outcome: Mutex<DirstateStatus<'on_disk>>,
173 outcome: Mutex<DirstateStatus<'on_disk>>,
174 /// New timestamps of directories to be used for caching their readdirs
174 /// New timestamps of directories to be used for caching their readdirs
175 new_cacheable_directories:
175 new_cacheable_directories:
176 Mutex<Vec<(Cow<'on_disk, HgPath>, TruncatedTimestamp)>>,
176 Mutex<Vec<(Cow<'on_disk, HgPath>, TruncatedTimestamp)>>,
177 /// Used to invalidate the readdir cache of directories
177 /// Used to invalidate the readdir cache of directories
178 outdated_cached_directories: Mutex<Vec<Cow<'on_disk, HgPath>>>,
178 outdated_cached_directories: Mutex<Vec<Cow<'on_disk, HgPath>>>,
179
179
180 /// Whether ignore files like `.hgignore` have changed since the previous
180 /// Whether ignore files like `.hgignore` have changed since the previous
181 /// time a `status()` call wrote their hash to the dirstate. `None` means
181 /// time a `status()` call wrote their hash to the dirstate. `None` means
182 /// we don’t know as this run doesn’t list either ignored or uknown files
182 /// we don’t know as this run doesn’t list either ignored or uknown files
183 /// and therefore isn’t reading `.hgignore`.
183 /// and therefore isn’t reading `.hgignore`.
184 ignore_patterns_have_changed: Option<bool>,
184 ignore_patterns_have_changed: Option<bool>,
185
185
186 /// The current time at the start of the `status()` algorithm, as measured
186 /// The current time at the start of the `status()` algorithm, as measured
187 /// and possibly truncated by the filesystem.
187 /// and possibly truncated by the filesystem.
188 filesystem_time_at_status_start: Option<TruncatedTimestamp>,
188 filesystem_time_at_status_start: Option<TruncatedTimestamp>,
189 }
189 }
190
190
191 enum Outcome {
191 enum Outcome {
192 Modified,
192 Modified,
193 Added,
193 Added,
194 Removed,
194 Removed,
195 Deleted,
195 Deleted,
196 Clean,
196 Clean,
197 Ignored,
197 Ignored,
198 Unknown,
198 Unknown,
199 Unsure,
199 Unsure,
200 }
200 }
201
201
202 /// Lazy computation of whether a given path has a hgignored
202 /// Lazy computation of whether a given path has a hgignored
203 /// ancestor.
203 /// ancestor.
204 struct HasIgnoredAncestor<'a> {
204 struct HasIgnoredAncestor<'a> {
205 /// `path` and `parent` constitute the inputs to the computation,
205 /// `path` and `parent` constitute the inputs to the computation,
206 /// `cache` stores the outcome.
206 /// `cache` stores the outcome.
207 path: &'a HgPath,
207 path: &'a HgPath,
208 parent: Option<&'a HasIgnoredAncestor<'a>>,
208 parent: Option<&'a HasIgnoredAncestor<'a>>,
209 cache: OnceCell<bool>,
209 cache: OnceCell<bool>,
210 }
210 }
211
211
212 impl<'a> HasIgnoredAncestor<'a> {
212 impl<'a> HasIgnoredAncestor<'a> {
213 fn create(
213 fn create(
214 parent: Option<&'a HasIgnoredAncestor<'a>>,
214 parent: Option<&'a HasIgnoredAncestor<'a>>,
215 path: &'a HgPath,
215 path: &'a HgPath,
216 ) -> HasIgnoredAncestor<'a> {
216 ) -> HasIgnoredAncestor<'a> {
217 Self {
217 Self {
218 path,
218 path,
219 parent,
219 parent,
220 cache: OnceCell::new(),
220 cache: OnceCell::new(),
221 }
221 }
222 }
222 }
223
223
224 fn force<'b>(&self, ignore_fn: &IgnoreFnType<'b>) -> bool {
224 fn force<'b>(&self, ignore_fn: &IgnoreFnType<'b>) -> bool {
225 match self.parent {
225 match self.parent {
226 None => false,
226 None => false,
227 Some(parent) => {
227 Some(parent) => {
228 *(parent.cache.get_or_init(|| {
228 *(parent.cache.get_or_init(|| {
229 parent.force(ignore_fn) || ignore_fn(&self.path)
229 parent.force(ignore_fn) || ignore_fn(&self.path)
230 }))
230 }))
231 }
231 }
232 }
232 }
233 }
233 }
234 }
234 }
235
235
236 impl<'a, 'tree, 'on_disk> StatusCommon<'a, 'tree, 'on_disk> {
236 impl<'a, 'tree, 'on_disk> StatusCommon<'a, 'tree, 'on_disk> {
237 fn push_outcome(
237 fn push_outcome(
238 &self,
238 &self,
239 which: Outcome,
239 which: Outcome,
240 dirstate_node: &NodeRef<'tree, 'on_disk>,
240 dirstate_node: &NodeRef<'tree, 'on_disk>,
241 ) -> Result<(), DirstateV2ParseError> {
241 ) -> Result<(), DirstateV2ParseError> {
242 let path = dirstate_node
242 let path = dirstate_node
243 .full_path_borrowed(self.dmap.on_disk)?
243 .full_path_borrowed(self.dmap.on_disk)?
244 .detach_from_tree();
244 .detach_from_tree();
245 let copy_source = if self.options.list_copies {
245 let copy_source = if self.options.list_copies {
246 dirstate_node
246 dirstate_node
247 .copy_source_borrowed(self.dmap.on_disk)?
247 .copy_source_borrowed(self.dmap.on_disk)?
248 .map(|source| source.detach_from_tree())
248 .map(|source| source.detach_from_tree())
249 } else {
249 } else {
250 None
250 None
251 };
251 };
252 self.push_outcome_common(which, path, copy_source);
252 self.push_outcome_common(which, path, copy_source);
253 Ok(())
253 Ok(())
254 }
254 }
255
255
256 fn push_outcome_without_copy_source(
256 fn push_outcome_without_copy_source(
257 &self,
257 &self,
258 which: Outcome,
258 which: Outcome,
259 path: &BorrowedPath<'_, 'on_disk>,
259 path: &BorrowedPath<'_, 'on_disk>,
260 ) {
260 ) {
261 self.push_outcome_common(which, path.detach_from_tree(), None)
261 self.push_outcome_common(which, path.detach_from_tree(), None)
262 }
262 }
263
263
264 fn push_outcome_common(
264 fn push_outcome_common(
265 &self,
265 &self,
266 which: Outcome,
266 which: Outcome,
267 path: HgPathCow<'on_disk>,
267 path: HgPathCow<'on_disk>,
268 copy_source: Option<HgPathCow<'on_disk>>,
268 copy_source: Option<HgPathCow<'on_disk>>,
269 ) {
269 ) {
270 let mut outcome = self.outcome.lock().unwrap();
270 let mut outcome = self.outcome.lock().unwrap();
271 let vec = match which {
271 let vec = match which {
272 Outcome::Modified => &mut outcome.modified,
272 Outcome::Modified => &mut outcome.modified,
273 Outcome::Added => &mut outcome.added,
273 Outcome::Added => &mut outcome.added,
274 Outcome::Removed => &mut outcome.removed,
274 Outcome::Removed => &mut outcome.removed,
275 Outcome::Deleted => &mut outcome.deleted,
275 Outcome::Deleted => &mut outcome.deleted,
276 Outcome::Clean => &mut outcome.clean,
276 Outcome::Clean => &mut outcome.clean,
277 Outcome::Ignored => &mut outcome.ignored,
277 Outcome::Ignored => &mut outcome.ignored,
278 Outcome::Unknown => &mut outcome.unknown,
278 Outcome::Unknown => &mut outcome.unknown,
279 Outcome::Unsure => &mut outcome.unsure,
279 Outcome::Unsure => &mut outcome.unsure,
280 };
280 };
281 vec.push(StatusPath { path, copy_source });
281 vec.push(StatusPath { path, copy_source });
282 }
282 }
283
283
284 fn read_dir(
284 fn read_dir(
285 &self,
285 &self,
286 hg_path: &HgPath,
286 hg_path: &HgPath,
287 fs_path: &Path,
287 fs_path: &Path,
288 is_at_repo_root: bool,
288 is_at_repo_root: bool,
289 ) -> Result<Vec<DirEntry>, ()> {
289 ) -> Result<Vec<DirEntry>, ()> {
290 DirEntry::read_dir(fs_path, is_at_repo_root)
290 DirEntry::read_dir(fs_path, is_at_repo_root)
291 .map_err(|error| self.io_error(error, hg_path))
291 .map_err(|error| self.io_error(error, hg_path))
292 }
292 }
293
293
294 fn io_error(&self, error: std::io::Error, hg_path: &HgPath) {
294 fn io_error(&self, error: std::io::Error, hg_path: &HgPath) {
295 let errno = error.raw_os_error().expect("expected real OS error");
295 let errno = error.raw_os_error().expect("expected real OS error");
296 self.outcome
296 self.outcome
297 .lock()
297 .lock()
298 .unwrap()
298 .unwrap()
299 .bad
299 .bad
300 .push((hg_path.to_owned().into(), BadMatch::OsError(errno)))
300 .push((hg_path.to_owned().into(), BadMatch::OsError(errno)))
301 }
301 }
302
302
303 fn check_for_outdated_directory_cache(
303 fn check_for_outdated_directory_cache(
304 &self,
304 &self,
305 dirstate_node: &NodeRef<'tree, 'on_disk>,
305 dirstate_node: &NodeRef<'tree, 'on_disk>,
306 ) -> Result<(), DirstateV2ParseError> {
306 ) -> Result<bool, DirstateV2ParseError> {
307 if self.ignore_patterns_have_changed == Some(true)
307 if self.ignore_patterns_have_changed == Some(true)
308 && dirstate_node.cached_directory_mtime()?.is_some()
308 && dirstate_node.cached_directory_mtime()?.is_some()
309 {
309 {
310 self.outdated_cached_directories.lock().unwrap().push(
310 self.outdated_cached_directories.lock().unwrap().push(
311 dirstate_node
311 dirstate_node
312 .full_path_borrowed(self.dmap.on_disk)?
312 .full_path_borrowed(self.dmap.on_disk)?
313 .detach_from_tree(),
313 .detach_from_tree(),
314 )
314 );
315 return Ok(true);
315 }
316 }
316 Ok(())
317 Ok(false)
317 }
318 }
318
319
319 /// If this returns true, we can get accurate results by only using
320 /// If this returns true, we can get accurate results by only using
320 /// `symlink_metadata` for child nodes that exist in the dirstate and don’t
321 /// `symlink_metadata` for child nodes that exist in the dirstate and don’t
321 /// need to call `read_dir`.
322 /// need to call `read_dir`.
322 fn can_skip_fs_readdir(
323 fn can_skip_fs_readdir(
323 &self,
324 &self,
324 directory_metadata: Option<&std::fs::Metadata>,
325 directory_metadata: Option<&std::fs::Metadata>,
325 cached_directory_mtime: Option<TruncatedTimestamp>,
326 cached_directory_mtime: Option<TruncatedTimestamp>,
326 ) -> bool {
327 ) -> bool {
327 if !self.options.list_unknown && !self.options.list_ignored {
328 if !self.options.list_unknown && !self.options.list_ignored {
328 // All states that we care about listing have corresponding
329 // All states that we care about listing have corresponding
329 // dirstate entries.
330 // dirstate entries.
330 // This happens for example with `hg status -mard`.
331 // This happens for example with `hg status -mard`.
331 return true;
332 return true;
332 }
333 }
333 if !self.options.list_ignored
334 if !self.options.list_ignored
334 && self.ignore_patterns_have_changed == Some(false)
335 && self.ignore_patterns_have_changed == Some(false)
335 {
336 {
336 if let Some(cached_mtime) = cached_directory_mtime {
337 if let Some(cached_mtime) = cached_directory_mtime {
337 // The dirstate contains a cached mtime for this directory, set
338 // The dirstate contains a cached mtime for this directory, set
338 // by a previous run of the `status` algorithm which found this
339 // by a previous run of the `status` algorithm which found this
339 // directory eligible for `read_dir` caching.
340 // directory eligible for `read_dir` caching.
340 if let Some(meta) = directory_metadata {
341 if let Some(meta) = directory_metadata {
341 if cached_mtime
342 if cached_mtime
342 .likely_equal_to_mtime_of(meta)
343 .likely_equal_to_mtime_of(meta)
343 .unwrap_or(false)
344 .unwrap_or(false)
344 {
345 {
345 // The mtime of that directory has not changed
346 // The mtime of that directory has not changed
346 // since then, which means that the results of
347 // since then, which means that the results of
347 // `read_dir` should also be unchanged.
348 // `read_dir` should also be unchanged.
348 return true;
349 return true;
349 }
350 }
350 }
351 }
351 }
352 }
352 }
353 }
353 false
354 false
354 }
355 }
355
356
356 /// Returns whether all child entries of the filesystem directory have a
357 /// Returns whether all child entries of the filesystem directory have a
357 /// corresponding dirstate node or are ignored.
358 /// corresponding dirstate node or are ignored.
358 fn traverse_fs_directory_and_dirstate<'ancestor>(
359 fn traverse_fs_directory_and_dirstate<'ancestor>(
359 &self,
360 &self,
360 has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>,
361 has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>,
361 dirstate_nodes: ChildNodesRef<'tree, 'on_disk>,
362 dirstate_nodes: ChildNodesRef<'tree, 'on_disk>,
362 directory_hg_path: &BorrowedPath<'tree, 'on_disk>,
363 directory_hg_path: &BorrowedPath<'tree, 'on_disk>,
363 directory_fs_path: &Path,
364 directory_fs_path: &Path,
364 directory_metadata: Option<&std::fs::Metadata>,
365 directory_metadata: Option<&std::fs::Metadata>,
365 cached_directory_mtime: Option<TruncatedTimestamp>,
366 cached_directory_mtime: Option<TruncatedTimestamp>,
366 is_at_repo_root: bool,
367 is_at_repo_root: bool,
367 ) -> Result<bool, DirstateV2ParseError> {
368 ) -> Result<bool, DirstateV2ParseError> {
368 if self.can_skip_fs_readdir(directory_metadata, cached_directory_mtime)
369 if self.can_skip_fs_readdir(directory_metadata, cached_directory_mtime)
369 {
370 {
370 dirstate_nodes
371 dirstate_nodes
371 .par_iter()
372 .par_iter()
372 .map(|dirstate_node| {
373 .map(|dirstate_node| {
373 let fs_path = directory_fs_path.join(get_path_from_bytes(
374 let fs_path = directory_fs_path.join(get_path_from_bytes(
374 dirstate_node.base_name(self.dmap.on_disk)?.as_bytes(),
375 dirstate_node.base_name(self.dmap.on_disk)?.as_bytes(),
375 ));
376 ));
376 match std::fs::symlink_metadata(&fs_path) {
377 match std::fs::symlink_metadata(&fs_path) {
377 Ok(fs_metadata) => self.traverse_fs_and_dirstate(
378 Ok(fs_metadata) => self.traverse_fs_and_dirstate(
378 &fs_path,
379 &fs_path,
379 &fs_metadata,
380 &fs_metadata,
380 dirstate_node,
381 dirstate_node,
381 has_ignored_ancestor,
382 has_ignored_ancestor,
382 ),
383 ),
383 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
384 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
384 self.traverse_dirstate_only(dirstate_node)
385 self.traverse_dirstate_only(dirstate_node)
385 }
386 }
386 Err(error) => {
387 Err(error) => {
387 let hg_path =
388 let hg_path =
388 dirstate_node.full_path(self.dmap.on_disk)?;
389 dirstate_node.full_path(self.dmap.on_disk)?;
389 Ok(self.io_error(error, hg_path))
390 Ok(self.io_error(error, hg_path))
390 }
391 }
391 }
392 }
392 })
393 })
393 .collect::<Result<_, _>>()?;
394 .collect::<Result<_, _>>()?;
394
395
395 // We don’t know, so conservatively say this isn’t the case
396 // We don’t know, so conservatively say this isn’t the case
396 let children_all_have_dirstate_node_or_are_ignored = false;
397 let children_all_have_dirstate_node_or_are_ignored = false;
397
398
398 return Ok(children_all_have_dirstate_node_or_are_ignored);
399 return Ok(children_all_have_dirstate_node_or_are_ignored);
399 }
400 }
400
401
401 let mut fs_entries = if let Ok(entries) = self.read_dir(
402 let mut fs_entries = if let Ok(entries) = self.read_dir(
402 directory_hg_path,
403 directory_hg_path,
403 directory_fs_path,
404 directory_fs_path,
404 is_at_repo_root,
405 is_at_repo_root,
405 ) {
406 ) {
406 entries
407 entries
407 } else {
408 } else {
408 // Treat an unreadable directory (typically because of insufficient
409 // Treat an unreadable directory (typically because of insufficient
409 // permissions) like an empty directory. `self.read_dir` has
410 // permissions) like an empty directory. `self.read_dir` has
410 // already called `self.io_error` so a warning will be emitted.
411 // already called `self.io_error` so a warning will be emitted.
411 Vec::new()
412 Vec::new()
412 };
413 };
413
414
414 // `merge_join_by` requires both its input iterators to be sorted:
415 // `merge_join_by` requires both its input iterators to be sorted:
415
416
416 let dirstate_nodes = dirstate_nodes.sorted();
417 let dirstate_nodes = dirstate_nodes.sorted();
417 // `sort_unstable_by_key` doesn’t allow keys borrowing from the value:
418 // `sort_unstable_by_key` doesn’t allow keys borrowing from the value:
418 // https://github.com/rust-lang/rust/issues/34162
419 // https://github.com/rust-lang/rust/issues/34162
419 fs_entries.sort_unstable_by(|e1, e2| e1.base_name.cmp(&e2.base_name));
420 fs_entries.sort_unstable_by(|e1, e2| e1.base_name.cmp(&e2.base_name));
420
421
421 // Propagate here any error that would happen inside the comparison
422 // Propagate here any error that would happen inside the comparison
422 // callback below
423 // callback below
423 for dirstate_node in &dirstate_nodes {
424 for dirstate_node in &dirstate_nodes {
424 dirstate_node.base_name(self.dmap.on_disk)?;
425 dirstate_node.base_name(self.dmap.on_disk)?;
425 }
426 }
426 itertools::merge_join_by(
427 itertools::merge_join_by(
427 dirstate_nodes,
428 dirstate_nodes,
428 &fs_entries,
429 &fs_entries,
429 |dirstate_node, fs_entry| {
430 |dirstate_node, fs_entry| {
430 // This `unwrap` never panics because we already propagated
431 // This `unwrap` never panics because we already propagated
431 // those errors above
432 // those errors above
432 dirstate_node
433 dirstate_node
433 .base_name(self.dmap.on_disk)
434 .base_name(self.dmap.on_disk)
434 .unwrap()
435 .unwrap()
435 .cmp(&fs_entry.base_name)
436 .cmp(&fs_entry.base_name)
436 },
437 },
437 )
438 )
438 .par_bridge()
439 .par_bridge()
439 .map(|pair| {
440 .map(|pair| {
440 use itertools::EitherOrBoth::*;
441 use itertools::EitherOrBoth::*;
441 let has_dirstate_node_or_is_ignored;
442 let has_dirstate_node_or_is_ignored;
442 match pair {
443 match pair {
443 Both(dirstate_node, fs_entry) => {
444 Both(dirstate_node, fs_entry) => {
444 self.traverse_fs_and_dirstate(
445 self.traverse_fs_and_dirstate(
445 &fs_entry.full_path,
446 &fs_entry.full_path,
446 &fs_entry.metadata,
447 &fs_entry.metadata,
447 dirstate_node,
448 dirstate_node,
448 has_ignored_ancestor,
449 has_ignored_ancestor,
449 )?;
450 )?;
450 has_dirstate_node_or_is_ignored = true
451 has_dirstate_node_or_is_ignored = true
451 }
452 }
452 Left(dirstate_node) => {
453 Left(dirstate_node) => {
453 self.traverse_dirstate_only(dirstate_node)?;
454 self.traverse_dirstate_only(dirstate_node)?;
454 has_dirstate_node_or_is_ignored = true;
455 has_dirstate_node_or_is_ignored = true;
455 }
456 }
456 Right(fs_entry) => {
457 Right(fs_entry) => {
457 has_dirstate_node_or_is_ignored = self.traverse_fs_only(
458 has_dirstate_node_or_is_ignored = self.traverse_fs_only(
458 has_ignored_ancestor.force(&self.ignore_fn),
459 has_ignored_ancestor.force(&self.ignore_fn),
459 directory_hg_path,
460 directory_hg_path,
460 fs_entry,
461 fs_entry,
461 )
462 )
462 }
463 }
463 }
464 }
464 Ok(has_dirstate_node_or_is_ignored)
465 Ok(has_dirstate_node_or_is_ignored)
465 })
466 })
466 .try_reduce(|| true, |a, b| Ok(a && b))
467 .try_reduce(|| true, |a, b| Ok(a && b))
467 }
468 }
468
469
469 fn traverse_fs_and_dirstate<'ancestor>(
470 fn traverse_fs_and_dirstate<'ancestor>(
470 &self,
471 &self,
471 fs_path: &Path,
472 fs_path: &Path,
472 fs_metadata: &std::fs::Metadata,
473 fs_metadata: &std::fs::Metadata,
473 dirstate_node: NodeRef<'tree, 'on_disk>,
474 dirstate_node: NodeRef<'tree, 'on_disk>,
474 has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>,
475 has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>,
475 ) -> Result<(), DirstateV2ParseError> {
476 ) -> Result<(), DirstateV2ParseError> {
476 self.check_for_outdated_directory_cache(&dirstate_node)?;
477 let outdated_dircache =
478 self.check_for_outdated_directory_cache(&dirstate_node)?;
477 let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?;
479 let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?;
478 let file_type = fs_metadata.file_type();
480 let file_type = fs_metadata.file_type();
479 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
481 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
480 if !file_or_symlink {
482 if !file_or_symlink {
481 // If we previously had a file here, it was removed (with
483 // If we previously had a file here, it was removed (with
482 // `hg rm` or similar) or deleted before it could be
484 // `hg rm` or similar) or deleted before it could be
483 // replaced by a directory or something else.
485 // replaced by a directory or something else.
484 self.mark_removed_or_deleted_if_file(&dirstate_node)?;
486 self.mark_removed_or_deleted_if_file(&dirstate_node)?;
485 }
487 }
486 if file_type.is_dir() {
488 if file_type.is_dir() {
487 if self.options.collect_traversed_dirs {
489 if self.options.collect_traversed_dirs {
488 self.outcome
490 self.outcome
489 .lock()
491 .lock()
490 .unwrap()
492 .unwrap()
491 .traversed
493 .traversed
492 .push(hg_path.detach_from_tree())
494 .push(hg_path.detach_from_tree())
493 }
495 }
494 let is_ignored = HasIgnoredAncestor::create(
496 let is_ignored = HasIgnoredAncestor::create(
495 Some(&has_ignored_ancestor),
497 Some(&has_ignored_ancestor),
496 hg_path,
498 hg_path,
497 );
499 );
498 let is_at_repo_root = false;
500 let is_at_repo_root = false;
499 let children_all_have_dirstate_node_or_are_ignored = self
501 let children_all_have_dirstate_node_or_are_ignored = self
500 .traverse_fs_directory_and_dirstate(
502 .traverse_fs_directory_and_dirstate(
501 &is_ignored,
503 &is_ignored,
502 dirstate_node.children(self.dmap.on_disk)?,
504 dirstate_node.children(self.dmap.on_disk)?,
503 hg_path,
505 hg_path,
504 fs_path,
506 fs_path,
505 Some(fs_metadata),
507 Some(fs_metadata),
506 dirstate_node.cached_directory_mtime()?,
508 dirstate_node.cached_directory_mtime()?,
507 is_at_repo_root,
509 is_at_repo_root,
508 )?;
510 )?;
509 self.maybe_save_directory_mtime(
511 self.maybe_save_directory_mtime(
510 children_all_have_dirstate_node_or_are_ignored,
512 children_all_have_dirstate_node_or_are_ignored,
511 fs_metadata,
513 fs_metadata,
512 dirstate_node,
514 dirstate_node,
515 outdated_dircache,
513 )?
516 )?
514 } else {
517 } else {
515 if file_or_symlink && self.matcher.matches(&hg_path) {
518 if file_or_symlink && self.matcher.matches(&hg_path) {
516 if let Some(entry) = dirstate_node.entry()? {
519 if let Some(entry) = dirstate_node.entry()? {
517 if !entry.any_tracked() {
520 if !entry.any_tracked() {
518 // Forward-compat if we start tracking unknown/ignored
521 // Forward-compat if we start tracking unknown/ignored
519 // files for caching reasons
522 // files for caching reasons
520 self.mark_unknown_or_ignored(
523 self.mark_unknown_or_ignored(
521 has_ignored_ancestor.force(&self.ignore_fn),
524 has_ignored_ancestor.force(&self.ignore_fn),
522 &hg_path,
525 &hg_path,
523 );
526 );
524 }
527 }
525 if entry.added() {
528 if entry.added() {
526 self.push_outcome(Outcome::Added, &dirstate_node)?;
529 self.push_outcome(Outcome::Added, &dirstate_node)?;
527 } else if entry.removed() {
530 } else if entry.removed() {
528 self.push_outcome(Outcome::Removed, &dirstate_node)?;
531 self.push_outcome(Outcome::Removed, &dirstate_node)?;
529 } else if entry.modified() {
532 } else if entry.modified() {
530 self.push_outcome(Outcome::Modified, &dirstate_node)?;
533 self.push_outcome(Outcome::Modified, &dirstate_node)?;
531 } else {
534 } else {
532 self.handle_normal_file(&dirstate_node, fs_metadata)?;
535 self.handle_normal_file(&dirstate_node, fs_metadata)?;
533 }
536 }
534 } else {
537 } else {
535 // `node.entry.is_none()` indicates a "directory"
538 // `node.entry.is_none()` indicates a "directory"
536 // node, but the filesystem has a file
539 // node, but the filesystem has a file
537 self.mark_unknown_or_ignored(
540 self.mark_unknown_or_ignored(
538 has_ignored_ancestor.force(&self.ignore_fn),
541 has_ignored_ancestor.force(&self.ignore_fn),
539 hg_path,
542 hg_path,
540 );
543 );
541 }
544 }
542 }
545 }
543
546
544 for child_node in dirstate_node.children(self.dmap.on_disk)?.iter()
547 for child_node in dirstate_node.children(self.dmap.on_disk)?.iter()
545 {
548 {
546 self.traverse_dirstate_only(child_node)?
549 self.traverse_dirstate_only(child_node)?
547 }
550 }
548 }
551 }
549 Ok(())
552 Ok(())
550 }
553 }
551
554
555 /// Save directory mtime if applicable.
556 ///
557 /// `outdated_directory_cache` is `true` if we've just invalidated the
558 /// cache for this directory in `check_for_outdated_directory_cache`,
559 /// which forces the update.
552 fn maybe_save_directory_mtime(
560 fn maybe_save_directory_mtime(
553 &self,
561 &self,
554 children_all_have_dirstate_node_or_are_ignored: bool,
562 children_all_have_dirstate_node_or_are_ignored: bool,
555 directory_metadata: &std::fs::Metadata,
563 directory_metadata: &std::fs::Metadata,
556 dirstate_node: NodeRef<'tree, 'on_disk>,
564 dirstate_node: NodeRef<'tree, 'on_disk>,
565 outdated_directory_cache: bool,
557 ) -> Result<(), DirstateV2ParseError> {
566 ) -> Result<(), DirstateV2ParseError> {
558 if !children_all_have_dirstate_node_or_are_ignored {
567 if !children_all_have_dirstate_node_or_are_ignored {
559 return Ok(());
568 return Ok(());
560 }
569 }
561 // All filesystem directory entries from `read_dir` have a
570 // All filesystem directory entries from `read_dir` have a
562 // corresponding node in the dirstate, so we can reconstitute the
571 // corresponding node in the dirstate, so we can reconstitute the
563 // names of those entries without calling `read_dir` again.
572 // names of those entries without calling `read_dir` again.
564
573
565 // TODO: use let-else here and below when available:
574 // TODO: use let-else here and below when available:
566 // https://github.com/rust-lang/rust/issues/87335
575 // https://github.com/rust-lang/rust/issues/87335
567 let status_start = if let Some(status_start) =
576 let status_start = if let Some(status_start) =
568 &self.filesystem_time_at_status_start
577 &self.filesystem_time_at_status_start
569 {
578 {
570 status_start
579 status_start
571 } else {
580 } else {
572 return Ok(());
581 return Ok(());
573 };
582 };
574
583
575 // Although the Rust standard library’s `SystemTime` type
584 // Although the Rust standard library’s `SystemTime` type
576 // has nanosecond precision, the times reported for a
585 // has nanosecond precision, the times reported for a
577 // directory’s (or file’s) modified time may have lower
586 // directory’s (or file’s) modified time may have lower
578 // resolution based on the filesystem (for example ext3
587 // resolution based on the filesystem (for example ext3
579 // only stores integer seconds), kernel (see
588 // only stores integer seconds), kernel (see
580 // https://stackoverflow.com/a/14393315/1162888), etc.
589 // https://stackoverflow.com/a/14393315/1162888), etc.
581 let directory_mtime = if let Ok(option) =
590 let directory_mtime = if let Ok(option) =
582 TruncatedTimestamp::for_reliable_mtime_of(
591 TruncatedTimestamp::for_reliable_mtime_of(
583 directory_metadata,
592 directory_metadata,
584 status_start,
593 status_start,
585 ) {
594 ) {
586 if let Some(directory_mtime) = option {
595 if let Some(directory_mtime) = option {
587 directory_mtime
596 directory_mtime
588 } else {
597 } else {
589 // The directory was modified too recently,
598 // The directory was modified too recently,
590 // don’t cache its `read_dir` results.
599 // don’t cache its `read_dir` results.
591 //
600 //
592 // 1. A change to this directory (direct child was
601 // 1. A change to this directory (direct child was
593 // added or removed) cause its mtime to be set
602 // added or removed) cause its mtime to be set
594 // (possibly truncated) to `directory_mtime`
603 // (possibly truncated) to `directory_mtime`
595 // 2. This `status` algorithm calls `read_dir`
604 // 2. This `status` algorithm calls `read_dir`
596 // 3. An other change is made to the same directory is
605 // 3. An other change is made to the same directory is
597 // made so that calling `read_dir` agin would give
606 // made so that calling `read_dir` agin would give
598 // different results, but soon enough after 1. that
607 // different results, but soon enough after 1. that
599 // the mtime stays the same
608 // the mtime stays the same
600 //
609 //
601 // On a system where the time resolution poor, this
610 // On a system where the time resolution poor, this
602 // scenario is not unlikely if all three steps are caused
611 // scenario is not unlikely if all three steps are caused
603 // by the same script.
612 // by the same script.
604 return Ok(());
613 return Ok(());
605 }
614 }
606 } else {
615 } else {
607 // OS/libc does not support mtime?
616 // OS/libc does not support mtime?
608 return Ok(());
617 return Ok(());
609 };
618 };
610 // We’ve observed (through `status_start`) that time has
619 // We’ve observed (through `status_start`) that time has
611 // “progressed” since `directory_mtime`, so any further
620 // “progressed” since `directory_mtime`, so any further
612 // change to this directory is extremely likely to cause a
621 // change to this directory is extremely likely to cause a
613 // different mtime.
622 // different mtime.
614 //
623 //
615 // Having the same mtime again is not entirely impossible
624 // Having the same mtime again is not entirely impossible
616 // since the system clock is not monotonous. It could jump
625 // since the system clock is not monotonous. It could jump
617 // backward to some point before `directory_mtime`, then a
626 // backward to some point before `directory_mtime`, then a
618 // directory change could potentially happen during exactly
627 // directory change could potentially happen during exactly
619 // the wrong tick.
628 // the wrong tick.
620 //
629 //
621 // We deem this scenario (unlike the previous one) to be
630 // We deem this scenario (unlike the previous one) to be
622 // unlikely enough in practice.
631 // unlikely enough in practice.
623
632
624 let is_up_to_date =
633 let is_up_to_date = if let Some(cached) =
625 if let Some(cached) = dirstate_node.cached_directory_mtime()? {
634 dirstate_node.cached_directory_mtime()?
626 cached.likely_equal(directory_mtime)
635 {
627 } else {
636 !outdated_directory_cache && cached.likely_equal(directory_mtime)
628 false
637 } else {
629 };
638 false
639 };
630 if !is_up_to_date {
640 if !is_up_to_date {
631 let hg_path = dirstate_node
641 let hg_path = dirstate_node
632 .full_path_borrowed(self.dmap.on_disk)?
642 .full_path_borrowed(self.dmap.on_disk)?
633 .detach_from_tree();
643 .detach_from_tree();
634 self.new_cacheable_directories
644 self.new_cacheable_directories
635 .lock()
645 .lock()
636 .unwrap()
646 .unwrap()
637 .push((hg_path, directory_mtime))
647 .push((hg_path, directory_mtime))
638 }
648 }
639 Ok(())
649 Ok(())
640 }
650 }
641
651
642 /// A file that is clean in the dirstate was found in the filesystem
652 /// A file that is clean in the dirstate was found in the filesystem
643 fn handle_normal_file(
653 fn handle_normal_file(
644 &self,
654 &self,
645 dirstate_node: &NodeRef<'tree, 'on_disk>,
655 dirstate_node: &NodeRef<'tree, 'on_disk>,
646 fs_metadata: &std::fs::Metadata,
656 fs_metadata: &std::fs::Metadata,
647 ) -> Result<(), DirstateV2ParseError> {
657 ) -> Result<(), DirstateV2ParseError> {
648 // Keep the low 31 bits
658 // Keep the low 31 bits
649 fn truncate_u64(value: u64) -> i32 {
659 fn truncate_u64(value: u64) -> i32 {
650 (value & 0x7FFF_FFFF) as i32
660 (value & 0x7FFF_FFFF) as i32
651 }
661 }
652
662
653 let entry = dirstate_node
663 let entry = dirstate_node
654 .entry()?
664 .entry()?
655 .expect("handle_normal_file called with entry-less node");
665 .expect("handle_normal_file called with entry-less node");
656 let mode_changed =
666 let mode_changed =
657 || self.options.check_exec && entry.mode_changed(fs_metadata);
667 || self.options.check_exec && entry.mode_changed(fs_metadata);
658 let size = entry.size();
668 let size = entry.size();
659 let size_changed = size != truncate_u64(fs_metadata.len());
669 let size_changed = size != truncate_u64(fs_metadata.len());
660 if size >= 0 && size_changed && fs_metadata.file_type().is_symlink() {
670 if size >= 0 && size_changed && fs_metadata.file_type().is_symlink() {
661 // issue6456: Size returned may be longer due to encryption
671 // issue6456: Size returned may be longer due to encryption
662 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
672 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
663 self.push_outcome(Outcome::Unsure, dirstate_node)?
673 self.push_outcome(Outcome::Unsure, dirstate_node)?
664 } else if dirstate_node.has_copy_source()
674 } else if dirstate_node.has_copy_source()
665 || entry.is_from_other_parent()
675 || entry.is_from_other_parent()
666 || (size >= 0 && (size_changed || mode_changed()))
676 || (size >= 0 && (size_changed || mode_changed()))
667 {
677 {
668 self.push_outcome(Outcome::Modified, dirstate_node)?
678 self.push_outcome(Outcome::Modified, dirstate_node)?
669 } else {
679 } else {
670 let mtime_looks_clean;
680 let mtime_looks_clean;
671 if let Some(dirstate_mtime) = entry.truncated_mtime() {
681 if let Some(dirstate_mtime) = entry.truncated_mtime() {
672 let fs_mtime = TruncatedTimestamp::for_mtime_of(fs_metadata)
682 let fs_mtime = TruncatedTimestamp::for_mtime_of(fs_metadata)
673 .expect("OS/libc does not support mtime?");
683 .expect("OS/libc does not support mtime?");
674 // There might be a change in the future if for example the
684 // There might be a change in the future if for example the
675 // internal clock become off while process run, but this is a
685 // internal clock become off while process run, but this is a
676 // case where the issues the user would face
686 // case where the issues the user would face
677 // would be a lot worse and there is nothing we
687 // would be a lot worse and there is nothing we
678 // can really do.
688 // can really do.
679 mtime_looks_clean = fs_mtime.likely_equal(dirstate_mtime)
689 mtime_looks_clean = fs_mtime.likely_equal(dirstate_mtime)
680 } else {
690 } else {
681 // No mtime in the dirstate entry
691 // No mtime in the dirstate entry
682 mtime_looks_clean = false
692 mtime_looks_clean = false
683 };
693 };
684 if !mtime_looks_clean {
694 if !mtime_looks_clean {
685 self.push_outcome(Outcome::Unsure, dirstate_node)?
695 self.push_outcome(Outcome::Unsure, dirstate_node)?
686 } else if self.options.list_clean {
696 } else if self.options.list_clean {
687 self.push_outcome(Outcome::Clean, dirstate_node)?
697 self.push_outcome(Outcome::Clean, dirstate_node)?
688 }
698 }
689 }
699 }
690 Ok(())
700 Ok(())
691 }
701 }
692
702
693 /// A node in the dirstate tree has no corresponding filesystem entry
703 /// A node in the dirstate tree has no corresponding filesystem entry
694 fn traverse_dirstate_only(
704 fn traverse_dirstate_only(
695 &self,
705 &self,
696 dirstate_node: NodeRef<'tree, 'on_disk>,
706 dirstate_node: NodeRef<'tree, 'on_disk>,
697 ) -> Result<(), DirstateV2ParseError> {
707 ) -> Result<(), DirstateV2ParseError> {
698 self.check_for_outdated_directory_cache(&dirstate_node)?;
708 self.check_for_outdated_directory_cache(&dirstate_node)?;
699 self.mark_removed_or_deleted_if_file(&dirstate_node)?;
709 self.mark_removed_or_deleted_if_file(&dirstate_node)?;
700 dirstate_node
710 dirstate_node
701 .children(self.dmap.on_disk)?
711 .children(self.dmap.on_disk)?
702 .par_iter()
712 .par_iter()
703 .map(|child_node| self.traverse_dirstate_only(child_node))
713 .map(|child_node| self.traverse_dirstate_only(child_node))
704 .collect()
714 .collect()
705 }
715 }
706
716
707 /// A node in the dirstate tree has no corresponding *file* on the
717 /// A node in the dirstate tree has no corresponding *file* on the
708 /// filesystem
718 /// filesystem
709 ///
719 ///
710 /// Does nothing on a "directory" node
720 /// Does nothing on a "directory" node
711 fn mark_removed_or_deleted_if_file(
721 fn mark_removed_or_deleted_if_file(
712 &self,
722 &self,
713 dirstate_node: &NodeRef<'tree, 'on_disk>,
723 dirstate_node: &NodeRef<'tree, 'on_disk>,
714 ) -> Result<(), DirstateV2ParseError> {
724 ) -> Result<(), DirstateV2ParseError> {
715 if let Some(entry) = dirstate_node.entry()? {
725 if let Some(entry) = dirstate_node.entry()? {
716 if !entry.any_tracked() {
726 if !entry.any_tracked() {
717 // Future-compat for when we start storing ignored and unknown
727 // Future-compat for when we start storing ignored and unknown
718 // files for caching reasons
728 // files for caching reasons
719 return Ok(());
729 return Ok(());
720 }
730 }
721 let path = dirstate_node.full_path(self.dmap.on_disk)?;
731 let path = dirstate_node.full_path(self.dmap.on_disk)?;
722 if self.matcher.matches(path) {
732 if self.matcher.matches(path) {
723 if entry.removed() {
733 if entry.removed() {
724 self.push_outcome(Outcome::Removed, dirstate_node)?
734 self.push_outcome(Outcome::Removed, dirstate_node)?
725 } else {
735 } else {
726 self.push_outcome(Outcome::Deleted, &dirstate_node)?
736 self.push_outcome(Outcome::Deleted, &dirstate_node)?
727 }
737 }
728 }
738 }
729 }
739 }
730 Ok(())
740 Ok(())
731 }
741 }
732
742
733 /// Something in the filesystem has no corresponding dirstate node
743 /// Something in the filesystem has no corresponding dirstate node
734 ///
744 ///
735 /// Returns whether that path is ignored
745 /// Returns whether that path is ignored
736 fn traverse_fs_only(
746 fn traverse_fs_only(
737 &self,
747 &self,
738 has_ignored_ancestor: bool,
748 has_ignored_ancestor: bool,
739 directory_hg_path: &HgPath,
749 directory_hg_path: &HgPath,
740 fs_entry: &DirEntry,
750 fs_entry: &DirEntry,
741 ) -> bool {
751 ) -> bool {
742 let hg_path = directory_hg_path.join(&fs_entry.base_name);
752 let hg_path = directory_hg_path.join(&fs_entry.base_name);
743 let file_type = fs_entry.metadata.file_type();
753 let file_type = fs_entry.metadata.file_type();
744 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
754 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
745 if file_type.is_dir() {
755 if file_type.is_dir() {
746 let is_ignored =
756 let is_ignored =
747 has_ignored_ancestor || (self.ignore_fn)(&hg_path);
757 has_ignored_ancestor || (self.ignore_fn)(&hg_path);
748 let traverse_children = if is_ignored {
758 let traverse_children = if is_ignored {
749 // Descendants of an ignored directory are all ignored
759 // Descendants of an ignored directory are all ignored
750 self.options.list_ignored
760 self.options.list_ignored
751 } else {
761 } else {
752 // Descendants of an unknown directory may be either unknown or
762 // Descendants of an unknown directory may be either unknown or
753 // ignored
763 // ignored
754 self.options.list_unknown || self.options.list_ignored
764 self.options.list_unknown || self.options.list_ignored
755 };
765 };
756 if traverse_children {
766 if traverse_children {
757 let is_at_repo_root = false;
767 let is_at_repo_root = false;
758 if let Ok(children_fs_entries) = self.read_dir(
768 if let Ok(children_fs_entries) = self.read_dir(
759 &hg_path,
769 &hg_path,
760 &fs_entry.full_path,
770 &fs_entry.full_path,
761 is_at_repo_root,
771 is_at_repo_root,
762 ) {
772 ) {
763 children_fs_entries.par_iter().for_each(|child_fs_entry| {
773 children_fs_entries.par_iter().for_each(|child_fs_entry| {
764 self.traverse_fs_only(
774 self.traverse_fs_only(
765 is_ignored,
775 is_ignored,
766 &hg_path,
776 &hg_path,
767 child_fs_entry,
777 child_fs_entry,
768 );
778 );
769 })
779 })
770 }
780 }
771 if self.options.collect_traversed_dirs {
781 if self.options.collect_traversed_dirs {
772 self.outcome.lock().unwrap().traversed.push(hg_path.into())
782 self.outcome.lock().unwrap().traversed.push(hg_path.into())
773 }
783 }
774 }
784 }
775 is_ignored
785 is_ignored
776 } else {
786 } else {
777 if file_or_symlink {
787 if file_or_symlink {
778 if self.matcher.matches(&hg_path) {
788 if self.matcher.matches(&hg_path) {
779 self.mark_unknown_or_ignored(
789 self.mark_unknown_or_ignored(
780 has_ignored_ancestor,
790 has_ignored_ancestor,
781 &BorrowedPath::InMemory(&hg_path),
791 &BorrowedPath::InMemory(&hg_path),
782 )
792 )
783 } else {
793 } else {
784 // We haven’t computed whether this path is ignored. It
794 // We haven’t computed whether this path is ignored. It
785 // might not be, and a future run of status might have a
795 // might not be, and a future run of status might have a
786 // different matcher that matches it. So treat it as not
796 // different matcher that matches it. So treat it as not
787 // ignored. That is, inhibit readdir caching of the parent
797 // ignored. That is, inhibit readdir caching of the parent
788 // directory.
798 // directory.
789 false
799 false
790 }
800 }
791 } else {
801 } else {
792 // This is neither a directory, a plain file, or a symlink.
802 // This is neither a directory, a plain file, or a symlink.
793 // Treat it like an ignored file.
803 // Treat it like an ignored file.
794 true
804 true
795 }
805 }
796 }
806 }
797 }
807 }
798
808
799 /// Returns whether that path is ignored
809 /// Returns whether that path is ignored
800 fn mark_unknown_or_ignored(
810 fn mark_unknown_or_ignored(
801 &self,
811 &self,
802 has_ignored_ancestor: bool,
812 has_ignored_ancestor: bool,
803 hg_path: &BorrowedPath<'_, 'on_disk>,
813 hg_path: &BorrowedPath<'_, 'on_disk>,
804 ) -> bool {
814 ) -> bool {
805 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path);
815 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path);
806 if is_ignored {
816 if is_ignored {
807 if self.options.list_ignored {
817 if self.options.list_ignored {
808 self.push_outcome_without_copy_source(
818 self.push_outcome_without_copy_source(
809 Outcome::Ignored,
819 Outcome::Ignored,
810 hg_path,
820 hg_path,
811 )
821 )
812 }
822 }
813 } else {
823 } else {
814 if self.options.list_unknown {
824 if self.options.list_unknown {
815 self.push_outcome_without_copy_source(
825 self.push_outcome_without_copy_source(
816 Outcome::Unknown,
826 Outcome::Unknown,
817 hg_path,
827 hg_path,
818 )
828 )
819 }
829 }
820 }
830 }
821 is_ignored
831 is_ignored
822 }
832 }
823 }
833 }
824
834
825 struct DirEntry {
835 struct DirEntry {
826 base_name: HgPathBuf,
836 base_name: HgPathBuf,
827 full_path: PathBuf,
837 full_path: PathBuf,
828 metadata: std::fs::Metadata,
838 metadata: std::fs::Metadata,
829 }
839 }
830
840
831 impl DirEntry {
841 impl DirEntry {
832 /// Returns **unsorted** entries in the given directory, with name and
842 /// Returns **unsorted** entries in the given directory, with name and
833 /// metadata.
843 /// metadata.
834 ///
844 ///
835 /// If a `.hg` sub-directory is encountered:
845 /// If a `.hg` sub-directory is encountered:
836 ///
846 ///
837 /// * At the repository root, ignore that sub-directory
847 /// * At the repository root, ignore that sub-directory
838 /// * Elsewhere, we’re listing the content of a sub-repo. Return an empty
848 /// * Elsewhere, we’re listing the content of a sub-repo. Return an empty
839 /// list instead.
849 /// list instead.
840 fn read_dir(path: &Path, is_at_repo_root: bool) -> io::Result<Vec<Self>> {
850 fn read_dir(path: &Path, is_at_repo_root: bool) -> io::Result<Vec<Self>> {
841 // `read_dir` returns a "not found" error for the empty path
851 // `read_dir` returns a "not found" error for the empty path
842 let at_cwd = path == Path::new("");
852 let at_cwd = path == Path::new("");
843 let read_dir_path = if at_cwd { Path::new(".") } else { path };
853 let read_dir_path = if at_cwd { Path::new(".") } else { path };
844 let mut results = Vec::new();
854 let mut results = Vec::new();
845 for entry in read_dir_path.read_dir()? {
855 for entry in read_dir_path.read_dir()? {
846 let entry = entry?;
856 let entry = entry?;
847 let metadata = match entry.metadata() {
857 let metadata = match entry.metadata() {
848 Ok(v) => v,
858 Ok(v) => v,
849 Err(e) => {
859 Err(e) => {
850 // race with file deletion?
860 // race with file deletion?
851 if e.kind() == std::io::ErrorKind::NotFound {
861 if e.kind() == std::io::ErrorKind::NotFound {
852 continue;
862 continue;
853 } else {
863 } else {
854 return Err(e);
864 return Err(e);
855 }
865 }
856 }
866 }
857 };
867 };
858 let file_name = entry.file_name();
868 let file_name = entry.file_name();
859 // FIXME don't do this when cached
869 // FIXME don't do this when cached
860 if file_name == ".hg" {
870 if file_name == ".hg" {
861 if is_at_repo_root {
871 if is_at_repo_root {
862 // Skip the repo’s own .hg (might be a symlink)
872 // Skip the repo’s own .hg (might be a symlink)
863 continue;
873 continue;
864 } else if metadata.is_dir() {
874 } else if metadata.is_dir() {
865 // A .hg sub-directory at another location means a subrepo,
875 // A .hg sub-directory at another location means a subrepo,
866 // skip it entirely.
876 // skip it entirely.
867 return Ok(Vec::new());
877 return Ok(Vec::new());
868 }
878 }
869 }
879 }
870 let full_path = if at_cwd {
880 let full_path = if at_cwd {
871 file_name.clone().into()
881 file_name.clone().into()
872 } else {
882 } else {
873 entry.path()
883 entry.path()
874 };
884 };
875 let base_name = get_bytes_from_os_string(file_name).into();
885 let base_name = get_bytes_from_os_string(file_name).into();
876 results.push(DirEntry {
886 results.push(DirEntry {
877 base_name,
887 base_name,
878 full_path,
888 full_path,
879 metadata,
889 metadata,
880 })
890 })
881 }
891 }
882 Ok(results)
892 Ok(results)
883 }
893 }
884 }
894 }
885
895
886 /// Return the `mtime` of a temporary file newly-created in the `.hg` directory
896 /// Return the `mtime` of a temporary file newly-created in the `.hg` directory
887 /// of the give repository.
897 /// of the give repository.
888 ///
898 ///
889 /// This is similar to `SystemTime::now()`, with the result truncated to the
899 /// This is similar to `SystemTime::now()`, with the result truncated to the
890 /// same time resolution as other files’ modification times. Using `.hg`
900 /// same time resolution as other files’ modification times. Using `.hg`
891 /// instead of the system’s default temporary directory (such as `/tmp`) makes
901 /// instead of the system’s default temporary directory (such as `/tmp`) makes
892 /// it more likely the temporary file is in the same disk partition as contents
902 /// it more likely the temporary file is in the same disk partition as contents
893 /// of the working directory, which can matter since different filesystems may
903 /// of the working directory, which can matter since different filesystems may
894 /// store timestamps with different resolutions.
904 /// store timestamps with different resolutions.
895 ///
905 ///
896 /// This may fail, typically if we lack write permissions. In that case we
906 /// This may fail, typically if we lack write permissions. In that case we
897 /// should continue the `status()` algoritm anyway and consider the current
907 /// should continue the `status()` algoritm anyway and consider the current
898 /// date/time to be unknown.
908 /// date/time to be unknown.
899 fn filesystem_now(repo_root: &Path) -> Result<SystemTime, io::Error> {
909 fn filesystem_now(repo_root: &Path) -> Result<SystemTime, io::Error> {
900 tempfile::tempfile_in(repo_root.join(".hg"))?
910 tempfile::tempfile_in(repo_root.join(".hg"))?
901 .metadata()?
911 .metadata()?
902 .modified()
912 .modified()
903 }
913 }
@@ -1,1010 +1,1002 b''
1 #testcases dirstate-v1 dirstate-v2
1 #testcases dirstate-v1 dirstate-v2
2
2
3 #if dirstate-v2
3 #if dirstate-v2
4 $ cat >> $HGRCPATH << EOF
4 $ cat >> $HGRCPATH << EOF
5 > [format]
5 > [format]
6 > use-dirstate-v2=1
6 > use-dirstate-v2=1
7 > [storage]
7 > [storage]
8 > dirstate-v2.slow-path=allow
8 > dirstate-v2.slow-path=allow
9 > EOF
9 > EOF
10 #endif
10 #endif
11
11
12 $ hg init repo1
12 $ hg init repo1
13 $ cd repo1
13 $ cd repo1
14 $ mkdir a b a/1 b/1 b/2
14 $ mkdir a b a/1 b/1 b/2
15 $ touch in_root a/in_a b/in_b a/1/in_a_1 b/1/in_b_1 b/2/in_b_2
15 $ touch in_root a/in_a b/in_b a/1/in_a_1 b/1/in_b_1 b/2/in_b_2
16
16
17 hg status in repo root:
17 hg status in repo root:
18
18
19 $ hg status
19 $ hg status
20 ? a/1/in_a_1
20 ? a/1/in_a_1
21 ? a/in_a
21 ? a/in_a
22 ? b/1/in_b_1
22 ? b/1/in_b_1
23 ? b/2/in_b_2
23 ? b/2/in_b_2
24 ? b/in_b
24 ? b/in_b
25 ? in_root
25 ? in_root
26
26
27 hg status . in repo root:
27 hg status . in repo root:
28
28
29 $ hg status .
29 $ hg status .
30 ? a/1/in_a_1
30 ? a/1/in_a_1
31 ? a/in_a
31 ? a/in_a
32 ? b/1/in_b_1
32 ? b/1/in_b_1
33 ? b/2/in_b_2
33 ? b/2/in_b_2
34 ? b/in_b
34 ? b/in_b
35 ? in_root
35 ? in_root
36
36
37 $ hg status --cwd a
37 $ hg status --cwd a
38 ? a/1/in_a_1
38 ? a/1/in_a_1
39 ? a/in_a
39 ? a/in_a
40 ? b/1/in_b_1
40 ? b/1/in_b_1
41 ? b/2/in_b_2
41 ? b/2/in_b_2
42 ? b/in_b
42 ? b/in_b
43 ? in_root
43 ? in_root
44 $ hg status --cwd a .
44 $ hg status --cwd a .
45 ? 1/in_a_1
45 ? 1/in_a_1
46 ? in_a
46 ? in_a
47 $ hg status --cwd a ..
47 $ hg status --cwd a ..
48 ? 1/in_a_1
48 ? 1/in_a_1
49 ? in_a
49 ? in_a
50 ? ../b/1/in_b_1
50 ? ../b/1/in_b_1
51 ? ../b/2/in_b_2
51 ? ../b/2/in_b_2
52 ? ../b/in_b
52 ? ../b/in_b
53 ? ../in_root
53 ? ../in_root
54
54
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 ? b/1/in_b_1
58 ? b/1/in_b_1
59 ? b/2/in_b_2
59 ? b/2/in_b_2
60 ? b/in_b
60 ? b/in_b
61 ? in_root
61 ? in_root
62 $ hg status --cwd b .
62 $ hg status --cwd b .
63 ? 1/in_b_1
63 ? 1/in_b_1
64 ? 2/in_b_2
64 ? 2/in_b_2
65 ? in_b
65 ? in_b
66 $ hg status --cwd b ..
66 $ hg status --cwd b ..
67 ? ../a/1/in_a_1
67 ? ../a/1/in_a_1
68 ? ../a/in_a
68 ? ../a/in_a
69 ? 1/in_b_1
69 ? 1/in_b_1
70 ? 2/in_b_2
70 ? 2/in_b_2
71 ? in_b
71 ? in_b
72 ? ../in_root
72 ? ../in_root
73
73
74 $ hg status --cwd a/1
74 $ hg status --cwd a/1
75 ? a/1/in_a_1
75 ? a/1/in_a_1
76 ? a/in_a
76 ? a/in_a
77 ? b/1/in_b_1
77 ? b/1/in_b_1
78 ? b/2/in_b_2
78 ? b/2/in_b_2
79 ? b/in_b
79 ? b/in_b
80 ? in_root
80 ? in_root
81 $ hg status --cwd a/1 .
81 $ hg status --cwd a/1 .
82 ? in_a_1
82 ? in_a_1
83 $ hg status --cwd a/1 ..
83 $ hg status --cwd a/1 ..
84 ? in_a_1
84 ? in_a_1
85 ? ../in_a
85 ? ../in_a
86
86
87 $ hg status --cwd b/1
87 $ hg status --cwd b/1
88 ? a/1/in_a_1
88 ? a/1/in_a_1
89 ? a/in_a
89 ? a/in_a
90 ? b/1/in_b_1
90 ? b/1/in_b_1
91 ? b/2/in_b_2
91 ? b/2/in_b_2
92 ? b/in_b
92 ? b/in_b
93 ? in_root
93 ? in_root
94 $ hg status --cwd b/1 .
94 $ hg status --cwd b/1 .
95 ? in_b_1
95 ? in_b_1
96 $ hg status --cwd b/1 ..
96 $ hg status --cwd b/1 ..
97 ? in_b_1
97 ? in_b_1
98 ? ../2/in_b_2
98 ? ../2/in_b_2
99 ? ../in_b
99 ? ../in_b
100
100
101 $ hg status --cwd b/2
101 $ hg status --cwd b/2
102 ? a/1/in_a_1
102 ? a/1/in_a_1
103 ? a/in_a
103 ? a/in_a
104 ? b/1/in_b_1
104 ? b/1/in_b_1
105 ? b/2/in_b_2
105 ? b/2/in_b_2
106 ? b/in_b
106 ? b/in_b
107 ? in_root
107 ? in_root
108 $ hg status --cwd b/2 .
108 $ hg status --cwd b/2 .
109 ? in_b_2
109 ? in_b_2
110 $ hg status --cwd b/2 ..
110 $ hg status --cwd b/2 ..
111 ? ../1/in_b_1
111 ? ../1/in_b_1
112 ? in_b_2
112 ? in_b_2
113 ? ../in_b
113 ? ../in_b
114
114
115 combining patterns with root and patterns without a root works
115 combining patterns with root and patterns without a root works
116
116
117 $ hg st a/in_a re:.*b$
117 $ hg st a/in_a re:.*b$
118 ? a/in_a
118 ? a/in_a
119 ? b/in_b
119 ? b/in_b
120
120
121 tweaking defaults works
121 tweaking defaults works
122 $ hg status --cwd a --config ui.tweakdefaults=yes
122 $ hg status --cwd a --config ui.tweakdefaults=yes
123 ? 1/in_a_1
123 ? 1/in_a_1
124 ? in_a
124 ? in_a
125 ? ../b/1/in_b_1
125 ? ../b/1/in_b_1
126 ? ../b/2/in_b_2
126 ? ../b/2/in_b_2
127 ? ../b/in_b
127 ? ../b/in_b
128 ? ../in_root
128 ? ../in_root
129 $ HGPLAIN=1 hg status --cwd a --config ui.tweakdefaults=yes
129 $ HGPLAIN=1 hg status --cwd a --config ui.tweakdefaults=yes
130 ? a/1/in_a_1 (glob)
130 ? a/1/in_a_1 (glob)
131 ? a/in_a (glob)
131 ? a/in_a (glob)
132 ? b/1/in_b_1 (glob)
132 ? b/1/in_b_1 (glob)
133 ? b/2/in_b_2 (glob)
133 ? b/2/in_b_2 (glob)
134 ? b/in_b (glob)
134 ? b/in_b (glob)
135 ? in_root
135 ? in_root
136 $ HGPLAINEXCEPT=tweakdefaults hg status --cwd a --config ui.tweakdefaults=yes
136 $ HGPLAINEXCEPT=tweakdefaults hg status --cwd a --config ui.tweakdefaults=yes
137 ? 1/in_a_1
137 ? 1/in_a_1
138 ? in_a
138 ? in_a
139 ? ../b/1/in_b_1
139 ? ../b/1/in_b_1
140 ? ../b/2/in_b_2
140 ? ../b/2/in_b_2
141 ? ../b/in_b
141 ? ../b/in_b
142 ? ../in_root (glob)
142 ? ../in_root (glob)
143
143
144 relative paths can be requested
144 relative paths can be requested
145
145
146 $ hg status --cwd a --config ui.relative-paths=yes
146 $ hg status --cwd a --config ui.relative-paths=yes
147 ? 1/in_a_1
147 ? 1/in_a_1
148 ? in_a
148 ? in_a
149 ? ../b/1/in_b_1
149 ? ../b/1/in_b_1
150 ? ../b/2/in_b_2
150 ? ../b/2/in_b_2
151 ? ../b/in_b
151 ? ../b/in_b
152 ? ../in_root
152 ? ../in_root
153
153
154 $ hg status --cwd a . --config ui.relative-paths=legacy
154 $ hg status --cwd a . --config ui.relative-paths=legacy
155 ? 1/in_a_1
155 ? 1/in_a_1
156 ? in_a
156 ? in_a
157 $ hg status --cwd a . --config ui.relative-paths=no
157 $ hg status --cwd a . --config ui.relative-paths=no
158 ? a/1/in_a_1
158 ? a/1/in_a_1
159 ? a/in_a
159 ? a/in_a
160
160
161 commands.status.relative overrides ui.relative-paths
161 commands.status.relative overrides ui.relative-paths
162
162
163 $ cat >> $HGRCPATH <<EOF
163 $ cat >> $HGRCPATH <<EOF
164 > [ui]
164 > [ui]
165 > relative-paths = False
165 > relative-paths = False
166 > [commands]
166 > [commands]
167 > status.relative = True
167 > status.relative = True
168 > EOF
168 > EOF
169 $ hg status --cwd a
169 $ hg status --cwd a
170 ? 1/in_a_1
170 ? 1/in_a_1
171 ? in_a
171 ? in_a
172 ? ../b/1/in_b_1
172 ? ../b/1/in_b_1
173 ? ../b/2/in_b_2
173 ? ../b/2/in_b_2
174 ? ../b/in_b
174 ? ../b/in_b
175 ? ../in_root
175 ? ../in_root
176 $ HGPLAIN=1 hg status --cwd a
176 $ HGPLAIN=1 hg status --cwd a
177 ? a/1/in_a_1 (glob)
177 ? a/1/in_a_1 (glob)
178 ? a/in_a (glob)
178 ? a/in_a (glob)
179 ? b/1/in_b_1 (glob)
179 ? b/1/in_b_1 (glob)
180 ? b/2/in_b_2 (glob)
180 ? b/2/in_b_2 (glob)
181 ? b/in_b (glob)
181 ? b/in_b (glob)
182 ? in_root
182 ? in_root
183
183
184 if relative paths are explicitly off, tweakdefaults doesn't change it
184 if relative paths are explicitly off, tweakdefaults doesn't change it
185 $ cat >> $HGRCPATH <<EOF
185 $ cat >> $HGRCPATH <<EOF
186 > [commands]
186 > [commands]
187 > status.relative = False
187 > status.relative = False
188 > EOF
188 > EOF
189 $ hg status --cwd a --config ui.tweakdefaults=yes
189 $ hg status --cwd a --config ui.tweakdefaults=yes
190 ? a/1/in_a_1
190 ? a/1/in_a_1
191 ? a/in_a
191 ? a/in_a
192 ? b/1/in_b_1
192 ? b/1/in_b_1
193 ? b/2/in_b_2
193 ? b/2/in_b_2
194 ? b/in_b
194 ? b/in_b
195 ? in_root
195 ? in_root
196
196
197 $ cd ..
197 $ cd ..
198
198
199 $ hg init repo2
199 $ hg init repo2
200 $ cd repo2
200 $ cd repo2
201 $ touch modified removed deleted ignored
201 $ touch modified removed deleted ignored
202 $ echo "^ignored$" > .hgignore
202 $ echo "^ignored$" > .hgignore
203 $ hg ci -A -m 'initial checkin'
203 $ hg ci -A -m 'initial checkin'
204 adding .hgignore
204 adding .hgignore
205 adding deleted
205 adding deleted
206 adding modified
206 adding modified
207 adding removed
207 adding removed
208 $ touch modified added unknown ignored
208 $ touch modified added unknown ignored
209 $ hg add added
209 $ hg add added
210 $ hg remove removed
210 $ hg remove removed
211 $ rm deleted
211 $ rm deleted
212
212
213 hg status:
213 hg status:
214
214
215 $ hg status
215 $ hg status
216 A added
216 A added
217 R removed
217 R removed
218 ! deleted
218 ! deleted
219 ? unknown
219 ? unknown
220
220
221 hg status -n:
221 hg status -n:
222 $ env RHG_ON_UNSUPPORTED=abort hg status -n
222 $ env RHG_ON_UNSUPPORTED=abort hg status -n
223 added
223 added
224 removed
224 removed
225 deleted
225 deleted
226 unknown
226 unknown
227
227
228 hg status modified added removed deleted unknown never-existed ignored:
228 hg status modified added removed deleted unknown never-existed ignored:
229
229
230 $ hg status modified added removed deleted unknown never-existed ignored
230 $ hg status modified added removed deleted unknown never-existed ignored
231 never-existed: * (glob)
231 never-existed: * (glob)
232 A added
232 A added
233 R removed
233 R removed
234 ! deleted
234 ! deleted
235 ? unknown
235 ? unknown
236
236
237 $ hg copy modified copied
237 $ hg copy modified copied
238
238
239 hg status -C:
239 hg status -C:
240
240
241 $ hg status -C
241 $ hg status -C
242 A added
242 A added
243 A copied
243 A copied
244 modified
244 modified
245 R removed
245 R removed
246 ! deleted
246 ! deleted
247 ? unknown
247 ? unknown
248
248
249 hg status -A:
249 hg status -A:
250
250
251 $ hg status -A
251 $ hg status -A
252 A added
252 A added
253 A copied
253 A copied
254 modified
254 modified
255 R removed
255 R removed
256 ! deleted
256 ! deleted
257 ? unknown
257 ? unknown
258 I ignored
258 I ignored
259 C .hgignore
259 C .hgignore
260 C modified
260 C modified
261
261
262 $ hg status -A -T '{status} {path} {node|shortest}\n'
262 $ hg status -A -T '{status} {path} {node|shortest}\n'
263 A added ffff
263 A added ffff
264 A copied ffff
264 A copied ffff
265 R removed ffff
265 R removed ffff
266 ! deleted ffff
266 ! deleted ffff
267 ? unknown ffff
267 ? unknown ffff
268 I ignored ffff
268 I ignored ffff
269 C .hgignore ffff
269 C .hgignore ffff
270 C modified ffff
270 C modified ffff
271
271
272 $ hg status -A -Tjson
272 $ hg status -A -Tjson
273 [
273 [
274 {
274 {
275 "itemtype": "file",
275 "itemtype": "file",
276 "path": "added",
276 "path": "added",
277 "status": "A"
277 "status": "A"
278 },
278 },
279 {
279 {
280 "itemtype": "file",
280 "itemtype": "file",
281 "path": "copied",
281 "path": "copied",
282 "source": "modified",
282 "source": "modified",
283 "status": "A"
283 "status": "A"
284 },
284 },
285 {
285 {
286 "itemtype": "file",
286 "itemtype": "file",
287 "path": "removed",
287 "path": "removed",
288 "status": "R"
288 "status": "R"
289 },
289 },
290 {
290 {
291 "itemtype": "file",
291 "itemtype": "file",
292 "path": "deleted",
292 "path": "deleted",
293 "status": "!"
293 "status": "!"
294 },
294 },
295 {
295 {
296 "itemtype": "file",
296 "itemtype": "file",
297 "path": "unknown",
297 "path": "unknown",
298 "status": "?"
298 "status": "?"
299 },
299 },
300 {
300 {
301 "itemtype": "file",
301 "itemtype": "file",
302 "path": "ignored",
302 "path": "ignored",
303 "status": "I"
303 "status": "I"
304 },
304 },
305 {
305 {
306 "itemtype": "file",
306 "itemtype": "file",
307 "path": ".hgignore",
307 "path": ".hgignore",
308 "status": "C"
308 "status": "C"
309 },
309 },
310 {
310 {
311 "itemtype": "file",
311 "itemtype": "file",
312 "path": "modified",
312 "path": "modified",
313 "status": "C"
313 "status": "C"
314 }
314 }
315 ]
315 ]
316
316
317 $ hg status -A -Tpickle > pickle
317 $ hg status -A -Tpickle > pickle
318 >>> import pickle
318 >>> import pickle
319 >>> from mercurial import util
319 >>> from mercurial import util
320 >>> data = sorted((x[b'status'].decode(), x[b'path'].decode()) for x in pickle.load(open("pickle", r"rb")))
320 >>> data = sorted((x[b'status'].decode(), x[b'path'].decode()) for x in pickle.load(open("pickle", r"rb")))
321 >>> for s, p in data: print("%s %s" % (s, p))
321 >>> for s, p in data: print("%s %s" % (s, p))
322 ! deleted
322 ! deleted
323 ? pickle
323 ? pickle
324 ? unknown
324 ? unknown
325 A added
325 A added
326 A copied
326 A copied
327 C .hgignore
327 C .hgignore
328 C modified
328 C modified
329 I ignored
329 I ignored
330 R removed
330 R removed
331 $ rm pickle
331 $ rm pickle
332
332
333 $ echo "^ignoreddir$" > .hgignore
333 $ echo "^ignoreddir$" > .hgignore
334 $ mkdir ignoreddir
334 $ mkdir ignoreddir
335 $ touch ignoreddir/file
335 $ touch ignoreddir/file
336
336
337 Test templater support:
337 Test templater support:
338
338
339 $ hg status -AT "[{status}]\t{if(source, '{source} -> ')}{path}\n"
339 $ hg status -AT "[{status}]\t{if(source, '{source} -> ')}{path}\n"
340 [M] .hgignore
340 [M] .hgignore
341 [A] added
341 [A] added
342 [A] modified -> copied
342 [A] modified -> copied
343 [R] removed
343 [R] removed
344 [!] deleted
344 [!] deleted
345 [?] ignored
345 [?] ignored
346 [?] unknown
346 [?] unknown
347 [I] ignoreddir/file
347 [I] ignoreddir/file
348 [C] modified
348 [C] modified
349 $ hg status -AT default
349 $ hg status -AT default
350 M .hgignore
350 M .hgignore
351 A added
351 A added
352 A copied
352 A copied
353 modified
353 modified
354 R removed
354 R removed
355 ! deleted
355 ! deleted
356 ? ignored
356 ? ignored
357 ? unknown
357 ? unknown
358 I ignoreddir/file
358 I ignoreddir/file
359 C modified
359 C modified
360 $ hg status -T compact
360 $ hg status -T compact
361 abort: "status" not in template map
361 abort: "status" not in template map
362 [255]
362 [255]
363
363
364 hg status ignoreddir/file:
364 hg status ignoreddir/file:
365
365
366 $ hg status ignoreddir/file
366 $ hg status ignoreddir/file
367
367
368 hg status -i ignoreddir/file:
368 hg status -i ignoreddir/file:
369
369
370 $ hg status -i ignoreddir/file
370 $ hg status -i ignoreddir/file
371 I ignoreddir/file
371 I ignoreddir/file
372 $ cd ..
372 $ cd ..
373
373
374 Check 'status -q' and some combinations
374 Check 'status -q' and some combinations
375
375
376 $ hg init repo3
376 $ hg init repo3
377 $ cd repo3
377 $ cd repo3
378 $ touch modified removed deleted ignored
378 $ touch modified removed deleted ignored
379 $ echo "^ignored$" > .hgignore
379 $ echo "^ignored$" > .hgignore
380 $ hg commit -A -m 'initial checkin'
380 $ hg commit -A -m 'initial checkin'
381 adding .hgignore
381 adding .hgignore
382 adding deleted
382 adding deleted
383 adding modified
383 adding modified
384 adding removed
384 adding removed
385 $ touch added unknown ignored
385 $ touch added unknown ignored
386 $ hg add added
386 $ hg add added
387 $ echo "test" >> modified
387 $ echo "test" >> modified
388 $ hg remove removed
388 $ hg remove removed
389 $ rm deleted
389 $ rm deleted
390 $ hg copy modified copied
390 $ hg copy modified copied
391
391
392 Specify working directory revision explicitly, that should be the same as
392 Specify working directory revision explicitly, that should be the same as
393 "hg status"
393 "hg status"
394
394
395 $ hg status --change "wdir()"
395 $ hg status --change "wdir()"
396 M modified
396 M modified
397 A added
397 A added
398 A copied
398 A copied
399 R removed
399 R removed
400 ! deleted
400 ! deleted
401 ? unknown
401 ? unknown
402
402
403 Run status with 2 different flags.
403 Run status with 2 different flags.
404 Check if result is the same or different.
404 Check if result is the same or different.
405 If result is not as expected, raise error
405 If result is not as expected, raise error
406
406
407 $ assert() {
407 $ assert() {
408 > hg status $1 > ../a
408 > hg status $1 > ../a
409 > hg status $2 > ../b
409 > hg status $2 > ../b
410 > if diff ../a ../b > /dev/null; then
410 > if diff ../a ../b > /dev/null; then
411 > out=0
411 > out=0
412 > else
412 > else
413 > out=1
413 > out=1
414 > fi
414 > fi
415 > if [ $3 -eq 0 ]; then
415 > if [ $3 -eq 0 ]; then
416 > df="same"
416 > df="same"
417 > else
417 > else
418 > df="different"
418 > df="different"
419 > fi
419 > fi
420 > if [ $out -ne $3 ]; then
420 > if [ $out -ne $3 ]; then
421 > echo "Error on $1 and $2, should be $df."
421 > echo "Error on $1 and $2, should be $df."
422 > fi
422 > fi
423 > }
423 > }
424
424
425 Assert flag1 flag2 [0-same | 1-different]
425 Assert flag1 flag2 [0-same | 1-different]
426
426
427 $ assert "-q" "-mard" 0
427 $ assert "-q" "-mard" 0
428 $ assert "-A" "-marduicC" 0
428 $ assert "-A" "-marduicC" 0
429 $ assert "-qA" "-mardcC" 0
429 $ assert "-qA" "-mardcC" 0
430 $ assert "-qAui" "-A" 0
430 $ assert "-qAui" "-A" 0
431 $ assert "-qAu" "-marducC" 0
431 $ assert "-qAu" "-marducC" 0
432 $ assert "-qAi" "-mardicC" 0
432 $ assert "-qAi" "-mardicC" 0
433 $ assert "-qu" "-u" 0
433 $ assert "-qu" "-u" 0
434 $ assert "-q" "-u" 1
434 $ assert "-q" "-u" 1
435 $ assert "-m" "-a" 1
435 $ assert "-m" "-a" 1
436 $ assert "-r" "-d" 1
436 $ assert "-r" "-d" 1
437 $ cd ..
437 $ cd ..
438
438
439 $ hg init repo4
439 $ hg init repo4
440 $ cd repo4
440 $ cd repo4
441 $ touch modified removed deleted
441 $ touch modified removed deleted
442 $ hg ci -q -A -m 'initial checkin'
442 $ hg ci -q -A -m 'initial checkin'
443 $ touch added unknown
443 $ touch added unknown
444 $ hg add added
444 $ hg add added
445 $ hg remove removed
445 $ hg remove removed
446 $ rm deleted
446 $ rm deleted
447 $ echo x > modified
447 $ echo x > modified
448 $ hg copy modified copied
448 $ hg copy modified copied
449 $ hg ci -m 'test checkin' -d "1000001 0"
449 $ hg ci -m 'test checkin' -d "1000001 0"
450 $ rm *
450 $ rm *
451 $ touch unrelated
451 $ touch unrelated
452 $ hg ci -q -A -m 'unrelated checkin' -d "1000002 0"
452 $ hg ci -q -A -m 'unrelated checkin' -d "1000002 0"
453
453
454 hg status --change 1:
454 hg status --change 1:
455
455
456 $ hg status --change 1
456 $ hg status --change 1
457 M modified
457 M modified
458 A added
458 A added
459 A copied
459 A copied
460 R removed
460 R removed
461
461
462 hg status --change 1 unrelated:
462 hg status --change 1 unrelated:
463
463
464 $ hg status --change 1 unrelated
464 $ hg status --change 1 unrelated
465
465
466 hg status -C --change 1 added modified copied removed deleted:
466 hg status -C --change 1 added modified copied removed deleted:
467
467
468 $ hg status -C --change 1 added modified copied removed deleted
468 $ hg status -C --change 1 added modified copied removed deleted
469 M modified
469 M modified
470 A added
470 A added
471 A copied
471 A copied
472 modified
472 modified
473 R removed
473 R removed
474
474
475 hg status -A --change 1 and revset:
475 hg status -A --change 1 and revset:
476
476
477 $ hg status -A --change '1|1'
477 $ hg status -A --change '1|1'
478 M modified
478 M modified
479 A added
479 A added
480 A copied
480 A copied
481 modified
481 modified
482 R removed
482 R removed
483 C deleted
483 C deleted
484
484
485 $ cd ..
485 $ cd ..
486
486
487 hg status with --rev and reverted changes:
487 hg status with --rev and reverted changes:
488
488
489 $ hg init reverted-changes-repo
489 $ hg init reverted-changes-repo
490 $ cd reverted-changes-repo
490 $ cd reverted-changes-repo
491 $ echo a > file
491 $ echo a > file
492 $ hg add file
492 $ hg add file
493 $ hg ci -m a
493 $ hg ci -m a
494 $ echo b > file
494 $ echo b > file
495 $ hg ci -m b
495 $ hg ci -m b
496
496
497 reverted file should appear clean
497 reverted file should appear clean
498
498
499 $ hg revert -r 0 .
499 $ hg revert -r 0 .
500 reverting file
500 reverting file
501 $ hg status -A --rev 0
501 $ hg status -A --rev 0
502 C file
502 C file
503
503
504 #if execbit
504 #if execbit
505 reverted file with changed flag should appear modified
505 reverted file with changed flag should appear modified
506
506
507 $ chmod +x file
507 $ chmod +x file
508 $ hg status -A --rev 0
508 $ hg status -A --rev 0
509 M file
509 M file
510
510
511 $ hg revert -r 0 .
511 $ hg revert -r 0 .
512 reverting file
512 reverting file
513
513
514 reverted and committed file with changed flag should appear modified
514 reverted and committed file with changed flag should appear modified
515
515
516 $ hg co -C .
516 $ hg co -C .
517 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
517 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
518 $ chmod +x file
518 $ chmod +x file
519 $ hg ci -m 'change flag'
519 $ hg ci -m 'change flag'
520 $ hg status -A --rev 1 --rev 2
520 $ hg status -A --rev 1 --rev 2
521 M file
521 M file
522 $ hg diff -r 1 -r 2
522 $ hg diff -r 1 -r 2
523
523
524 #endif
524 #endif
525
525
526 $ cd ..
526 $ cd ..
527
527
528 hg status of binary file starting with '\1\n', a separator for metadata:
528 hg status of binary file starting with '\1\n', a separator for metadata:
529
529
530 $ hg init repo5
530 $ hg init repo5
531 $ cd repo5
531 $ cd repo5
532 >>> open("010a", r"wb").write(b"\1\nfoo") and None
532 >>> open("010a", r"wb").write(b"\1\nfoo") and None
533 $ hg ci -q -A -m 'initial checkin'
533 $ hg ci -q -A -m 'initial checkin'
534 $ hg status -A
534 $ hg status -A
535 C 010a
535 C 010a
536
536
537 >>> open("010a", r"wb").write(b"\1\nbar") and None
537 >>> open("010a", r"wb").write(b"\1\nbar") and None
538 $ hg status -A
538 $ hg status -A
539 M 010a
539 M 010a
540 $ hg ci -q -m 'modify 010a'
540 $ hg ci -q -m 'modify 010a'
541 $ hg status -A --rev 0:1
541 $ hg status -A --rev 0:1
542 M 010a
542 M 010a
543
543
544 $ touch empty
544 $ touch empty
545 $ hg ci -q -A -m 'add another file'
545 $ hg ci -q -A -m 'add another file'
546 $ hg status -A --rev 1:2 010a
546 $ hg status -A --rev 1:2 010a
547 C 010a
547 C 010a
548
548
549 $ cd ..
549 $ cd ..
550
550
551 test "hg status" with "directory pattern" which matches against files
551 test "hg status" with "directory pattern" which matches against files
552 only known on target revision.
552 only known on target revision.
553
553
554 $ hg init repo6
554 $ hg init repo6
555 $ cd repo6
555 $ cd repo6
556
556
557 $ echo a > a.txt
557 $ echo a > a.txt
558 $ hg add a.txt
558 $ hg add a.txt
559 $ hg commit -m '#0'
559 $ hg commit -m '#0'
560 $ mkdir -p 1/2/3/4/5
560 $ mkdir -p 1/2/3/4/5
561 $ echo b > 1/2/3/4/5/b.txt
561 $ echo b > 1/2/3/4/5/b.txt
562 $ hg add 1/2/3/4/5/b.txt
562 $ hg add 1/2/3/4/5/b.txt
563 $ hg commit -m '#1'
563 $ hg commit -m '#1'
564
564
565 $ hg update -C 0 > /dev/null
565 $ hg update -C 0 > /dev/null
566 $ hg status -A
566 $ hg status -A
567 C a.txt
567 C a.txt
568
568
569 the directory matching against specified pattern should be removed,
569 the directory matching against specified pattern should be removed,
570 because directory existence prevents 'dirstate.walk()' from showing
570 because directory existence prevents 'dirstate.walk()' from showing
571 warning message about such pattern.
571 warning message about such pattern.
572
572
573 $ test ! -d 1
573 $ test ! -d 1
574 $ hg status -A --rev 1 1/2/3/4/5/b.txt
574 $ hg status -A --rev 1 1/2/3/4/5/b.txt
575 R 1/2/3/4/5/b.txt
575 R 1/2/3/4/5/b.txt
576 $ hg status -A --rev 1 1/2/3/4/5
576 $ hg status -A --rev 1 1/2/3/4/5
577 R 1/2/3/4/5/b.txt
577 R 1/2/3/4/5/b.txt
578 $ hg status -A --rev 1 1/2/3
578 $ hg status -A --rev 1 1/2/3
579 R 1/2/3/4/5/b.txt
579 R 1/2/3/4/5/b.txt
580 $ hg status -A --rev 1 1
580 $ hg status -A --rev 1 1
581 R 1/2/3/4/5/b.txt
581 R 1/2/3/4/5/b.txt
582
582
583 $ hg status --config ui.formatdebug=True --rev 1 1
583 $ hg status --config ui.formatdebug=True --rev 1 1
584 status = [
584 status = [
585 {
585 {
586 'itemtype': 'file',
586 'itemtype': 'file',
587 'path': '1/2/3/4/5/b.txt',
587 'path': '1/2/3/4/5/b.txt',
588 'status': 'R'
588 'status': 'R'
589 },
589 },
590 ]
590 ]
591
591
592 #if windows
592 #if windows
593 $ hg --config ui.slash=false status -A --rev 1 1
593 $ hg --config ui.slash=false status -A --rev 1 1
594 R 1\2\3\4\5\b.txt
594 R 1\2\3\4\5\b.txt
595 #endif
595 #endif
596
596
597 $ cd ..
597 $ cd ..
598
598
599 Status after move overwriting a file (issue4458)
599 Status after move overwriting a file (issue4458)
600 =================================================
600 =================================================
601
601
602
602
603 $ hg init issue4458
603 $ hg init issue4458
604 $ cd issue4458
604 $ cd issue4458
605 $ echo a > a
605 $ echo a > a
606 $ echo b > b
606 $ echo b > b
607 $ hg commit -Am base
607 $ hg commit -Am base
608 adding a
608 adding a
609 adding b
609 adding b
610
610
611
611
612 with --force
612 with --force
613
613
614 $ hg mv b --force a
614 $ hg mv b --force a
615 $ hg st --copies
615 $ hg st --copies
616 M a
616 M a
617 b
617 b
618 R b
618 R b
619 $ hg revert --all
619 $ hg revert --all
620 reverting a
620 reverting a
621 undeleting b
621 undeleting b
622 $ rm *.orig
622 $ rm *.orig
623
623
624 without force
624 without force
625
625
626 $ hg rm a
626 $ hg rm a
627 $ hg st --copies
627 $ hg st --copies
628 R a
628 R a
629 $ hg mv b a
629 $ hg mv b a
630 $ hg st --copies
630 $ hg st --copies
631 M a
631 M a
632 b
632 b
633 R b
633 R b
634
634
635 using ui.statuscopies setting
635 using ui.statuscopies setting
636 $ hg st --config ui.statuscopies=true
636 $ hg st --config ui.statuscopies=true
637 M a
637 M a
638 b
638 b
639 R b
639 R b
640 $ hg st --config ui.statuscopies=true --no-copies
640 $ hg st --config ui.statuscopies=true --no-copies
641 M a
641 M a
642 R b
642 R b
643 $ hg st --config ui.statuscopies=false
643 $ hg st --config ui.statuscopies=false
644 M a
644 M a
645 R b
645 R b
646 $ hg st --config ui.statuscopies=false --copies
646 $ hg st --config ui.statuscopies=false --copies
647 M a
647 M a
648 b
648 b
649 R b
649 R b
650 $ hg st --config ui.tweakdefaults=yes
650 $ hg st --config ui.tweakdefaults=yes
651 M a
651 M a
652 b
652 b
653 R b
653 R b
654
654
655 using log status template (issue5155)
655 using log status template (issue5155)
656 $ hg log -Tstatus -r 'wdir()' -C
656 $ hg log -Tstatus -r 'wdir()' -C
657 changeset: 2147483647:ffffffffffff
657 changeset: 2147483647:ffffffffffff
658 parent: 0:8c55c58b4c0e
658 parent: 0:8c55c58b4c0e
659 user: test
659 user: test
660 date: * (glob)
660 date: * (glob)
661 files:
661 files:
662 M a
662 M a
663 b
663 b
664 R b
664 R b
665
665
666 $ hg log -GTstatus -r 'wdir()' -C
666 $ hg log -GTstatus -r 'wdir()' -C
667 o changeset: 2147483647:ffffffffffff
667 o changeset: 2147483647:ffffffffffff
668 | parent: 0:8c55c58b4c0e
668 | parent: 0:8c55c58b4c0e
669 ~ user: test
669 ~ user: test
670 date: * (glob)
670 date: * (glob)
671 files:
671 files:
672 M a
672 M a
673 b
673 b
674 R b
674 R b
675
675
676
676
677 Other "bug" highlight, the revision status does not report the copy information.
677 Other "bug" highlight, the revision status does not report the copy information.
678 This is buggy behavior.
678 This is buggy behavior.
679
679
680 $ hg commit -m 'blah'
680 $ hg commit -m 'blah'
681 $ hg st --copies --change .
681 $ hg st --copies --change .
682 M a
682 M a
683 R b
683 R b
684
684
685 using log status template, the copy information is displayed correctly.
685 using log status template, the copy information is displayed correctly.
686 $ hg log -Tstatus -r. -C
686 $ hg log -Tstatus -r. -C
687 changeset: 1:6685fde43d21
687 changeset: 1:6685fde43d21
688 tag: tip
688 tag: tip
689 user: test
689 user: test
690 date: * (glob)
690 date: * (glob)
691 summary: blah
691 summary: blah
692 files:
692 files:
693 M a
693 M a
694 b
694 b
695 R b
695 R b
696
696
697
697
698 $ cd ..
698 $ cd ..
699
699
700 Make sure .hg doesn't show up even as a symlink
700 Make sure .hg doesn't show up even as a symlink
701
701
702 $ hg init repo0
702 $ hg init repo0
703 $ mkdir symlink-repo0
703 $ mkdir symlink-repo0
704 $ cd symlink-repo0
704 $ cd symlink-repo0
705 $ ln -s ../repo0/.hg
705 $ ln -s ../repo0/.hg
706 $ hg status
706 $ hg status
707
707
708 If the size hasnt changed but mtime has, status needs to read the contents
708 If the size hasnt changed but mtime has, status needs to read the contents
709 of the file to check whether it has changed
709 of the file to check whether it has changed
710
710
711 $ echo 1 > a
711 $ echo 1 > a
712 $ echo 1 > b
712 $ echo 1 > b
713 $ touch -t 200102030000 a b
713 $ touch -t 200102030000 a b
714 $ hg commit -Aqm '#0'
714 $ hg commit -Aqm '#0'
715 $ echo 2 > a
715 $ echo 2 > a
716 $ touch -t 200102040000 a b
716 $ touch -t 200102040000 a b
717 $ hg status
717 $ hg status
718 M a
718 M a
719
719
720 Asking specifically for the status of a deleted/removed file
720 Asking specifically for the status of a deleted/removed file
721
721
722 $ rm a
722 $ rm a
723 $ rm b
723 $ rm b
724 $ hg status a
724 $ hg status a
725 ! a
725 ! a
726 $ hg rm a
726 $ hg rm a
727 $ hg rm b
727 $ hg rm b
728 $ hg status a
728 $ hg status a
729 R a
729 R a
730 $ hg commit -qm '#1'
730 $ hg commit -qm '#1'
731 $ hg status a
731 $ hg status a
732 a: $ENOENT$
732 a: $ENOENT$
733
733
734 Check using include flag with pattern when status does not need to traverse
734 Check using include flag with pattern when status does not need to traverse
735 the working directory (issue6483)
735 the working directory (issue6483)
736
736
737 $ cd ..
737 $ cd ..
738 $ hg init issue6483
738 $ hg init issue6483
739 $ cd issue6483
739 $ cd issue6483
740 $ touch a.py b.rs
740 $ touch a.py b.rs
741 $ hg add a.py b.rs
741 $ hg add a.py b.rs
742 $ hg st -aI "*.py"
742 $ hg st -aI "*.py"
743 A a.py
743 A a.py
744
744
745 Also check exclude pattern
745 Also check exclude pattern
746
746
747 $ hg st -aX "*.rs"
747 $ hg st -aX "*.rs"
748 A a.py
748 A a.py
749
749
750 issue6335
750 issue6335
751 When a directory containing a tracked file gets symlinked, as of 5.8
751 When a directory containing a tracked file gets symlinked, as of 5.8
752 `hg st` only gives the correct answer about clean (or deleted) files
752 `hg st` only gives the correct answer about clean (or deleted) files
753 if also listing unknowns.
753 if also listing unknowns.
754 The tree-based dirstate and status algorithm fix this:
754 The tree-based dirstate and status algorithm fix this:
755
755
756 #if symlink no-dirstate-v1 rust
756 #if symlink no-dirstate-v1 rust
757
757
758 $ cd ..
758 $ cd ..
759 $ hg init issue6335
759 $ hg init issue6335
760 $ cd issue6335
760 $ cd issue6335
761 $ mkdir foo
761 $ mkdir foo
762 $ touch foo/a
762 $ touch foo/a
763 $ hg ci -Ama
763 $ hg ci -Ama
764 adding foo/a
764 adding foo/a
765 $ mv foo bar
765 $ mv foo bar
766 $ ln -s bar foo
766 $ ln -s bar foo
767 $ hg status
767 $ hg status
768 ! foo/a
768 ! foo/a
769 ? bar/a
769 ? bar/a
770 ? foo
770 ? foo
771
771
772 $ hg status -c # incorrect output without the Rust implementation
772 $ hg status -c # incorrect output without the Rust implementation
773 $ hg status -cu
773 $ hg status -cu
774 ? bar/a
774 ? bar/a
775 ? foo
775 ? foo
776 $ hg status -d # incorrect output without the Rust implementation
776 $ hg status -d # incorrect output without the Rust implementation
777 ! foo/a
777 ! foo/a
778 $ hg status -du
778 $ hg status -du
779 ! foo/a
779 ! foo/a
780 ? bar/a
780 ? bar/a
781 ? foo
781 ? foo
782
782
783 #endif
783 #endif
784
784
785
785
786 Create a repo with files in each possible status
786 Create a repo with files in each possible status
787
787
788 $ cd ..
788 $ cd ..
789 $ hg init repo7
789 $ hg init repo7
790 $ cd repo7
790 $ cd repo7
791 $ mkdir subdir
791 $ mkdir subdir
792 $ touch clean modified deleted removed
792 $ touch clean modified deleted removed
793 $ touch subdir/clean subdir/modified subdir/deleted subdir/removed
793 $ touch subdir/clean subdir/modified subdir/deleted subdir/removed
794 $ echo ignored > .hgignore
794 $ echo ignored > .hgignore
795 $ hg ci -Aqm '#0'
795 $ hg ci -Aqm '#0'
796 $ echo 1 > modified
796 $ echo 1 > modified
797 $ echo 1 > subdir/modified
797 $ echo 1 > subdir/modified
798 $ rm deleted
798 $ rm deleted
799 $ rm subdir/deleted
799 $ rm subdir/deleted
800 $ hg rm removed
800 $ hg rm removed
801 $ hg rm subdir/removed
801 $ hg rm subdir/removed
802 $ touch unknown ignored
802 $ touch unknown ignored
803 $ touch subdir/unknown subdir/ignored
803 $ touch subdir/unknown subdir/ignored
804
804
805 Check the output
805 Check the output
806
806
807 $ hg status
807 $ hg status
808 M modified
808 M modified
809 M subdir/modified
809 M subdir/modified
810 R removed
810 R removed
811 R subdir/removed
811 R subdir/removed
812 ! deleted
812 ! deleted
813 ! subdir/deleted
813 ! subdir/deleted
814 ? subdir/unknown
814 ? subdir/unknown
815 ? unknown
815 ? unknown
816
816
817 $ hg status -mard
817 $ hg status -mard
818 M modified
818 M modified
819 M subdir/modified
819 M subdir/modified
820 R removed
820 R removed
821 R subdir/removed
821 R subdir/removed
822 ! deleted
822 ! deleted
823 ! subdir/deleted
823 ! subdir/deleted
824
824
825 $ hg status -A
825 $ hg status -A
826 M modified
826 M modified
827 M subdir/modified
827 M subdir/modified
828 R removed
828 R removed
829 R subdir/removed
829 R subdir/removed
830 ! deleted
830 ! deleted
831 ! subdir/deleted
831 ! subdir/deleted
832 ? subdir/unknown
832 ? subdir/unknown
833 ? unknown
833 ? unknown
834 I ignored
834 I ignored
835 I subdir/ignored
835 I subdir/ignored
836 C .hgignore
836 C .hgignore
837 C clean
837 C clean
838 C subdir/clean
838 C subdir/clean
839
839
840 Note: `hg status some-name` creates a patternmatcher which is not supported
840 Note: `hg status some-name` creates a patternmatcher which is not supported
841 yet by the Rust implementation of status, but includematcher is supported.
841 yet by the Rust implementation of status, but includematcher is supported.
842 --include is used below for that reason
842 --include is used below for that reason
843
843
844 #if unix-permissions
844 #if unix-permissions
845
845
846 Not having permission to read a directory that contains tracked files makes
846 Not having permission to read a directory that contains tracked files makes
847 status emit a warning then behave as if the directory was empty or removed
847 status emit a warning then behave as if the directory was empty or removed
848 entirely:
848 entirely:
849
849
850 $ chmod 0 subdir
850 $ chmod 0 subdir
851 $ hg status --include subdir
851 $ hg status --include subdir
852 subdir: Permission denied
852 subdir: Permission denied
853 R subdir/removed
853 R subdir/removed
854 ! subdir/clean
854 ! subdir/clean
855 ! subdir/deleted
855 ! subdir/deleted
856 ! subdir/modified
856 ! subdir/modified
857 $ chmod 755 subdir
857 $ chmod 755 subdir
858
858
859 #endif
859 #endif
860
860
861 Remove a directory that contains tracked files
861 Remove a directory that contains tracked files
862
862
863 $ rm -r subdir
863 $ rm -r subdir
864 $ hg status --include subdir
864 $ hg status --include subdir
865 R subdir/removed
865 R subdir/removed
866 ! subdir/clean
866 ! subdir/clean
867 ! subdir/deleted
867 ! subdir/deleted
868 ! subdir/modified
868 ! subdir/modified
869
869
870 and replace it by a file
870 and replace it by a file
871
871
872 $ touch subdir
872 $ touch subdir
873 $ hg status --include subdir
873 $ hg status --include subdir
874 R subdir/removed
874 R subdir/removed
875 ! subdir/clean
875 ! subdir/clean
876 ! subdir/deleted
876 ! subdir/deleted
877 ! subdir/modified
877 ! subdir/modified
878 ? subdir
878 ? subdir
879
879
880 Replaced a deleted or removed file with a directory
880 Replaced a deleted or removed file with a directory
881
881
882 $ mkdir deleted removed
882 $ mkdir deleted removed
883 $ touch deleted/1 removed/1
883 $ touch deleted/1 removed/1
884 $ hg status --include deleted --include removed
884 $ hg status --include deleted --include removed
885 R removed
885 R removed
886 ! deleted
886 ! deleted
887 ? deleted/1
887 ? deleted/1
888 ? removed/1
888 ? removed/1
889 $ hg add removed/1
889 $ hg add removed/1
890 $ hg status --include deleted --include removed
890 $ hg status --include deleted --include removed
891 A removed/1
891 A removed/1
892 R removed
892 R removed
893 ! deleted
893 ! deleted
894 ? deleted/1
894 ? deleted/1
895
895
896 Deeply nested files in an ignored directory are still listed on request
896 Deeply nested files in an ignored directory are still listed on request
897
897
898 $ echo ignored-dir >> .hgignore
898 $ echo ignored-dir >> .hgignore
899 $ mkdir ignored-dir
899 $ mkdir ignored-dir
900 $ mkdir ignored-dir/subdir
900 $ mkdir ignored-dir/subdir
901 $ touch ignored-dir/subdir/1
901 $ touch ignored-dir/subdir/1
902 $ hg status --ignored
902 $ hg status --ignored
903 I ignored
903 I ignored
904 I ignored-dir/subdir/1
904 I ignored-dir/subdir/1
905
905
906 Check using include flag while listing ignored composes correctly (issue6514)
906 Check using include flag while listing ignored composes correctly (issue6514)
907
907
908 $ cd ..
908 $ cd ..
909 $ hg init issue6514
909 $ hg init issue6514
910 $ cd issue6514
910 $ cd issue6514
911 $ mkdir ignored-folder
911 $ mkdir ignored-folder
912 $ touch A.hs B.hs C.hs ignored-folder/other.txt ignored-folder/ctest.hs
912 $ touch A.hs B.hs C.hs ignored-folder/other.txt ignored-folder/ctest.hs
913 $ cat >.hgignore <<EOF
913 $ cat >.hgignore <<EOF
914 > A.hs
914 > A.hs
915 > B.hs
915 > B.hs
916 > ignored-folder/
916 > ignored-folder/
917 > EOF
917 > EOF
918 $ hg st -i -I 're:.*\.hs$'
918 $ hg st -i -I 're:.*\.hs$'
919 I A.hs
919 I A.hs
920 I B.hs
920 I B.hs
921 I ignored-folder/ctest.hs
921 I ignored-folder/ctest.hs
922
922
923 #if rust dirstate-v2
923 #if rust dirstate-v2
924
924
925 Check read_dir caching
925 Check read_dir caching
926
926
927 $ cd ..
927 $ cd ..
928 $ hg init repo8
928 $ hg init repo8
929 $ cd repo8
929 $ cd repo8
930 $ mkdir subdir
930 $ mkdir subdir
931 $ touch subdir/a subdir/b
931 $ touch subdir/a subdir/b
932 $ hg ci -Aqm '#0'
932 $ hg ci -Aqm '#0'
933
933
934 The cached mtime is initially unset
934 The cached mtime is initially unset
935
935
936 $ hg debugdirstate --all --no-dates | grep '^ '
936 $ hg debugdirstate --all --no-dates | grep '^ '
937 0 -1 unset subdir
937 0 -1 unset subdir
938
938
939 It is still not set when there are unknown files
939 It is still not set when there are unknown files
940
940
941 $ touch subdir/unknown
941 $ touch subdir/unknown
942 $ hg status
942 $ hg status
943 ? subdir/unknown
943 ? subdir/unknown
944 $ hg debugdirstate --all --no-dates | grep '^ '
944 $ hg debugdirstate --all --no-dates | grep '^ '
945 0 -1 unset subdir
945 0 -1 unset subdir
946
946
947 Now the directory is eligible for caching, so its mtime is saved in the dirstate
947 Now the directory is eligible for caching, so its mtime is saved in the dirstate
948
948
949 $ rm subdir/unknown
949 $ rm subdir/unknown
950 $ sleep 0.1 # ensure the kernels internal clock for mtimes has ticked
950 $ sleep 0.1 # ensure the kernels internal clock for mtimes has ticked
951 $ hg status
951 $ hg status
952 $ hg debugdirstate --all --no-dates | grep '^ '
952 $ hg debugdirstate --all --no-dates | grep '^ '
953 0 -1 set subdir
953 0 -1 set subdir
954
954
955 This time the command should be ever so slightly faster since it does not need `read_dir("subdir")`
955 This time the command should be ever so slightly faster since it does not need `read_dir("subdir")`
956
956
957 $ hg status
957 $ hg status
958
958
959 Creating a new file changes the directorys mtime, invalidating the cache
959 Creating a new file changes the directorys mtime, invalidating the cache
960
960
961 $ touch subdir/unknown
961 $ touch subdir/unknown
962 $ hg status
962 $ hg status
963 ? subdir/unknown
963 ? subdir/unknown
964
964
965 $ rm subdir/unknown
965 $ rm subdir/unknown
966 $ hg status
966 $ hg status
967
967
968 Removing a node from the dirstate resets the cache for its parent directory
968 Removing a node from the dirstate resets the cache for its parent directory
969
969
970 $ hg forget subdir/a
970 $ hg forget subdir/a
971 $ hg debugdirstate --all --no-dates | grep '^ '
971 $ hg debugdirstate --all --no-dates | grep '^ '
972 0 -1 set subdir
972 0 -1 set subdir
973 $ hg ci -qm '#1'
973 $ hg ci -qm '#1'
974 $ hg debugdirstate --all --no-dates | grep '^ '
974 $ hg debugdirstate --all --no-dates | grep '^ '
975 0 -1 unset subdir
975 0 -1 unset subdir
976 $ hg status
976 $ hg status
977 ? subdir/a
977 ? subdir/a
978
978
979 Changing the hgignore rules makes us recompute the status (and rewrite the dirstate).
979 Changing the hgignore rules makes us recompute the status (and rewrite the dirstate).
980
980
981 $ rm subdir/a
981 $ rm subdir/a
982 $ mkdir another-subdir
982 $ mkdir another-subdir
983 $ touch another-subdir/something-else
983 $ touch another-subdir/something-else
984
984
985 $ cat > "$TESTDIR"/extra-hgignore <<EOF
985 $ cat > "$TESTDIR"/extra-hgignore <<EOF
986 > something-else
986 > something-else
987 > EOF
987 > EOF
988
988
989 $ hg status --config ui.ignore.global="$TESTDIR"/extra-hgignore
989 $ hg status --config ui.ignore.global="$TESTDIR"/extra-hgignore
990 $ hg debugdirstate --all --no-dates | grep '^ '
990 $ hg debugdirstate --all --no-dates | grep '^ '
991 0 -1 set subdir
991 0 -1 set subdir
992
992
993 $ hg status
993 $ hg status
994 ? another-subdir/something-else
994 ? another-subdir/something-else
995
995
996 $ hg debugdirstate --all --no-dates | grep '^ '
996 One invocation of status is enough to populate the cache even if it's invalidated
997 0 -1 unset subdir (known-bad-output !)
997 in the same run.
998
999 For some reason the first [status] is not enough to save the updated
1000 directory mtime into the cache. The second invocation does it.
1001 The first call only clears the directory cache by marking the directories
1002 as "outdated", which seems like a bug.
1003
1004 $ hg status
1005 ? another-subdir/something-else
1006
998
1007 $ hg debugdirstate --all --no-dates | grep '^ '
999 $ hg debugdirstate --all --no-dates | grep '^ '
1008 0 -1 set subdir
1000 0 -1 set subdir
1009
1001
1010 #endif
1002 #endif
General Comments 0
You need to be logged in to leave comments. Login now