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