##// END OF EJS Templates
dirstate-tree: Handle I/O errors in status...
Simon Sapin -
r47885:1b4f0f81 default
parent child Browse files
Show More
@@ -1,385 +1,408
1 1 use crate::dirstate::status::IgnoreFnType;
2 2 use crate::dirstate_tree::dirstate_map::ChildNodes;
3 3 use crate::dirstate_tree::dirstate_map::DirstateMap;
4 4 use crate::dirstate_tree::dirstate_map::Node;
5 5 use crate::matchers::get_ignore_function;
6 6 use crate::matchers::Matcher;
7 7 use crate::utils::files::get_bytes_from_os_string;
8 8 use crate::utils::hg_path::HgPath;
9 use crate::BadMatch;
9 10 use crate::DirstateStatus;
10 11 use crate::EntryState;
11 12 use crate::HgPathBuf;
12 13 use crate::PatternFileWarning;
13 14 use crate::StatusError;
14 15 use crate::StatusOptions;
15 16 use std::borrow::Cow;
16 17 use std::io;
17 18 use std::path::Path;
18 19 use std::path::PathBuf;
19 20
20 21 /// Returns the status of the working directory compared to its parent
21 22 /// changeset.
22 23 ///
23 24 /// This algorithm is based on traversing the filesystem tree (`fs` in function
24 25 /// and variable names) and dirstate tree at the same time. The core of this
25 26 /// traversal is the recursive `traverse_fs_directory_and_dirstate` function
26 27 /// and its use of `itertools::merge_join_by`. When reaching a path that only
27 28 /// exists in one of the two trees, depending on information requested by
28 29 /// `options` we may need to traverse the remaining subtree.
29 30 pub fn status<'tree>(
30 31 dmap: &'tree mut DirstateMap,
31 32 matcher: &(dyn Matcher + Sync),
32 33 root_dir: PathBuf,
33 34 ignore_files: Vec<PathBuf>,
34 35 options: StatusOptions,
35 36 ) -> Result<(DirstateStatus<'tree>, Vec<PatternFileWarning>), StatusError> {
36 37 let (ignore_fn, warnings): (IgnoreFnType, _) =
37 38 if options.list_ignored || options.list_unknown {
38 39 get_ignore_function(ignore_files, &root_dir)?
39 40 } else {
40 41 (Box::new(|&_| true), vec![])
41 42 };
42 43
43 44 let mut common = StatusCommon {
44 45 options,
45 46 matcher,
46 47 ignore_fn,
47 48 outcome: DirstateStatus::default(),
48 49 };
49 50 let is_at_repo_root = true;
50 51 let hg_path = HgPath::new("");
51 52 let has_ignored_ancestor = false;
52 53 common.traverse_fs_directory_and_dirstate(
53 54 has_ignored_ancestor,
54 55 &mut dmap.root,
55 56 hg_path,
56 57 &root_dir,
57 58 is_at_repo_root,
58 59 );
59 60 Ok((common.outcome, warnings))
60 61 }
61 62
62 63 /// Bag of random things needed by various parts of the algorithm. Reduces the
63 64 /// number of parameters passed to functions.
64 65 struct StatusCommon<'tree, 'a> {
65 66 options: StatusOptions,
66 67 matcher: &'a (dyn Matcher + Sync),
67 68 ignore_fn: IgnoreFnType<'a>,
68 69 outcome: DirstateStatus<'tree>,
69 70 }
70 71
71 72 impl<'tree, 'a> StatusCommon<'tree, 'a> {
73 fn read_dir(
74 &mut self,
75 hg_path: &HgPath,
76 fs_path: &Path,
77 is_at_repo_root: bool,
78 ) -> Result<Vec<DirEntry>, ()> {
79 DirEntry::read_dir(fs_path, is_at_repo_root).map_err(|error| {
80 let errno = error.raw_os_error().expect("expected real OS error");
81 self.outcome
82 .bad
83 .push((hg_path.to_owned().into(), BadMatch::OsError(errno)))
84 })
85 }
86
72 87 fn traverse_fs_directory_and_dirstate(
73 88 &mut self,
74 89 has_ignored_ancestor: bool,
75 90 dirstate_nodes: &'tree mut ChildNodes,
76 directory_hg_path: &HgPath,
77 fs_path: &Path,
91 directory_hg_path: &'tree HgPath,
92 directory_fs_path: &Path,
78 93 is_at_repo_root: bool,
79 94 ) {
80 // TODO: handle I/O errors
81 let mut fs_entries =
82 DirEntry::read_dir(fs_path, is_at_repo_root).unwrap();
95 let mut fs_entries = if let Ok(entries) = self.read_dir(
96 directory_hg_path,
97 directory_fs_path,
98 is_at_repo_root,
99 ) {
100 entries
101 } else {
102 return;
103 };
83 104
84 105 // `merge_join_by` requires both its input iterators to be sorted:
85 106
86 107 // * `BTreeMap` iterates according to keys’ ordering by definition
87 108
88 109 // `sort_unstable_by_key` doesn’t allow keys borrowing from the value:
89 110 // https://github.com/rust-lang/rust/issues/34162
90 111 fs_entries.sort_unstable_by(|e1, e2| e1.base_name.cmp(&e2.base_name));
91 112
92 113 for pair in itertools::merge_join_by(
93 114 dirstate_nodes,
94 115 &fs_entries,
95 116 |(full_path, _node), fs_entry| {
96 117 full_path.base_name().cmp(&fs_entry.base_name)
97 118 },
98 119 ) {
99 120 use itertools::EitherOrBoth::*;
100 121 match pair {
101 122 Both((hg_path, dirstate_node), fs_entry) => {
102 123 self.traverse_fs_and_dirstate(
103 124 fs_entry,
104 125 hg_path.full_path(),
105 126 dirstate_node,
106 127 has_ignored_ancestor,
107 128 );
108 129 }
109 130 Left((hg_path, dirstate_node)) => self.traverse_dirstate_only(
110 131 hg_path.full_path(),
111 132 dirstate_node,
112 133 ),
113 134 Right(fs_entry) => self.traverse_fs_only(
114 135 has_ignored_ancestor,
115 136 directory_hg_path,
116 137 fs_entry,
117 138 ),
118 139 }
119 140 }
120 141 }
121 142
122 143 fn traverse_fs_and_dirstate(
123 144 &mut self,
124 145 fs_entry: &DirEntry,
125 146 hg_path: &'tree HgPath,
126 147 dirstate_node: &'tree mut Node,
127 148 has_ignored_ancestor: bool,
128 149 ) {
129 150 let file_type = fs_entry.metadata.file_type();
130 151 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
131 152 if !file_or_symlink {
132 153 // If we previously had a file here, it was removed (with
133 154 // `hg rm` or similar) or deleted before it could be
134 155 // replaced by a directory or something else.
135 156 self.mark_removed_or_deleted_if_file(
136 157 hg_path,
137 158 dirstate_node.state(),
138 159 );
139 160 }
140 161 if file_type.is_dir() {
141 162 if self.options.collect_traversed_dirs {
142 163 self.outcome.traversed.push(hg_path.into())
143 164 }
144 165 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path);
145 166 let is_at_repo_root = false;
146 167 self.traverse_fs_directory_and_dirstate(
147 168 is_ignored,
148 169 &mut dirstate_node.children,
149 170 hg_path,
150 171 &fs_entry.full_path,
151 172 is_at_repo_root,
152 173 );
153 174 } else {
154 175 if file_or_symlink && self.matcher.matches(hg_path) {
155 176 let full_path = Cow::from(hg_path);
156 177 if let Some(entry) = &dirstate_node.entry {
157 178 match entry.state {
158 179 EntryState::Added => {
159 180 self.outcome.added.push(full_path)
160 181 }
161 182 EntryState::Removed => {
162 183 self.outcome.removed.push(full_path)
163 184 }
164 185 EntryState::Merged => {
165 186 self.outcome.modified.push(full_path)
166 187 }
167 188 EntryState::Normal => {
168 189 self.handle_normal_file(
169 190 full_path,
170 191 dirstate_node,
171 192 entry,
172 193 fs_entry,
173 194 );
174 195 }
175 196 // This variant is not used in DirstateMap
176 197 // nodes
177 198 EntryState::Unknown => unreachable!(),
178 199 }
179 200 } else {
180 201 // `node.entry.is_none()` indicates a "directory"
181 202 // node, but the filesystem has a file
182 203 self.mark_unknown_or_ignored(
183 204 has_ignored_ancestor,
184 205 full_path,
185 206 )
186 207 }
187 208 }
188 209
189 210 for (child_hg_path, child_node) in &mut dirstate_node.children {
190 211 self.traverse_dirstate_only(
191 212 child_hg_path.full_path(),
192 213 child_node,
193 214 )
194 215 }
195 216 }
196 217 }
197 218
198 219 /// A file with `EntryState::Normal` in the dirstate was found in the
199 220 /// filesystem
200 221 fn handle_normal_file(
201 222 &mut self,
202 223 full_path: Cow<'tree, HgPath>,
203 224 dirstate_node: &Node,
204 225 entry: &crate::DirstateEntry,
205 226 fs_entry: &DirEntry,
206 227 ) {
207 228 // Keep the low 31 bits
208 229 fn truncate_u64(value: u64) -> i32 {
209 230 (value & 0x7FFF_FFFF) as i32
210 231 }
211 232 fn truncate_i64(value: i64) -> i32 {
212 233 (value & 0x7FFF_FFFF) as i32
213 234 }
214 235
215 236 let mode_changed = || {
216 237 self.options.check_exec && entry.mode_changed(&fs_entry.metadata)
217 238 };
218 239 let size_changed = entry.size != truncate_u64(fs_entry.metadata.len());
219 240 if entry.size >= 0
220 241 && size_changed
221 242 && fs_entry.metadata.file_type().is_symlink()
222 243 {
223 244 // issue6456: Size returned may be longer due to encryption
224 245 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
225 246 self.outcome.unsure.push(full_path)
226 247 } else if dirstate_node.copy_source.is_some()
227 248 || entry.is_from_other_parent()
228 249 || (entry.size >= 0 && (size_changed || mode_changed()))
229 250 {
230 251 self.outcome.modified.push(full_path)
231 252 } else {
232 253 let mtime = mtime_seconds(&fs_entry.metadata);
233 254 if truncate_i64(mtime) != entry.mtime
234 255 || mtime == self.options.last_normal_time
235 256 {
236 257 self.outcome.unsure.push(full_path)
237 258 } else if self.options.list_clean {
238 259 self.outcome.clean.push(full_path)
239 260 }
240 261 }
241 262 }
242 263
243 264 /// A node in the dirstate tree has no corresponding filesystem entry
244 265 fn traverse_dirstate_only(
245 266 &mut self,
246 267 hg_path: &'tree HgPath,
247 268 dirstate_node: &'tree mut Node,
248 269 ) {
249 270 self.mark_removed_or_deleted_if_file(hg_path, dirstate_node.state());
250 271 for (child_hg_path, child_node) in &mut dirstate_node.children {
251 272 self.traverse_dirstate_only(child_hg_path.full_path(), child_node)
252 273 }
253 274 }
254 275
255 276 /// A node in the dirstate tree has no corresponding *file* on the
256 277 /// filesystem
257 278 ///
258 279 /// Does nothing on a "directory" node
259 280 fn mark_removed_or_deleted_if_file(
260 281 &mut self,
261 282 hg_path: &'tree HgPath,
262 283 dirstate_node_state: Option<EntryState>,
263 284 ) {
264 285 if let Some(state) = dirstate_node_state {
265 286 if self.matcher.matches(hg_path) {
266 287 if let EntryState::Removed = state {
267 288 self.outcome.removed.push(hg_path.into())
268 289 } else {
269 290 self.outcome.deleted.push(hg_path.into())
270 291 }
271 292 }
272 293 }
273 294 }
274 295
275 296 /// Something in the filesystem has no corresponding dirstate node
276 297 fn traverse_fs_only(
277 298 &mut self,
278 299 has_ignored_ancestor: bool,
279 300 directory_hg_path: &HgPath,
280 301 fs_entry: &DirEntry,
281 302 ) {
282 303 let hg_path = directory_hg_path.join(&fs_entry.base_name);
283 304 let file_type = fs_entry.metadata.file_type();
284 305 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
285 306 if file_type.is_dir() {
286 307 let is_ignored =
287 308 has_ignored_ancestor || (self.ignore_fn)(&hg_path);
288 309 let traverse_children = if is_ignored {
289 310 // Descendants of an ignored directory are all ignored
290 311 self.options.list_ignored
291 312 } else {
292 313 // Descendants of an unknown directory may be either unknown or
293 314 // ignored
294 315 self.options.list_unknown || self.options.list_ignored
295 316 };
296 317 if traverse_children {
297 318 let is_at_repo_root = false;
298 // TODO: handle I/O errors
299 let children_fs_entries =
300 DirEntry::read_dir(&fs_entry.full_path, is_at_repo_root)
301 .unwrap();
302 for child_fs_entry in children_fs_entries {
303 self.traverse_fs_only(
304 is_ignored,
305 &hg_path,
306 &child_fs_entry,
307 )
319 if let Ok(children_fs_entries) = self.read_dir(
320 &hg_path,
321 &fs_entry.full_path,
322 is_at_repo_root,
323 ) {
324 for child_fs_entry in children_fs_entries {
325 self.traverse_fs_only(
326 is_ignored,
327 &hg_path,
328 &child_fs_entry,
329 )
330 }
308 331 }
309 332 }
310 333 if self.options.collect_traversed_dirs {
311 334 self.outcome.traversed.push(hg_path.into())
312 335 }
313 336 } else if file_or_symlink && self.matcher.matches(&hg_path) {
314 337 self.mark_unknown_or_ignored(has_ignored_ancestor, hg_path.into())
315 338 }
316 339 }
317 340
318 341 fn mark_unknown_or_ignored(
319 342 &mut self,
320 343 has_ignored_ancestor: bool,
321 344 hg_path: Cow<'tree, HgPath>,
322 345 ) {
323 346 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path);
324 347 if is_ignored {
325 348 if self.options.list_ignored {
326 349 self.outcome.ignored.push(hg_path)
327 350 }
328 351 } else {
329 352 if self.options.list_unknown {
330 353 self.outcome.unknown.push(hg_path)
331 354 }
332 355 }
333 356 }
334 357 }
335 358
336 359 #[cfg(unix)] // TODO
337 360 fn mtime_seconds(metadata: &std::fs::Metadata) -> i64 {
338 361 // Going through `Metadata::modified()` would be portable, but would take
339 362 // care to construct a `SystemTime` value with sub-second precision just
340 363 // for us to throw that away here.
341 364 use std::os::unix::fs::MetadataExt;
342 365 metadata.mtime()
343 366 }
344 367
345 368 struct DirEntry {
346 369 base_name: HgPathBuf,
347 370 full_path: PathBuf,
348 371 metadata: std::fs::Metadata,
349 372 }
350 373
351 374 impl DirEntry {
352 375 /// Returns **unsorted** entries in the given directory, with name and
353 376 /// metadata.
354 377 ///
355 378 /// If a `.hg` sub-directory is encountered:
356 379 ///
357 380 /// * At the repository root, ignore that sub-directory
358 381 /// * Elsewhere, we’re listing the content of a sub-repo. Return an empty
359 382 /// list instead.
360 383 fn read_dir(path: &Path, is_at_repo_root: bool) -> io::Result<Vec<Self>> {
361 384 let mut results = Vec::new();
362 385 for entry in path.read_dir()? {
363 386 let entry = entry?;
364 387 let metadata = entry.metadata()?;
365 388 let name = get_bytes_from_os_string(entry.file_name());
366 389 // FIXME don't do this when cached
367 390 if name == b".hg" {
368 391 if is_at_repo_root {
369 392 // Skip the repo’s own .hg (might be a symlink)
370 393 continue;
371 394 } else if metadata.is_dir() {
372 395 // A .hg sub-directory at another location means a subrepo,
373 396 // skip it entirely.
374 397 return Ok(Vec::new());
375 398 }
376 399 }
377 400 results.push(DirEntry {
378 401 base_name: name.into(),
379 402 full_path: entry.path(),
380 403 metadata,
381 404 })
382 405 }
383 406 Ok(results)
384 407 }
385 408 }
General Comments 0
You need to be logged in to leave comments. Login now