##// END OF EJS Templates
dirstate-tree: Fix status algorithm with unreadable directory...
Simon Sapin -
r48135:5e12b6bf default
parent child Browse files
Show More
@@ -1,466 +1,469 b''
1 1 use crate::dirstate::status::IgnoreFnType;
2 2 use crate::dirstate_tree::dirstate_map::ChildNodesRef;
3 3 use crate::dirstate_tree::dirstate_map::DirstateMap;
4 4 use crate::dirstate_tree::dirstate_map::NodeRef;
5 5 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
6 6 use crate::matchers::get_ignore_function;
7 7 use crate::matchers::Matcher;
8 8 use crate::utils::files::get_bytes_from_os_string;
9 9 use crate::utils::files::get_path_from_bytes;
10 10 use crate::utils::hg_path::HgPath;
11 11 use crate::BadMatch;
12 12 use crate::DirstateStatus;
13 13 use crate::EntryState;
14 14 use crate::HgPathBuf;
15 15 use crate::PatternFileWarning;
16 16 use crate::StatusError;
17 17 use crate::StatusOptions;
18 18 use micro_timer::timed;
19 19 use rayon::prelude::*;
20 20 use std::borrow::Cow;
21 21 use std::io;
22 22 use std::path::Path;
23 23 use std::path::PathBuf;
24 24 use std::sync::Mutex;
25 25
26 26 /// Returns the status of the working directory compared to its parent
27 27 /// changeset.
28 28 ///
29 29 /// This algorithm is based on traversing the filesystem tree (`fs` in function
30 30 /// and variable names) and dirstate tree at the same time. The core of this
31 31 /// traversal is the recursive `traverse_fs_directory_and_dirstate` function
32 32 /// and its use of `itertools::merge_join_by`. When reaching a path that only
33 33 /// exists in one of the two trees, depending on information requested by
34 34 /// `options` we may need to traverse the remaining subtree.
35 35 #[timed]
36 36 pub fn status<'tree, 'on_disk: 'tree>(
37 37 dmap: &'tree mut DirstateMap<'on_disk>,
38 38 matcher: &(dyn Matcher + Sync),
39 39 root_dir: PathBuf,
40 40 ignore_files: Vec<PathBuf>,
41 41 options: StatusOptions,
42 42 ) -> Result<(DirstateStatus<'tree>, Vec<PatternFileWarning>), StatusError> {
43 43 let (ignore_fn, warnings): (IgnoreFnType, _) =
44 44 if options.list_ignored || options.list_unknown {
45 45 get_ignore_function(ignore_files, &root_dir)?
46 46 } else {
47 47 (Box::new(|&_| true), vec![])
48 48 };
49 49
50 50 let common = StatusCommon {
51 51 dmap,
52 52 options,
53 53 matcher,
54 54 ignore_fn,
55 55 outcome: Mutex::new(DirstateStatus::default()),
56 56 };
57 57 let is_at_repo_root = true;
58 58 let hg_path = HgPath::new("");
59 59 let has_ignored_ancestor = false;
60 60 common.traverse_fs_directory_and_dirstate(
61 61 has_ignored_ancestor,
62 62 dmap.root.as_ref(),
63 63 hg_path,
64 64 &root_dir,
65 65 is_at_repo_root,
66 66 )?;
67 67 Ok((common.outcome.into_inner().unwrap(), warnings))
68 68 }
69 69
70 70 /// Bag of random things needed by various parts of the algorithm. Reduces the
71 71 /// number of parameters passed to functions.
72 72 struct StatusCommon<'tree, 'a, 'on_disk: 'tree> {
73 73 dmap: &'tree DirstateMap<'on_disk>,
74 74 options: StatusOptions,
75 75 matcher: &'a (dyn Matcher + Sync),
76 76 ignore_fn: IgnoreFnType<'a>,
77 77 outcome: Mutex<DirstateStatus<'tree>>,
78 78 }
79 79
80 80 impl<'tree, 'a> StatusCommon<'tree, 'a, '_> {
81 81 fn read_dir(
82 82 &self,
83 83 hg_path: &HgPath,
84 84 fs_path: &Path,
85 85 is_at_repo_root: bool,
86 86 ) -> Result<Vec<DirEntry>, ()> {
87 87 DirEntry::read_dir(fs_path, is_at_repo_root)
88 88 .map_err(|error| self.io_error(error, hg_path))
89 89 }
90 90
91 91 fn io_error(&self, error: std::io::Error, hg_path: &HgPath) {
92 92 let errno = error.raw_os_error().expect("expected real OS error");
93 93 self.outcome
94 94 .lock()
95 95 .unwrap()
96 96 .bad
97 97 .push((hg_path.to_owned().into(), BadMatch::OsError(errno)))
98 98 }
99 99
100 100 fn traverse_fs_directory_and_dirstate(
101 101 &self,
102 102 has_ignored_ancestor: bool,
103 103 dirstate_nodes: ChildNodesRef<'tree, '_>,
104 104 directory_hg_path: &'tree HgPath,
105 105 directory_fs_path: &Path,
106 106 is_at_repo_root: bool,
107 107 ) -> Result<(), DirstateV2ParseError> {
108 108 if !self.options.list_unknown && !self.options.list_ignored {
109 109 // We only care about files in the dirstate, so we can skip listing
110 110 // filesystem directories entirely.
111 111 return dirstate_nodes
112 112 .par_iter()
113 113 .map(|dirstate_node| {
114 114 let fs_path = directory_fs_path.join(get_path_from_bytes(
115 115 dirstate_node.base_name(self.dmap.on_disk)?.as_bytes(),
116 116 ));
117 117 match std::fs::symlink_metadata(&fs_path) {
118 118 Ok(fs_metadata) => self.traverse_fs_and_dirstate(
119 119 &fs_path,
120 120 &fs_metadata,
121 121 dirstate_node,
122 122 has_ignored_ancestor,
123 123 ),
124 124 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
125 125 self.traverse_dirstate_only(dirstate_node)
126 126 }
127 127 Err(error) => {
128 128 let hg_path =
129 129 dirstate_node.full_path(self.dmap.on_disk)?;
130 130 Ok(self.io_error(error, hg_path))
131 131 }
132 132 }
133 133 })
134 134 .collect();
135 135 }
136 136
137 137 let mut fs_entries = if let Ok(entries) = self.read_dir(
138 138 directory_hg_path,
139 139 directory_fs_path,
140 140 is_at_repo_root,
141 141 ) {
142 142 entries
143 143 } else {
144 return Ok(());
144 // Treat an unreadable directory (typically because of insufficient
145 // permissions) like an empty directory. `self.read_dir` has
146 // already called `self.io_error` so a warning will be emitted.
147 Vec::new()
145 148 };
146 149
147 150 // `merge_join_by` requires both its input iterators to be sorted:
148 151
149 152 let dirstate_nodes = dirstate_nodes.sorted();
150 153 // `sort_unstable_by_key` doesn’t allow keys borrowing from the value:
151 154 // https://github.com/rust-lang/rust/issues/34162
152 155 fs_entries.sort_unstable_by(|e1, e2| e1.base_name.cmp(&e2.base_name));
153 156
154 157 // Propagate here any error that would happen inside the comparison
155 158 // callback below
156 159 for dirstate_node in &dirstate_nodes {
157 160 dirstate_node.base_name(self.dmap.on_disk)?;
158 161 }
159 162 itertools::merge_join_by(
160 163 dirstate_nodes,
161 164 &fs_entries,
162 165 |dirstate_node, fs_entry| {
163 166 // This `unwrap` never panics because we already propagated
164 167 // those errors above
165 168 dirstate_node
166 169 .base_name(self.dmap.on_disk)
167 170 .unwrap()
168 171 .cmp(&fs_entry.base_name)
169 172 },
170 173 )
171 174 .par_bridge()
172 175 .map(|pair| {
173 176 use itertools::EitherOrBoth::*;
174 177 match pair {
175 178 Both(dirstate_node, fs_entry) => self
176 179 .traverse_fs_and_dirstate(
177 180 &fs_entry.full_path,
178 181 &fs_entry.metadata,
179 182 dirstate_node,
180 183 has_ignored_ancestor,
181 184 ),
182 185 Left(dirstate_node) => {
183 186 self.traverse_dirstate_only(dirstate_node)
184 187 }
185 188 Right(fs_entry) => Ok(self.traverse_fs_only(
186 189 has_ignored_ancestor,
187 190 directory_hg_path,
188 191 fs_entry,
189 192 )),
190 193 }
191 194 })
192 195 .collect()
193 196 }
194 197
195 198 fn traverse_fs_and_dirstate(
196 199 &self,
197 200 fs_path: &Path,
198 201 fs_metadata: &std::fs::Metadata,
199 202 dirstate_node: NodeRef<'tree, '_>,
200 203 has_ignored_ancestor: bool,
201 204 ) -> Result<(), DirstateV2ParseError> {
202 205 let hg_path = dirstate_node.full_path(self.dmap.on_disk)?;
203 206 let file_type = fs_metadata.file_type();
204 207 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
205 208 if !file_or_symlink {
206 209 // If we previously had a file here, it was removed (with
207 210 // `hg rm` or similar) or deleted before it could be
208 211 // replaced by a directory or something else.
209 212 self.mark_removed_or_deleted_if_file(
210 213 hg_path,
211 214 dirstate_node.state()?,
212 215 );
213 216 }
214 217 if file_type.is_dir() {
215 218 if self.options.collect_traversed_dirs {
216 219 self.outcome.lock().unwrap().traversed.push(hg_path.into())
217 220 }
218 221 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path);
219 222 let is_at_repo_root = false;
220 223 self.traverse_fs_directory_and_dirstate(
221 224 is_ignored,
222 225 dirstate_node.children(self.dmap.on_disk)?,
223 226 hg_path,
224 227 fs_path,
225 228 is_at_repo_root,
226 229 )?
227 230 } else {
228 231 if file_or_symlink && self.matcher.matches(hg_path) {
229 232 let full_path = Cow::from(hg_path);
230 233 if let Some(state) = dirstate_node.state()? {
231 234 match state {
232 235 EntryState::Added => {
233 236 self.outcome.lock().unwrap().added.push(full_path)
234 237 }
235 238 EntryState::Removed => self
236 239 .outcome
237 240 .lock()
238 241 .unwrap()
239 242 .removed
240 243 .push(full_path),
241 244 EntryState::Merged => self
242 245 .outcome
243 246 .lock()
244 247 .unwrap()
245 248 .modified
246 249 .push(full_path),
247 250 EntryState::Normal => self
248 251 .handle_normal_file(&dirstate_node, fs_metadata)?,
249 252 // This variant is not used in DirstateMap
250 253 // nodes
251 254 EntryState::Unknown => unreachable!(),
252 255 }
253 256 } else {
254 257 // `node.entry.is_none()` indicates a "directory"
255 258 // node, but the filesystem has a file
256 259 self.mark_unknown_or_ignored(
257 260 has_ignored_ancestor,
258 261 full_path,
259 262 )
260 263 }
261 264 }
262 265
263 266 for child_node in dirstate_node.children(self.dmap.on_disk)?.iter()
264 267 {
265 268 self.traverse_dirstate_only(child_node)?
266 269 }
267 270 }
268 271 Ok(())
269 272 }
270 273
271 274 /// A file with `EntryState::Normal` in the dirstate was found in the
272 275 /// filesystem
273 276 fn handle_normal_file(
274 277 &self,
275 278 dirstate_node: &NodeRef<'tree, '_>,
276 279 fs_metadata: &std::fs::Metadata,
277 280 ) -> Result<(), DirstateV2ParseError> {
278 281 // Keep the low 31 bits
279 282 fn truncate_u64(value: u64) -> i32 {
280 283 (value & 0x7FFF_FFFF) as i32
281 284 }
282 285 fn truncate_i64(value: i64) -> i32 {
283 286 (value & 0x7FFF_FFFF) as i32
284 287 }
285 288
286 289 let entry = dirstate_node
287 290 .entry()?
288 291 .expect("handle_normal_file called with entry-less node");
289 292 let full_path = Cow::from(dirstate_node.full_path(self.dmap.on_disk)?);
290 293 let mode_changed =
291 294 || self.options.check_exec && entry.mode_changed(fs_metadata);
292 295 let size_changed = entry.size != truncate_u64(fs_metadata.len());
293 296 if entry.size >= 0
294 297 && size_changed
295 298 && fs_metadata.file_type().is_symlink()
296 299 {
297 300 // issue6456: Size returned may be longer due to encryption
298 301 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
299 302 self.outcome.lock().unwrap().unsure.push(full_path)
300 303 } else if dirstate_node.has_copy_source()
301 304 || entry.is_from_other_parent()
302 305 || (entry.size >= 0 && (size_changed || mode_changed()))
303 306 {
304 307 self.outcome.lock().unwrap().modified.push(full_path)
305 308 } else {
306 309 let mtime = mtime_seconds(fs_metadata);
307 310 if truncate_i64(mtime) != entry.mtime
308 311 || mtime == self.options.last_normal_time
309 312 {
310 313 self.outcome.lock().unwrap().unsure.push(full_path)
311 314 } else if self.options.list_clean {
312 315 self.outcome.lock().unwrap().clean.push(full_path)
313 316 }
314 317 }
315 318 Ok(())
316 319 }
317 320
318 321 /// A node in the dirstate tree has no corresponding filesystem entry
319 322 fn traverse_dirstate_only(
320 323 &self,
321 324 dirstate_node: NodeRef<'tree, '_>,
322 325 ) -> Result<(), DirstateV2ParseError> {
323 326 self.mark_removed_or_deleted_if_file(
324 327 dirstate_node.full_path(self.dmap.on_disk)?,
325 328 dirstate_node.state()?,
326 329 );
327 330 dirstate_node
328 331 .children(self.dmap.on_disk)?
329 332 .par_iter()
330 333 .map(|child_node| self.traverse_dirstate_only(child_node))
331 334 .collect()
332 335 }
333 336
334 337 /// A node in the dirstate tree has no corresponding *file* on the
335 338 /// filesystem
336 339 ///
337 340 /// Does nothing on a "directory" node
338 341 fn mark_removed_or_deleted_if_file(
339 342 &self,
340 343 hg_path: &'tree HgPath,
341 344 dirstate_node_state: Option<EntryState>,
342 345 ) {
343 346 if let Some(state) = dirstate_node_state {
344 347 if self.matcher.matches(hg_path) {
345 348 if let EntryState::Removed = state {
346 349 self.outcome.lock().unwrap().removed.push(hg_path.into())
347 350 } else {
348 351 self.outcome.lock().unwrap().deleted.push(hg_path.into())
349 352 }
350 353 }
351 354 }
352 355 }
353 356
354 357 /// Something in the filesystem has no corresponding dirstate node
355 358 fn traverse_fs_only(
356 359 &self,
357 360 has_ignored_ancestor: bool,
358 361 directory_hg_path: &HgPath,
359 362 fs_entry: &DirEntry,
360 363 ) {
361 364 let hg_path = directory_hg_path.join(&fs_entry.base_name);
362 365 let file_type = fs_entry.metadata.file_type();
363 366 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
364 367 if file_type.is_dir() {
365 368 let is_ignored =
366 369 has_ignored_ancestor || (self.ignore_fn)(&hg_path);
367 370 let traverse_children = if is_ignored {
368 371 // Descendants of an ignored directory are all ignored
369 372 self.options.list_ignored
370 373 } else {
371 374 // Descendants of an unknown directory may be either unknown or
372 375 // ignored
373 376 self.options.list_unknown || self.options.list_ignored
374 377 };
375 378 if traverse_children {
376 379 let is_at_repo_root = false;
377 380 if let Ok(children_fs_entries) = self.read_dir(
378 381 &hg_path,
379 382 &fs_entry.full_path,
380 383 is_at_repo_root,
381 384 ) {
382 385 children_fs_entries.par_iter().for_each(|child_fs_entry| {
383 386 self.traverse_fs_only(
384 387 is_ignored,
385 388 &hg_path,
386 389 child_fs_entry,
387 390 )
388 391 })
389 392 }
390 393 }
391 394 if self.options.collect_traversed_dirs {
392 395 self.outcome.lock().unwrap().traversed.push(hg_path.into())
393 396 }
394 397 } else if file_or_symlink && self.matcher.matches(&hg_path) {
395 398 self.mark_unknown_or_ignored(has_ignored_ancestor, hg_path.into())
396 399 }
397 400 }
398 401
399 402 fn mark_unknown_or_ignored(
400 403 &self,
401 404 has_ignored_ancestor: bool,
402 405 hg_path: Cow<'tree, HgPath>,
403 406 ) {
404 407 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path);
405 408 if is_ignored {
406 409 if self.options.list_ignored {
407 410 self.outcome.lock().unwrap().ignored.push(hg_path)
408 411 }
409 412 } else {
410 413 if self.options.list_unknown {
411 414 self.outcome.lock().unwrap().unknown.push(hg_path)
412 415 }
413 416 }
414 417 }
415 418 }
416 419
417 420 #[cfg(unix)] // TODO
418 421 fn mtime_seconds(metadata: &std::fs::Metadata) -> i64 {
419 422 // Going through `Metadata::modified()` would be portable, but would take
420 423 // care to construct a `SystemTime` value with sub-second precision just
421 424 // for us to throw that away here.
422 425 use std::os::unix::fs::MetadataExt;
423 426 metadata.mtime()
424 427 }
425 428
426 429 struct DirEntry {
427 430 base_name: HgPathBuf,
428 431 full_path: PathBuf,
429 432 metadata: std::fs::Metadata,
430 433 }
431 434
432 435 impl DirEntry {
433 436 /// Returns **unsorted** entries in the given directory, with name and
434 437 /// metadata.
435 438 ///
436 439 /// If a `.hg` sub-directory is encountered:
437 440 ///
438 441 /// * At the repository root, ignore that sub-directory
439 442 /// * Elsewhere, we’re listing the content of a sub-repo. Return an empty
440 443 /// list instead.
441 444 fn read_dir(path: &Path, is_at_repo_root: bool) -> io::Result<Vec<Self>> {
442 445 let mut results = Vec::new();
443 446 for entry in path.read_dir()? {
444 447 let entry = entry?;
445 448 let metadata = entry.metadata()?;
446 449 let name = get_bytes_from_os_string(entry.file_name());
447 450 // FIXME don't do this when cached
448 451 if name == b".hg" {
449 452 if is_at_repo_root {
450 453 // Skip the repo’s own .hg (might be a symlink)
451 454 continue;
452 455 } else if metadata.is_dir() {
453 456 // A .hg sub-directory at another location means a subrepo,
454 457 // skip it entirely.
455 458 return Ok(Vec::new());
456 459 }
457 460 }
458 461 results.push(DirEntry {
459 462 base_name: name.into(),
460 463 full_path: entry.path(),
461 464 metadata,
462 465 })
463 466 }
464 467 Ok(results)
465 468 }
466 469 }
@@ -1,900 +1,917 b''
1 1 #testcases dirstate-v1 dirstate-v1-tree dirstate-v2
2 2
3 3 #if no-rust
4 4 $ hg init repo0 --config format.exp-dirstate-v2=1
5 5 abort: dirstate v2 format requested by config but not supported (requires Rust extensions)
6 6 [255]
7 7 #endif
8 8
9 9 #if dirstate-v1-tree
10 10 #require rust
11 11 $ echo '[experimental]' >> $HGRCPATH
12 12 $ echo 'dirstate-tree.in-memory=1' >> $HGRCPATH
13 13 #endif
14 14
15 15 #if dirstate-v2
16 16 #require rust
17 17 $ echo '[format]' >> $HGRCPATH
18 18 $ echo 'exp-dirstate-v2=1' >> $HGRCPATH
19 19 #endif
20 20
21 21 $ hg init repo1
22 22 $ cd repo1
23 23 $ mkdir a b a/1 b/1 b/2
24 24 $ touch in_root a/in_a b/in_b a/1/in_a_1 b/1/in_b_1 b/2/in_b_2
25 25
26 26 hg status in repo root:
27 27
28 28 $ hg status
29 29 ? a/1/in_a_1
30 30 ? a/in_a
31 31 ? b/1/in_b_1
32 32 ? b/2/in_b_2
33 33 ? b/in_b
34 34 ? in_root
35 35
36 36 hg status . in repo root:
37 37
38 38 $ hg status .
39 39 ? a/1/in_a_1
40 40 ? a/in_a
41 41 ? b/1/in_b_1
42 42 ? b/2/in_b_2
43 43 ? b/in_b
44 44 ? in_root
45 45
46 46 $ hg status --cwd a
47 47 ? a/1/in_a_1
48 48 ? a/in_a
49 49 ? b/1/in_b_1
50 50 ? b/2/in_b_2
51 51 ? b/in_b
52 52 ? in_root
53 53 $ hg status --cwd a .
54 54 ? 1/in_a_1
55 55 ? in_a
56 56 $ hg status --cwd a ..
57 57 ? 1/in_a_1
58 58 ? in_a
59 59 ? ../b/1/in_b_1
60 60 ? ../b/2/in_b_2
61 61 ? ../b/in_b
62 62 ? ../in_root
63 63
64 64 $ hg status --cwd b
65 65 ? a/1/in_a_1
66 66 ? a/in_a
67 67 ? b/1/in_b_1
68 68 ? b/2/in_b_2
69 69 ? b/in_b
70 70 ? in_root
71 71 $ hg status --cwd b .
72 72 ? 1/in_b_1
73 73 ? 2/in_b_2
74 74 ? in_b
75 75 $ hg status --cwd b ..
76 76 ? ../a/1/in_a_1
77 77 ? ../a/in_a
78 78 ? 1/in_b_1
79 79 ? 2/in_b_2
80 80 ? in_b
81 81 ? ../in_root
82 82
83 83 $ hg status --cwd a/1
84 84 ? a/1/in_a_1
85 85 ? a/in_a
86 86 ? b/1/in_b_1
87 87 ? b/2/in_b_2
88 88 ? b/in_b
89 89 ? in_root
90 90 $ hg status --cwd a/1 .
91 91 ? in_a_1
92 92 $ hg status --cwd a/1 ..
93 93 ? in_a_1
94 94 ? ../in_a
95 95
96 96 $ hg status --cwd b/1
97 97 ? a/1/in_a_1
98 98 ? a/in_a
99 99 ? b/1/in_b_1
100 100 ? b/2/in_b_2
101 101 ? b/in_b
102 102 ? in_root
103 103 $ hg status --cwd b/1 .
104 104 ? in_b_1
105 105 $ hg status --cwd b/1 ..
106 106 ? in_b_1
107 107 ? ../2/in_b_2
108 108 ? ../in_b
109 109
110 110 $ hg status --cwd b/2
111 111 ? a/1/in_a_1
112 112 ? a/in_a
113 113 ? b/1/in_b_1
114 114 ? b/2/in_b_2
115 115 ? b/in_b
116 116 ? in_root
117 117 $ hg status --cwd b/2 .
118 118 ? in_b_2
119 119 $ hg status --cwd b/2 ..
120 120 ? ../1/in_b_1
121 121 ? in_b_2
122 122 ? ../in_b
123 123
124 124 combining patterns with root and patterns without a root works
125 125
126 126 $ hg st a/in_a re:.*b$
127 127 ? a/in_a
128 128 ? b/in_b
129 129
130 130 tweaking defaults works
131 131 $ hg status --cwd a --config ui.tweakdefaults=yes
132 132 ? 1/in_a_1
133 133 ? in_a
134 134 ? ../b/1/in_b_1
135 135 ? ../b/2/in_b_2
136 136 ? ../b/in_b
137 137 ? ../in_root
138 138 $ HGPLAIN=1 hg status --cwd a --config ui.tweakdefaults=yes
139 139 ? a/1/in_a_1 (glob)
140 140 ? a/in_a (glob)
141 141 ? b/1/in_b_1 (glob)
142 142 ? b/2/in_b_2 (glob)
143 143 ? b/in_b (glob)
144 144 ? in_root
145 145 $ HGPLAINEXCEPT=tweakdefaults hg status --cwd a --config ui.tweakdefaults=yes
146 146 ? 1/in_a_1
147 147 ? in_a
148 148 ? ../b/1/in_b_1
149 149 ? ../b/2/in_b_2
150 150 ? ../b/in_b
151 151 ? ../in_root (glob)
152 152
153 153 relative paths can be requested
154 154
155 155 $ hg status --cwd a --config ui.relative-paths=yes
156 156 ? 1/in_a_1
157 157 ? in_a
158 158 ? ../b/1/in_b_1
159 159 ? ../b/2/in_b_2
160 160 ? ../b/in_b
161 161 ? ../in_root
162 162
163 163 $ hg status --cwd a . --config ui.relative-paths=legacy
164 164 ? 1/in_a_1
165 165 ? in_a
166 166 $ hg status --cwd a . --config ui.relative-paths=no
167 167 ? a/1/in_a_1
168 168 ? a/in_a
169 169
170 170 commands.status.relative overrides ui.relative-paths
171 171
172 172 $ cat >> $HGRCPATH <<EOF
173 173 > [ui]
174 174 > relative-paths = False
175 175 > [commands]
176 176 > status.relative = True
177 177 > EOF
178 178 $ hg status --cwd a
179 179 ? 1/in_a_1
180 180 ? in_a
181 181 ? ../b/1/in_b_1
182 182 ? ../b/2/in_b_2
183 183 ? ../b/in_b
184 184 ? ../in_root
185 185 $ HGPLAIN=1 hg status --cwd a
186 186 ? a/1/in_a_1 (glob)
187 187 ? a/in_a (glob)
188 188 ? b/1/in_b_1 (glob)
189 189 ? b/2/in_b_2 (glob)
190 190 ? b/in_b (glob)
191 191 ? in_root
192 192
193 193 if relative paths are explicitly off, tweakdefaults doesn't change it
194 194 $ cat >> $HGRCPATH <<EOF
195 195 > [commands]
196 196 > status.relative = False
197 197 > EOF
198 198 $ hg status --cwd a --config ui.tweakdefaults=yes
199 199 ? a/1/in_a_1
200 200 ? a/in_a
201 201 ? b/1/in_b_1
202 202 ? b/2/in_b_2
203 203 ? b/in_b
204 204 ? in_root
205 205
206 206 $ cd ..
207 207
208 208 $ hg init repo2
209 209 $ cd repo2
210 210 $ touch modified removed deleted ignored
211 211 $ echo "^ignored$" > .hgignore
212 212 $ hg ci -A -m 'initial checkin'
213 213 adding .hgignore
214 214 adding deleted
215 215 adding modified
216 216 adding removed
217 217 $ touch modified added unknown ignored
218 218 $ hg add added
219 219 $ hg remove removed
220 220 $ rm deleted
221 221
222 222 hg status:
223 223
224 224 $ hg status
225 225 A added
226 226 R removed
227 227 ! deleted
228 228 ? unknown
229 229
230 230 hg status modified added removed deleted unknown never-existed ignored:
231 231
232 232 $ hg status modified added removed deleted unknown never-existed ignored
233 233 never-existed: * (glob)
234 234 A added
235 235 R removed
236 236 ! deleted
237 237 ? unknown
238 238
239 239 $ hg copy modified copied
240 240
241 241 hg status -C:
242 242
243 243 $ hg status -C
244 244 A added
245 245 A copied
246 246 modified
247 247 R removed
248 248 ! deleted
249 249 ? unknown
250 250
251 251 hg status -A:
252 252
253 253 $ hg status -A
254 254 A added
255 255 A copied
256 256 modified
257 257 R removed
258 258 ! deleted
259 259 ? unknown
260 260 I ignored
261 261 C .hgignore
262 262 C modified
263 263
264 264 $ hg status -A -T '{status} {path} {node|shortest}\n'
265 265 A added ffff
266 266 A copied ffff
267 267 R removed ffff
268 268 ! deleted ffff
269 269 ? unknown ffff
270 270 I ignored ffff
271 271 C .hgignore ffff
272 272 C modified ffff
273 273
274 274 $ hg status -A -Tjson
275 275 [
276 276 {
277 277 "itemtype": "file",
278 278 "path": "added",
279 279 "status": "A"
280 280 },
281 281 {
282 282 "itemtype": "file",
283 283 "path": "copied",
284 284 "source": "modified",
285 285 "status": "A"
286 286 },
287 287 {
288 288 "itemtype": "file",
289 289 "path": "removed",
290 290 "status": "R"
291 291 },
292 292 {
293 293 "itemtype": "file",
294 294 "path": "deleted",
295 295 "status": "!"
296 296 },
297 297 {
298 298 "itemtype": "file",
299 299 "path": "unknown",
300 300 "status": "?"
301 301 },
302 302 {
303 303 "itemtype": "file",
304 304 "path": "ignored",
305 305 "status": "I"
306 306 },
307 307 {
308 308 "itemtype": "file",
309 309 "path": ".hgignore",
310 310 "status": "C"
311 311 },
312 312 {
313 313 "itemtype": "file",
314 314 "path": "modified",
315 315 "status": "C"
316 316 }
317 317 ]
318 318
319 319 $ hg status -A -Tpickle > pickle
320 320 >>> from __future__ import print_function
321 321 >>> from mercurial import util
322 322 >>> pickle = util.pickle
323 323 >>> data = sorted((x[b'status'].decode(), x[b'path'].decode()) for x in pickle.load(open("pickle", r"rb")))
324 324 >>> for s, p in data: print("%s %s" % (s, p))
325 325 ! deleted
326 326 ? pickle
327 327 ? unknown
328 328 A added
329 329 A copied
330 330 C .hgignore
331 331 C modified
332 332 I ignored
333 333 R removed
334 334 $ rm pickle
335 335
336 336 $ echo "^ignoreddir$" > .hgignore
337 337 $ mkdir ignoreddir
338 338 $ touch ignoreddir/file
339 339
340 340 Test templater support:
341 341
342 342 $ hg status -AT "[{status}]\t{if(source, '{source} -> ')}{path}\n"
343 343 [M] .hgignore
344 344 [A] added
345 345 [A] modified -> copied
346 346 [R] removed
347 347 [!] deleted
348 348 [?] ignored
349 349 [?] unknown
350 350 [I] ignoreddir/file
351 351 [C] modified
352 352 $ hg status -AT default
353 353 M .hgignore
354 354 A added
355 355 A copied
356 356 modified
357 357 R removed
358 358 ! deleted
359 359 ? ignored
360 360 ? unknown
361 361 I ignoreddir/file
362 362 C modified
363 363 $ hg status -T compact
364 364 abort: "status" not in template map
365 365 [255]
366 366
367 367 hg status ignoreddir/file:
368 368
369 369 $ hg status ignoreddir/file
370 370
371 371 hg status -i ignoreddir/file:
372 372
373 373 $ hg status -i ignoreddir/file
374 374 I ignoreddir/file
375 375 $ cd ..
376 376
377 377 Check 'status -q' and some combinations
378 378
379 379 $ hg init repo3
380 380 $ cd repo3
381 381 $ touch modified removed deleted ignored
382 382 $ echo "^ignored$" > .hgignore
383 383 $ hg commit -A -m 'initial checkin'
384 384 adding .hgignore
385 385 adding deleted
386 386 adding modified
387 387 adding removed
388 388 $ touch added unknown ignored
389 389 $ hg add added
390 390 $ echo "test" >> modified
391 391 $ hg remove removed
392 392 $ rm deleted
393 393 $ hg copy modified copied
394 394
395 395 Specify working directory revision explicitly, that should be the same as
396 396 "hg status"
397 397
398 398 $ hg status --change "wdir()"
399 399 M modified
400 400 A added
401 401 A copied
402 402 R removed
403 403 ! deleted
404 404 ? unknown
405 405
406 406 Run status with 2 different flags.
407 407 Check if result is the same or different.
408 408 If result is not as expected, raise error
409 409
410 410 $ assert() {
411 411 > hg status $1 > ../a
412 412 > hg status $2 > ../b
413 413 > if diff ../a ../b > /dev/null; then
414 414 > out=0
415 415 > else
416 416 > out=1
417 417 > fi
418 418 > if [ $3 -eq 0 ]; then
419 419 > df="same"
420 420 > else
421 421 > df="different"
422 422 > fi
423 423 > if [ $out -ne $3 ]; then
424 424 > echo "Error on $1 and $2, should be $df."
425 425 > fi
426 426 > }
427 427
428 428 Assert flag1 flag2 [0-same | 1-different]
429 429
430 430 $ assert "-q" "-mard" 0
431 431 $ assert "-A" "-marduicC" 0
432 432 $ assert "-qA" "-mardcC" 0
433 433 $ assert "-qAui" "-A" 0
434 434 $ assert "-qAu" "-marducC" 0
435 435 $ assert "-qAi" "-mardicC" 0
436 436 $ assert "-qu" "-u" 0
437 437 $ assert "-q" "-u" 1
438 438 $ assert "-m" "-a" 1
439 439 $ assert "-r" "-d" 1
440 440 $ cd ..
441 441
442 442 $ hg init repo4
443 443 $ cd repo4
444 444 $ touch modified removed deleted
445 445 $ hg ci -q -A -m 'initial checkin'
446 446 $ touch added unknown
447 447 $ hg add added
448 448 $ hg remove removed
449 449 $ rm deleted
450 450 $ echo x > modified
451 451 $ hg copy modified copied
452 452 $ hg ci -m 'test checkin' -d "1000001 0"
453 453 $ rm *
454 454 $ touch unrelated
455 455 $ hg ci -q -A -m 'unrelated checkin' -d "1000002 0"
456 456
457 457 hg status --change 1:
458 458
459 459 $ hg status --change 1
460 460 M modified
461 461 A added
462 462 A copied
463 463 R removed
464 464
465 465 hg status --change 1 unrelated:
466 466
467 467 $ hg status --change 1 unrelated
468 468
469 469 hg status -C --change 1 added modified copied removed deleted:
470 470
471 471 $ hg status -C --change 1 added modified copied removed deleted
472 472 M modified
473 473 A added
474 474 A copied
475 475 modified
476 476 R removed
477 477
478 478 hg status -A --change 1 and revset:
479 479
480 480 $ hg status -A --change '1|1'
481 481 M modified
482 482 A added
483 483 A copied
484 484 modified
485 485 R removed
486 486 C deleted
487 487
488 488 $ cd ..
489 489
490 490 hg status with --rev and reverted changes:
491 491
492 492 $ hg init reverted-changes-repo
493 493 $ cd reverted-changes-repo
494 494 $ echo a > file
495 495 $ hg add file
496 496 $ hg ci -m a
497 497 $ echo b > file
498 498 $ hg ci -m b
499 499
500 500 reverted file should appear clean
501 501
502 502 $ hg revert -r 0 .
503 503 reverting file
504 504 $ hg status -A --rev 0
505 505 C file
506 506
507 507 #if execbit
508 508 reverted file with changed flag should appear modified
509 509
510 510 $ chmod +x file
511 511 $ hg status -A --rev 0
512 512 M file
513 513
514 514 $ hg revert -r 0 .
515 515 reverting file
516 516
517 517 reverted and committed file with changed flag should appear modified
518 518
519 519 $ hg co -C .
520 520 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
521 521 $ chmod +x file
522 522 $ hg ci -m 'change flag'
523 523 $ hg status -A --rev 1 --rev 2
524 524 M file
525 525 $ hg diff -r 1 -r 2
526 526
527 527 #endif
528 528
529 529 $ cd ..
530 530
531 531 hg status of binary file starting with '\1\n', a separator for metadata:
532 532
533 533 $ hg init repo5
534 534 $ cd repo5
535 535 >>> open("010a", r"wb").write(b"\1\nfoo") and None
536 536 $ hg ci -q -A -m 'initial checkin'
537 537 $ hg status -A
538 538 C 010a
539 539
540 540 >>> open("010a", r"wb").write(b"\1\nbar") and None
541 541 $ hg status -A
542 542 M 010a
543 543 $ hg ci -q -m 'modify 010a'
544 544 $ hg status -A --rev 0:1
545 545 M 010a
546 546
547 547 $ touch empty
548 548 $ hg ci -q -A -m 'add another file'
549 549 $ hg status -A --rev 1:2 010a
550 550 C 010a
551 551
552 552 $ cd ..
553 553
554 554 test "hg status" with "directory pattern" which matches against files
555 555 only known on target revision.
556 556
557 557 $ hg init repo6
558 558 $ cd repo6
559 559
560 560 $ echo a > a.txt
561 561 $ hg add a.txt
562 562 $ hg commit -m '#0'
563 563 $ mkdir -p 1/2/3/4/5
564 564 $ echo b > 1/2/3/4/5/b.txt
565 565 $ hg add 1/2/3/4/5/b.txt
566 566 $ hg commit -m '#1'
567 567
568 568 $ hg update -C 0 > /dev/null
569 569 $ hg status -A
570 570 C a.txt
571 571
572 572 the directory matching against specified pattern should be removed,
573 573 because directory existence prevents 'dirstate.walk()' from showing
574 574 warning message about such pattern.
575 575
576 576 $ test ! -d 1
577 577 $ hg status -A --rev 1 1/2/3/4/5/b.txt
578 578 R 1/2/3/4/5/b.txt
579 579 $ hg status -A --rev 1 1/2/3/4/5
580 580 R 1/2/3/4/5/b.txt
581 581 $ hg status -A --rev 1 1/2/3
582 582 R 1/2/3/4/5/b.txt
583 583 $ hg status -A --rev 1 1
584 584 R 1/2/3/4/5/b.txt
585 585
586 586 $ hg status --config ui.formatdebug=True --rev 1 1
587 587 status = [
588 588 {
589 589 'itemtype': 'file',
590 590 'path': '1/2/3/4/5/b.txt',
591 591 'status': 'R'
592 592 },
593 593 ]
594 594
595 595 #if windows
596 596 $ hg --config ui.slash=false status -A --rev 1 1
597 597 R 1\2\3\4\5\b.txt
598 598 #endif
599 599
600 600 $ cd ..
601 601
602 602 Status after move overwriting a file (issue4458)
603 603 =================================================
604 604
605 605
606 606 $ hg init issue4458
607 607 $ cd issue4458
608 608 $ echo a > a
609 609 $ echo b > b
610 610 $ hg commit -Am base
611 611 adding a
612 612 adding b
613 613
614 614
615 615 with --force
616 616
617 617 $ hg mv b --force a
618 618 $ hg st --copies
619 619 M a
620 620 b
621 621 R b
622 622 $ hg revert --all
623 623 reverting a
624 624 undeleting b
625 625 $ rm *.orig
626 626
627 627 without force
628 628
629 629 $ hg rm a
630 630 $ hg st --copies
631 631 R a
632 632 $ hg mv b a
633 633 $ hg st --copies
634 634 M a
635 635 b
636 636 R b
637 637
638 638 using ui.statuscopies setting
639 639 $ hg st --config ui.statuscopies=true
640 640 M a
641 641 b
642 642 R b
643 643 $ hg st --config ui.statuscopies=false
644 644 M a
645 645 R b
646 646 $ hg st --config ui.tweakdefaults=yes
647 647 M a
648 648 b
649 649 R b
650 650
651 651 using log status template (issue5155)
652 652 $ hg log -Tstatus -r 'wdir()' -C
653 653 changeset: 2147483647:ffffffffffff
654 654 parent: 0:8c55c58b4c0e
655 655 user: test
656 656 date: * (glob)
657 657 files:
658 658 M a
659 659 b
660 660 R b
661 661
662 662 $ hg log -GTstatus -r 'wdir()' -C
663 663 o changeset: 2147483647:ffffffffffff
664 664 | parent: 0:8c55c58b4c0e
665 665 ~ user: test
666 666 date: * (glob)
667 667 files:
668 668 M a
669 669 b
670 670 R b
671 671
672 672
673 673 Other "bug" highlight, the revision status does not report the copy information.
674 674 This is buggy behavior.
675 675
676 676 $ hg commit -m 'blah'
677 677 $ hg st --copies --change .
678 678 M a
679 679 R b
680 680
681 681 using log status template, the copy information is displayed correctly.
682 682 $ hg log -Tstatus -r. -C
683 683 changeset: 1:6685fde43d21
684 684 tag: tip
685 685 user: test
686 686 date: * (glob)
687 687 summary: blah
688 688 files:
689 689 M a
690 690 b
691 691 R b
692 692
693 693
694 694 $ cd ..
695 695
696 696 Make sure .hg doesn't show up even as a symlink
697 697
698 698 $ hg init repo0
699 699 $ mkdir symlink-repo0
700 700 $ cd symlink-repo0
701 701 $ ln -s ../repo0/.hg
702 702 $ hg status
703 703
704 704 If the size hasn’t changed but mtime has, status needs to read the contents
705 705 of the file to check whether it has changed
706 706
707 707 $ echo 1 > a
708 708 $ echo 1 > b
709 709 $ touch -t 200102030000 a b
710 710 $ hg commit -Aqm '#0'
711 711 $ echo 2 > a
712 712 $ touch -t 200102040000 a b
713 713 $ hg status
714 714 M a
715 715
716 716 Asking specifically for the status of a deleted/removed file
717 717
718 718 $ rm a
719 719 $ rm b
720 720 $ hg status a
721 721 ! a
722 722 $ hg rm a
723 723 $ hg rm b
724 724 $ hg status a
725 725 R a
726 726 $ hg commit -qm '#1'
727 727 $ hg status a
728 728 a: $ENOENT$
729 729
730 730 Check using include flag with pattern when status does not need to traverse
731 731 the working directory (issue6483)
732 732
733 733 $ cd ..
734 734 $ hg init issue6483
735 735 $ cd issue6483
736 736 $ touch a.py b.rs
737 737 $ hg add a.py b.rs
738 738 $ hg st -aI "*.py"
739 739 A a.py
740 740
741 741 Also check exclude pattern
742 742
743 743 $ hg st -aX "*.rs"
744 744 A a.py
745 745
746 746 issue6335
747 747 When a directory containing a tracked file gets symlinked, as of 5.8
748 748 `hg st` only gives the correct answer about clean (or deleted) files
749 749 if also listing unknowns.
750 750 The tree-based dirstate and status algorithm fix this:
751 751
752 752 #if symlink no-dirstate-v1
753 753
754 754 $ cd ..
755 755 $ hg init issue6335
756 756 $ cd issue6335
757 757 $ mkdir foo
758 758 $ touch foo/a
759 759 $ hg ci -Ama
760 760 adding foo/a
761 761 $ mv foo bar
762 762 $ ln -s bar foo
763 763 $ hg status
764 764 ! foo/a
765 765 ? bar/a
766 766 ? foo
767 767
768 768 $ hg status -c # incorrect output with `dirstate-v1`
769 769 $ hg status -cu
770 770 ? bar/a
771 771 ? foo
772 772 $ hg status -d # incorrect output with `dirstate-v1`
773 773 ! foo/a
774 774 $ hg status -du
775 775 ! foo/a
776 776 ? bar/a
777 777 ? foo
778 778
779 779 #endif
780 780
781 781
782 782 Create a repo with files in each possible status
783 783
784 784 $ cd ..
785 785 $ hg init repo7
786 786 $ cd repo7
787 787 $ mkdir subdir
788 788 $ touch clean modified deleted removed
789 789 $ touch subdir/clean subdir/modified subdir/deleted subdir/removed
790 790 $ echo ignored > .hgignore
791 791 $ hg ci -Aqm '#0'
792 792 $ echo 1 > modified
793 793 $ echo 1 > subdir/modified
794 794 $ rm deleted
795 795 $ rm subdir/deleted
796 796 $ hg rm removed
797 797 $ hg rm subdir/removed
798 798 $ touch unknown ignored
799 799 $ touch subdir/unknown subdir/ignored
800 800
801 801 Check the output
802 802
803 803 $ hg status
804 804 M modified
805 805 M subdir/modified
806 806 R removed
807 807 R subdir/removed
808 808 ! deleted
809 809 ! subdir/deleted
810 810 ? subdir/unknown
811 811 ? unknown
812 812
813 813 $ hg status -mard
814 814 M modified
815 815 M subdir/modified
816 816 R removed
817 817 R subdir/removed
818 818 ! deleted
819 819 ! subdir/deleted
820 820
821 821 $ hg status -A
822 822 M modified
823 823 M subdir/modified
824 824 R removed
825 825 R subdir/removed
826 826 ! deleted
827 827 ! subdir/deleted
828 828 ? subdir/unknown
829 829 ? unknown
830 830 I ignored
831 831 I subdir/ignored
832 832 C .hgignore
833 833 C clean
834 834 C subdir/clean
835 835
836 836 Note: `hg status some-name` creates a patternmatcher which is not supported
837 837 yet by the Rust implementation of status, but includematcher is supported.
838 838 --include is used below for that reason
839 839
840 #if unix-permissions
841
842 Not having permission to read a directory that contains tracked files makes
843 status emit a warning then behave as if the directory was empty or removed
844 entirely:
845
846 $ chmod 0 subdir
847 $ hg status --include subdir
848 subdir: Permission denied
849 R subdir/removed
850 ! subdir/clean
851 ! subdir/deleted
852 ! subdir/modified
853 $ chmod 755 subdir
854
855 #endif
856
840 857 Remove a directory that contains tracked files
841 858
842 859 $ rm -r subdir
843 860 $ hg status --include subdir
844 861 R subdir/removed
845 862 ! subdir/clean
846 863 ! subdir/deleted
847 864 ! subdir/modified
848 865
849 866 … and replace it by a file
850 867
851 868 $ touch subdir
852 869 $ hg status --include subdir
853 870 R subdir/removed
854 871 ! subdir/clean
855 872 ! subdir/deleted
856 873 ! subdir/modified
857 874 ? subdir
858 875
859 876 Replaced a deleted or removed file with a directory
860 877
861 878 $ mkdir deleted removed
862 879 $ touch deleted/1 removed/1
863 880 $ hg status --include deleted --include removed
864 881 R removed
865 882 ! deleted
866 883 ? deleted/1
867 884 ? removed/1
868 885 $ hg add removed/1
869 886 $ hg status --include deleted --include removed
870 887 A removed/1
871 888 R removed
872 889 ! deleted
873 890 ? deleted/1
874 891
875 892 Deeply nested files in an ignored directory are still listed on request
876 893
877 894 $ echo ignored-dir >> .hgignore
878 895 $ mkdir ignored-dir
879 896 $ mkdir ignored-dir/subdir
880 897 $ touch ignored-dir/subdir/1
881 898 $ hg status --ignored
882 899 I ignored
883 900 I ignored-dir/subdir/1
884 901
885 902 Check using include flag while listing ignored composes correctly (issue6514)
886 903
887 904 $ cd ..
888 905 $ hg init issue6514
889 906 $ cd issue6514
890 907 $ mkdir ignored-folder
891 908 $ touch A.hs B.hs C.hs ignored-folder/other.txt ignored-folder/ctest.hs
892 909 $ cat >.hgignore <<EOF
893 910 > A.hs
894 911 > B.hs
895 912 > ignored-folder/
896 913 > EOF
897 914 $ hg st -i -I 're:.*\.hs$'
898 915 I A.hs
899 916 I B.hs
900 917 I ignored-folder/ctest.hs
General Comments 0
You need to be logged in to leave comments. Login now