##// END OF EJS Templates
rust-status: add util for listing a directory...
Raphaël Gomès -
r45010:0d97bcb3 default
parent child Browse files
Show More
@@ -1,306 +1,335 b''
1 // status.rs
1 // status.rs
2 //
2 //
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
6 // GNU General Public License version 2 or any later version.
7
7
8 //! Rust implementation of dirstate.status (dirstate.py).
8 //! Rust implementation of dirstate.status (dirstate.py).
9 //! It is currently missing a lot of functionality compared to the Python one
9 //! It is currently missing a lot of functionality compared to the Python one
10 //! and will only be triggered in narrow cases.
10 //! and will only be triggered in narrow cases.
11
11
12 use crate::{
12 use crate::{
13 dirstate::SIZE_FROM_OTHER_PARENT,
13 dirstate::SIZE_FROM_OTHER_PARENT,
14 matchers::Matcher,
14 matchers::Matcher,
15 utils::{
15 utils::{
16 files::HgMetadata,
16 files::HgMetadata,
17 hg_path::{hg_path_to_path_buf, HgPath},
17 hg_path::{
18 hg_path_to_path_buf, os_string_to_hg_path_buf, HgPath, HgPathBuf,
19 },
18 },
20 },
19 CopyMap, DirstateEntry, DirstateMap, EntryState,
21 CopyMap, DirstateEntry, DirstateMap, EntryState,
20 };
22 };
21 use rayon::prelude::*;
23 use rayon::prelude::*;
22 use std::collections::HashSet;
24 use std::collections::HashSet;
25 use std::fs::{read_dir, DirEntry};
23 use std::path::Path;
26 use std::path::Path;
24
27
25 /// Marker enum used to dispatch new status entries into the right collections.
28 /// Marker enum used to dispatch new status entries into the right collections.
26 /// Is similar to `crate::EntryState`, but represents the transient state of
29 /// Is similar to `crate::EntryState`, but represents the transient state of
27 /// entries during the lifetime of a command.
30 /// entries during the lifetime of a command.
28 enum Dispatch {
31 enum Dispatch {
29 Unsure,
32 Unsure,
30 Modified,
33 Modified,
31 Added,
34 Added,
32 Removed,
35 Removed,
33 Deleted,
36 Deleted,
34 Clean,
37 Clean,
35 Unknown,
38 Unknown,
36 }
39 }
37
40
38 type IoResult<T> = std::io::Result<T>;
41 type IoResult<T> = std::io::Result<T>;
39
42
40 /// Dates and times that are outside the 31-bit signed range are compared
43 /// Dates and times that are outside the 31-bit signed range are compared
41 /// modulo 2^31. This should prevent hg from behaving badly with very large
44 /// modulo 2^31. This should prevent hg from behaving badly with very large
42 /// files or corrupt dates while still having a high probability of detecting
45 /// files or corrupt dates while still having a high probability of detecting
43 /// changes. (issue2608)
46 /// changes. (issue2608)
44 /// TODO I haven't found a way of having `b` be `Into<i32>`, since `From<u64>`
47 /// TODO I haven't found a way of having `b` be `Into<i32>`, since `From<u64>`
45 /// is not defined for `i32`, and there is no `As` trait. This forces the
48 /// is not defined for `i32`, and there is no `As` trait. This forces the
46 /// caller to cast `b` as `i32`.
49 /// caller to cast `b` as `i32`.
47 fn mod_compare(a: i32, b: i32) -> bool {
50 fn mod_compare(a: i32, b: i32) -> bool {
48 a & i32::max_value() != b & i32::max_value()
51 a & i32::max_value() != b & i32::max_value()
49 }
52 }
50
53
54 /// Return a sorted list containing information about the entries
55 /// in the directory.
56 ///
57 /// * `skip_dot_hg` - Return an empty vec if `path` contains a `.hg` directory
58 fn list_directory(
59 path: impl AsRef<Path>,
60 skip_dot_hg: bool,
61 ) -> std::io::Result<Vec<(HgPathBuf, DirEntry)>> {
62 let mut results = vec![];
63 let entries = read_dir(path.as_ref())?;
64
65 for entry in entries {
66 let entry = entry?;
67 let filename = os_string_to_hg_path_buf(entry.file_name())?;
68 let file_type = entry.file_type()?;
69 if skip_dot_hg && filename.as_bytes() == b".hg" && file_type.is_dir() {
70 return Ok(vec![]);
71 } else {
72 results.push((HgPathBuf::from(filename), entry))
73 }
74 }
75
76 results.sort_unstable_by_key(|e| e.0.clone());
77 Ok(results)
78 }
79
51 /// The file corresponding to the dirstate entry was found on the filesystem.
80 /// The file corresponding to the dirstate entry was found on the filesystem.
52 fn dispatch_found(
81 fn dispatch_found(
53 filename: impl AsRef<HgPath>,
82 filename: impl AsRef<HgPath>,
54 entry: DirstateEntry,
83 entry: DirstateEntry,
55 metadata: HgMetadata,
84 metadata: HgMetadata,
56 copy_map: &CopyMap,
85 copy_map: &CopyMap,
57 check_exec: bool,
86 check_exec: bool,
58 list_clean: bool,
87 list_clean: bool,
59 last_normal_time: i64,
88 last_normal_time: i64,
60 ) -> Dispatch {
89 ) -> Dispatch {
61 let DirstateEntry {
90 let DirstateEntry {
62 state,
91 state,
63 mode,
92 mode,
64 mtime,
93 mtime,
65 size,
94 size,
66 } = entry;
95 } = entry;
67
96
68 let HgMetadata {
97 let HgMetadata {
69 st_mode,
98 st_mode,
70 st_size,
99 st_size,
71 st_mtime,
100 st_mtime,
72 ..
101 ..
73 } = metadata;
102 } = metadata;
74
103
75 match state {
104 match state {
76 EntryState::Normal => {
105 EntryState::Normal => {
77 let size_changed = mod_compare(size, st_size as i32);
106 let size_changed = mod_compare(size, st_size as i32);
78 let mode_changed =
107 let mode_changed =
79 (mode ^ st_mode as i32) & 0o100 != 0o000 && check_exec;
108 (mode ^ st_mode as i32) & 0o100 != 0o000 && check_exec;
80 let metadata_changed = size >= 0 && (size_changed || mode_changed);
109 let metadata_changed = size >= 0 && (size_changed || mode_changed);
81 let other_parent = size == SIZE_FROM_OTHER_PARENT;
110 let other_parent = size == SIZE_FROM_OTHER_PARENT;
82 if metadata_changed
111 if metadata_changed
83 || other_parent
112 || other_parent
84 || copy_map.contains_key(filename.as_ref())
113 || copy_map.contains_key(filename.as_ref())
85 {
114 {
86 Dispatch::Modified
115 Dispatch::Modified
87 } else if mod_compare(mtime, st_mtime as i32) {
116 } else if mod_compare(mtime, st_mtime as i32) {
88 Dispatch::Unsure
117 Dispatch::Unsure
89 } else if st_mtime == last_normal_time {
118 } else if st_mtime == last_normal_time {
90 // the file may have just been marked as normal and
119 // the file may have just been marked as normal and
91 // it may have changed in the same second without
120 // it may have changed in the same second without
92 // changing its size. This can happen if we quickly
121 // changing its size. This can happen if we quickly
93 // do multiple commits. Force lookup, so we don't
122 // do multiple commits. Force lookup, so we don't
94 // miss such a racy file change.
123 // miss such a racy file change.
95 Dispatch::Unsure
124 Dispatch::Unsure
96 } else if list_clean {
125 } else if list_clean {
97 Dispatch::Clean
126 Dispatch::Clean
98 } else {
127 } else {
99 Dispatch::Unknown
128 Dispatch::Unknown
100 }
129 }
101 }
130 }
102 EntryState::Merged => Dispatch::Modified,
131 EntryState::Merged => Dispatch::Modified,
103 EntryState::Added => Dispatch::Added,
132 EntryState::Added => Dispatch::Added,
104 EntryState::Removed => Dispatch::Removed,
133 EntryState::Removed => Dispatch::Removed,
105 EntryState::Unknown => Dispatch::Unknown,
134 EntryState::Unknown => Dispatch::Unknown,
106 }
135 }
107 }
136 }
108
137
109 /// The file corresponding to this Dirstate entry is missing.
138 /// The file corresponding to this Dirstate entry is missing.
110 fn dispatch_missing(state: EntryState) -> Dispatch {
139 fn dispatch_missing(state: EntryState) -> Dispatch {
111 match state {
140 match state {
112 // File was removed from the filesystem during commands
141 // File was removed from the filesystem during commands
113 EntryState::Normal | EntryState::Merged | EntryState::Added => {
142 EntryState::Normal | EntryState::Merged | EntryState::Added => {
114 Dispatch::Deleted
143 Dispatch::Deleted
115 }
144 }
116 // File was removed, everything is normal
145 // File was removed, everything is normal
117 EntryState::Removed => Dispatch::Removed,
146 EntryState::Removed => Dispatch::Removed,
118 // File is unknown to Mercurial, everything is normal
147 // File is unknown to Mercurial, everything is normal
119 EntryState::Unknown => Dispatch::Unknown,
148 EntryState::Unknown => Dispatch::Unknown,
120 }
149 }
121 }
150 }
122
151
123 /// Get stat data about the files explicitly specified by match.
152 /// Get stat data about the files explicitly specified by match.
124 /// TODO subrepos
153 /// TODO subrepos
125 fn walk_explicit<'a>(
154 fn walk_explicit<'a>(
126 files: &'a HashSet<&HgPath>,
155 files: &'a HashSet<&HgPath>,
127 dmap: &'a DirstateMap,
156 dmap: &'a DirstateMap,
128 root_dir: impl AsRef<Path> + Sync + Send,
157 root_dir: impl AsRef<Path> + Sync + Send,
129 check_exec: bool,
158 check_exec: bool,
130 list_clean: bool,
159 list_clean: bool,
131 last_normal_time: i64,
160 last_normal_time: i64,
132 ) -> impl ParallelIterator<Item = IoResult<(&'a HgPath, Dispatch)>> {
161 ) -> impl ParallelIterator<Item = IoResult<(&'a HgPath, Dispatch)>> {
133 files.par_iter().filter_map(move |filename| {
162 files.par_iter().filter_map(move |filename| {
134 // TODO normalization
163 // TODO normalization
135 let normalized = filename.as_ref();
164 let normalized = filename.as_ref();
136
165
137 let buf = match hg_path_to_path_buf(normalized) {
166 let buf = match hg_path_to_path_buf(normalized) {
138 Ok(x) => x,
167 Ok(x) => x,
139 Err(e) => return Some(Err(e.into())),
168 Err(e) => return Some(Err(e.into())),
140 };
169 };
141 let target = root_dir.as_ref().join(buf);
170 let target = root_dir.as_ref().join(buf);
142 let st = target.symlink_metadata();
171 let st = target.symlink_metadata();
143 match st {
172 match st {
144 Ok(meta) => {
173 Ok(meta) => {
145 let file_type = meta.file_type();
174 let file_type = meta.file_type();
146 if file_type.is_file() || file_type.is_symlink() {
175 if file_type.is_file() || file_type.is_symlink() {
147 if let Some(entry) = dmap.get(normalized) {
176 if let Some(entry) = dmap.get(normalized) {
148 return Some(Ok((
177 return Some(Ok((
149 normalized,
178 normalized,
150 dispatch_found(
179 dispatch_found(
151 &normalized,
180 &normalized,
152 *entry,
181 *entry,
153 HgMetadata::from_metadata(meta),
182 HgMetadata::from_metadata(meta),
154 &dmap.copy_map,
183 &dmap.copy_map,
155 check_exec,
184 check_exec,
156 list_clean,
185 list_clean,
157 last_normal_time,
186 last_normal_time,
158 ),
187 ),
159 )));
188 )));
160 }
189 }
161 } else {
190 } else {
162 if dmap.contains_key(normalized) {
191 if dmap.contains_key(normalized) {
163 return Some(Ok((normalized, Dispatch::Removed)));
192 return Some(Ok((normalized, Dispatch::Removed)));
164 }
193 }
165 }
194 }
166 }
195 }
167 Err(_) => {
196 Err(_) => {
168 if let Some(entry) = dmap.get(normalized) {
197 if let Some(entry) = dmap.get(normalized) {
169 return Some(Ok((
198 return Some(Ok((
170 normalized,
199 normalized,
171 dispatch_missing(entry.state),
200 dispatch_missing(entry.state),
172 )));
201 )));
173 }
202 }
174 }
203 }
175 };
204 };
176 None
205 None
177 })
206 })
178 }
207 }
179
208
180 /// Stat all entries in the `DirstateMap` and mark them for dispatch into
209 /// Stat all entries in the `DirstateMap` and mark them for dispatch into
181 /// the relevant collections.
210 /// the relevant collections.
182 fn stat_dmap_entries(
211 fn stat_dmap_entries(
183 dmap: &DirstateMap,
212 dmap: &DirstateMap,
184 root_dir: impl AsRef<Path> + Sync + Send,
213 root_dir: impl AsRef<Path> + Sync + Send,
185 check_exec: bool,
214 check_exec: bool,
186 list_clean: bool,
215 list_clean: bool,
187 last_normal_time: i64,
216 last_normal_time: i64,
188 ) -> impl ParallelIterator<Item = IoResult<(&HgPath, Dispatch)>> {
217 ) -> impl ParallelIterator<Item = IoResult<(&HgPath, Dispatch)>> {
189 dmap.par_iter().map(move |(filename, entry)| {
218 dmap.par_iter().map(move |(filename, entry)| {
190 let filename: &HgPath = filename;
219 let filename: &HgPath = filename;
191 let filename_as_path = hg_path_to_path_buf(filename)?;
220 let filename_as_path = hg_path_to_path_buf(filename)?;
192 let meta = root_dir.as_ref().join(filename_as_path).symlink_metadata();
221 let meta = root_dir.as_ref().join(filename_as_path).symlink_metadata();
193
222
194 match meta {
223 match meta {
195 Ok(ref m)
224 Ok(ref m)
196 if !(m.file_type().is_file()
225 if !(m.file_type().is_file()
197 || m.file_type().is_symlink()) =>
226 || m.file_type().is_symlink()) =>
198 {
227 {
199 Ok((filename, dispatch_missing(entry.state)))
228 Ok((filename, dispatch_missing(entry.state)))
200 }
229 }
201 Ok(m) => Ok((
230 Ok(m) => Ok((
202 filename,
231 filename,
203 dispatch_found(
232 dispatch_found(
204 filename,
233 filename,
205 *entry,
234 *entry,
206 HgMetadata::from_metadata(m),
235 HgMetadata::from_metadata(m),
207 &dmap.copy_map,
236 &dmap.copy_map,
208 check_exec,
237 check_exec,
209 list_clean,
238 list_clean,
210 last_normal_time,
239 last_normal_time,
211 ),
240 ),
212 )),
241 )),
213 Err(ref e)
242 Err(ref e)
214 if e.kind() == std::io::ErrorKind::NotFound
243 if e.kind() == std::io::ErrorKind::NotFound
215 || e.raw_os_error() == Some(20) =>
244 || e.raw_os_error() == Some(20) =>
216 {
245 {
217 // Rust does not yet have an `ErrorKind` for
246 // Rust does not yet have an `ErrorKind` for
218 // `NotADirectory` (errno 20)
247 // `NotADirectory` (errno 20)
219 // It happens if the dirstate contains `foo/bar` and
248 // It happens if the dirstate contains `foo/bar` and
220 // foo is not a directory
249 // foo is not a directory
221 Ok((filename, dispatch_missing(entry.state)))
250 Ok((filename, dispatch_missing(entry.state)))
222 }
251 }
223 Err(e) => Err(e),
252 Err(e) => Err(e),
224 }
253 }
225 })
254 })
226 }
255 }
227
256
228 pub struct StatusResult<'a> {
257 pub struct StatusResult<'a> {
229 pub modified: Vec<&'a HgPath>,
258 pub modified: Vec<&'a HgPath>,
230 pub added: Vec<&'a HgPath>,
259 pub added: Vec<&'a HgPath>,
231 pub removed: Vec<&'a HgPath>,
260 pub removed: Vec<&'a HgPath>,
232 pub deleted: Vec<&'a HgPath>,
261 pub deleted: Vec<&'a HgPath>,
233 pub clean: Vec<&'a HgPath>,
262 pub clean: Vec<&'a HgPath>,
234 /* TODO ignored
263 /* TODO ignored
235 * TODO unknown */
264 * TODO unknown */
236 }
265 }
237
266
238 fn build_response<'a>(
267 fn build_response<'a>(
239 results: impl IntoIterator<Item = IoResult<(&'a HgPath, Dispatch)>>,
268 results: impl IntoIterator<Item = IoResult<(&'a HgPath, Dispatch)>>,
240 ) -> IoResult<(Vec<&'a HgPath>, StatusResult<'a>)> {
269 ) -> IoResult<(Vec<&'a HgPath>, StatusResult<'a>)> {
241 let mut lookup = vec![];
270 let mut lookup = vec![];
242 let mut modified = vec![];
271 let mut modified = vec![];
243 let mut added = vec![];
272 let mut added = vec![];
244 let mut removed = vec![];
273 let mut removed = vec![];
245 let mut deleted = vec![];
274 let mut deleted = vec![];
246 let mut clean = vec![];
275 let mut clean = vec![];
247
276
248 for res in results.into_iter() {
277 for res in results.into_iter() {
249 let (filename, dispatch) = res?;
278 let (filename, dispatch) = res?;
250 match dispatch {
279 match dispatch {
251 Dispatch::Unknown => {}
280 Dispatch::Unknown => {}
252 Dispatch::Unsure => lookup.push(filename),
281 Dispatch::Unsure => lookup.push(filename),
253 Dispatch::Modified => modified.push(filename),
282 Dispatch::Modified => modified.push(filename),
254 Dispatch::Added => added.push(filename),
283 Dispatch::Added => added.push(filename),
255 Dispatch::Removed => removed.push(filename),
284 Dispatch::Removed => removed.push(filename),
256 Dispatch::Deleted => deleted.push(filename),
285 Dispatch::Deleted => deleted.push(filename),
257 Dispatch::Clean => clean.push(filename),
286 Dispatch::Clean => clean.push(filename),
258 }
287 }
259 }
288 }
260
289
261 Ok((
290 Ok((
262 lookup,
291 lookup,
263 StatusResult {
292 StatusResult {
264 modified,
293 modified,
265 added,
294 added,
266 removed,
295 removed,
267 deleted,
296 deleted,
268 clean,
297 clean,
269 },
298 },
270 ))
299 ))
271 }
300 }
272
301
273 pub fn status<'a: 'c, 'b: 'c, 'c>(
302 pub fn status<'a: 'c, 'b: 'c, 'c>(
274 dmap: &'a DirstateMap,
303 dmap: &'a DirstateMap,
275 matcher: &'b impl Matcher,
304 matcher: &'b impl Matcher,
276 root_dir: impl AsRef<Path> + Sync + Send + Copy,
305 root_dir: impl AsRef<Path> + Sync + Send + Copy,
277 list_clean: bool,
306 list_clean: bool,
278 last_normal_time: i64,
307 last_normal_time: i64,
279 check_exec: bool,
308 check_exec: bool,
280 ) -> IoResult<(Vec<&'c HgPath>, StatusResult<'c>)> {
309 ) -> IoResult<(Vec<&'c HgPath>, StatusResult<'c>)> {
281 let files = matcher.file_set();
310 let files = matcher.file_set();
282 let mut results = vec![];
311 let mut results = vec![];
283 if let Some(files) = files {
312 if let Some(files) = files {
284 results.par_extend(walk_explicit(
313 results.par_extend(walk_explicit(
285 &files,
314 &files,
286 &dmap,
315 &dmap,
287 root_dir,
316 root_dir,
288 check_exec,
317 check_exec,
289 list_clean,
318 list_clean,
290 last_normal_time,
319 last_normal_time,
291 ));
320 ));
292 }
321 }
293
322
294 if !matcher.is_exact() {
323 if !matcher.is_exact() {
295 let stat_results = stat_dmap_entries(
324 let stat_results = stat_dmap_entries(
296 &dmap,
325 &dmap,
297 root_dir,
326 root_dir,
298 check_exec,
327 check_exec,
299 list_clean,
328 list_clean,
300 last_normal_time,
329 last_normal_time,
301 );
330 );
302 results.par_extend(stat_results);
331 results.par_extend(stat_results);
303 }
332 }
304
333
305 build_response(results)
334 build_response(results)
306 }
335 }
General Comments 0
You need to be logged in to leave comments. Login now