##// END OF EJS Templates
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
Simon Sapin -
r48124:69530e5d default
parent child Browse files
Show More
@@ -1,639 +1,773 b''
1 use bytes_cast::BytesCast;
1 use bytes_cast::BytesCast;
2 use micro_timer::timed;
2 use micro_timer::timed;
3 use std::borrow::Cow;
3 use std::borrow::Cow;
4 use std::convert::TryInto;
4 use std::convert::TryInto;
5 use std::path::PathBuf;
5 use std::path::PathBuf;
6
6
7 use super::on_disk;
7 use super::on_disk;
8 use super::path_with_basename::WithBasename;
8 use super::path_with_basename::WithBasename;
9 use crate::dirstate::parsers::pack_entry;
9 use crate::dirstate::parsers::pack_entry;
10 use crate::dirstate::parsers::packed_entry_size;
10 use crate::dirstate::parsers::packed_entry_size;
11 use crate::dirstate::parsers::parse_dirstate_entries;
11 use crate::dirstate::parsers::parse_dirstate_entries;
12 use crate::dirstate::parsers::Timestamp;
12 use crate::dirstate::parsers::Timestamp;
13 use crate::matchers::Matcher;
13 use crate::matchers::Matcher;
14 use crate::utils::hg_path::{HgPath, HgPathBuf};
14 use crate::utils::hg_path::{HgPath, HgPathBuf};
15 use crate::CopyMapIter;
15 use crate::CopyMapIter;
16 use crate::DirstateEntry;
16 use crate::DirstateEntry;
17 use crate::DirstateError;
17 use crate::DirstateError;
18 use crate::DirstateMapError;
18 use crate::DirstateMapError;
19 use crate::DirstateParents;
19 use crate::DirstateParents;
20 use crate::DirstateStatus;
20 use crate::DirstateStatus;
21 use crate::EntryState;
21 use crate::EntryState;
22 use crate::FastHashMap;
22 use crate::FastHashMap;
23 use crate::PatternFileWarning;
23 use crate::PatternFileWarning;
24 use crate::StateMapIter;
24 use crate::StateMapIter;
25 use crate::StatusError;
25 use crate::StatusError;
26 use crate::StatusOptions;
26 use crate::StatusOptions;
27
27
28 pub struct DirstateMap<'on_disk> {
28 pub struct DirstateMap<'on_disk> {
29 /// Contents of the `.hg/dirstate` file
29 /// Contents of the `.hg/dirstate` file
30 pub(super) on_disk: &'on_disk [u8],
30 pub(super) on_disk: &'on_disk [u8],
31
31
32 pub(super) root: ChildNodes<'on_disk>,
32 pub(super) root: ChildNodes<'on_disk>,
33
33
34 /// Number of nodes anywhere in the tree that have `.entry.is_some()`.
34 /// Number of nodes anywhere in the tree that have `.entry.is_some()`.
35 pub(super) nodes_with_entry_count: u32,
35 pub(super) nodes_with_entry_count: u32,
36
36
37 /// Number of nodes anywhere in the tree that have
37 /// Number of nodes anywhere in the tree that have
38 /// `.copy_source.is_some()`.
38 /// `.copy_source.is_some()`.
39 pub(super) nodes_with_copy_source_count: u32,
39 pub(super) nodes_with_copy_source_count: u32,
40 }
40 }
41
41
42 /// Using a plain `HgPathBuf` of the full path from the repository root as a
42 /// Using a plain `HgPathBuf` of the full path from the repository root as a
43 /// map key would also work: all paths in a given map have the same parent
43 /// map key would also work: all paths in a given map have the same parent
44 /// path, so comparing full paths gives the same result as comparing base
44 /// path, so comparing full paths gives the same result as comparing base
45 /// names. However `HashMap` would waste time always re-hashing the same
45 /// names. However `HashMap` would waste time always re-hashing the same
46 /// string prefix.
46 /// string prefix.
47 pub(super) type NodeKey<'on_disk> = WithBasename<Cow<'on_disk, HgPath>>;
47 pub(super) type NodeKey<'on_disk> = WithBasename<Cow<'on_disk, HgPath>>;
48 pub(super) type ChildNodes<'on_disk> =
48
49 FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>;
49 pub(super) enum ChildNodes<'on_disk> {
50 InMemory(FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
51 }
52
53 pub(super) enum ChildNodesRef<'tree, 'on_disk> {
54 InMemory(&'tree FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
55 }
56
57 pub(super) enum NodeRef<'tree, 'on_disk> {
58 InMemory(&'tree NodeKey<'on_disk>, &'tree Node<'on_disk>),
59 }
60
61 impl Default for ChildNodes<'_> {
62 fn default() -> Self {
63 ChildNodes::InMemory(Default::default())
64 }
65 }
66
67 impl<'on_disk> ChildNodes<'on_disk> {
68 pub(super) fn as_ref<'tree>(
69 &'tree self,
70 ) -> ChildNodesRef<'tree, 'on_disk> {
71 match self {
72 ChildNodes::InMemory(nodes) => ChildNodesRef::InMemory(nodes),
73 }
74 }
75
76 pub(super) fn is_empty(&self) -> bool {
77 match self {
78 ChildNodes::InMemory(nodes) => nodes.is_empty(),
79 }
80 }
81
82 pub(super) fn make_mut(
83 &mut self,
84 ) -> &mut FastHashMap<NodeKey<'on_disk>, Node<'on_disk>> {
85 match self {
86 ChildNodes::InMemory(nodes) => nodes,
87 }
88 }
89 }
90
91 impl<'tree, 'on_disk> ChildNodesRef<'tree, 'on_disk> {
92 pub(super) fn get(
93 &self,
94 base_name: &HgPath,
95 ) -> Option<NodeRef<'tree, 'on_disk>> {
96 match self {
97 ChildNodesRef::InMemory(nodes) => nodes
98 .get_key_value(base_name)
99 .map(|(k, v)| NodeRef::InMemory(k, v)),
100 }
101 }
102
103 /// Iterate in undefined order
104 pub(super) fn iter(
105 &self,
106 ) -> impl Iterator<Item = NodeRef<'tree, 'on_disk>> {
107 match self {
108 ChildNodesRef::InMemory(nodes) => {
109 nodes.iter().map(|(k, v)| NodeRef::InMemory(k, v))
110 }
111 }
112 }
113
114 /// Iterate in parallel in undefined order
115 pub(super) fn par_iter(
116 &self,
117 ) -> impl rayon::iter::ParallelIterator<Item = NodeRef<'tree, 'on_disk>>
118 {
119 use rayon::prelude::*;
120 match self {
121 ChildNodesRef::InMemory(nodes) => {
122 nodes.par_iter().map(|(k, v)| NodeRef::InMemory(k, v))
123 }
124 }
125 }
126
127 pub(super) fn sorted(&self) -> Vec<NodeRef<'tree, 'on_disk>> {
128 match self {
129 ChildNodesRef::InMemory(nodes) => {
130 let mut vec: Vec<_> = nodes
131 .iter()
132 .map(|(k, v)| NodeRef::InMemory(k, v))
133 .collect();
134 // `sort_unstable_by_key` doesn’t allow keys borrowing from the
135 // value: https://github.com/rust-lang/rust/issues/34162
136 vec.sort_unstable_by(|a, b| a.base_name().cmp(b.base_name()));
137 vec
138 }
139 }
140 }
141 }
142
143 impl<'tree, 'on_disk> NodeRef<'tree, 'on_disk> {
144 pub(super) fn full_path(&self) -> &'tree HgPath {
145 match self {
146 NodeRef::InMemory(path, _node) => path.full_path(),
147 }
148 }
149
150 /// Returns a `Cow` that can borrow 'on_disk but is detached from 'tree
151 pub(super) fn full_path_cow(&self) -> Cow<'on_disk, HgPath> {
152 match self {
153 NodeRef::InMemory(path, _node) => path.full_path().clone(),
154 }
155 }
156
157 pub(super) fn base_name(&self) -> &'tree HgPath {
158 match self {
159 NodeRef::InMemory(path, _node) => path.base_name(),
160 }
161 }
162
163 pub(super) fn children(&self) -> ChildNodesRef<'tree, 'on_disk> {
164 match self {
165 NodeRef::InMemory(_path, node) => node.children.as_ref(),
166 }
167 }
168
169 pub(super) fn copy_source(&self) -> Option<&'tree HgPath> {
170 match self {
171 NodeRef::InMemory(_path, node) => {
172 node.copy_source.as_ref().map(|s| &**s)
173 }
174 }
175 }
176
177 pub(super) fn has_entry(&self) -> bool {
178 match self {
179 NodeRef::InMemory(_path, node) => node.entry.is_some(),
180 }
181 }
182
183 pub(super) fn entry(&self) -> Option<DirstateEntry> {
184 match self {
185 NodeRef::InMemory(_path, node) => node.entry,
186 }
187 }
188 pub(super) fn state(&self) -> Option<EntryState> {
189 match self {
190 NodeRef::InMemory(_path, node) => {
191 node.entry.as_ref().map(|entry| entry.state)
192 }
193 }
194 }
195
196 pub(super) fn tracked_descendants_count(&self) -> u32 {
197 match self {
198 NodeRef::InMemory(_path, node) => node.tracked_descendants_count,
199 }
200 }
201 }
50
202
51 /// Represents a file or a directory
203 /// Represents a file or a directory
52 #[derive(Default)]
204 #[derive(Default)]
53 pub(super) struct Node<'on_disk> {
205 pub(super) struct Node<'on_disk> {
54 /// `None` for directories
206 /// `None` for directories
55 pub(super) entry: Option<DirstateEntry>,
207 pub(super) entry: Option<DirstateEntry>,
56
208
57 pub(super) copy_source: Option<Cow<'on_disk, HgPath>>,
209 pub(super) copy_source: Option<Cow<'on_disk, HgPath>>,
58
210
59 pub(super) children: ChildNodes<'on_disk>,
211 pub(super) children: ChildNodes<'on_disk>,
60
212
61 /// How many (non-inclusive) descendants of this node are tracked files
213 /// How many (non-inclusive) descendants of this node are tracked files
62 pub(super) tracked_descendants_count: u32,
214 pub(super) tracked_descendants_count: u32,
63 }
215 }
64
216
65 impl<'on_disk> Node<'on_disk> {
66 pub(super) fn state(&self) -> Option<EntryState> {
67 self.entry.as_ref().map(|entry| entry.state)
68 }
69
70 pub(super) fn sorted<'tree>(
71 nodes: &'tree ChildNodes<'on_disk>,
72 ) -> Vec<(&'tree NodeKey<'on_disk>, &'tree Self)> {
73 let mut vec: Vec<_> = nodes.iter().collect();
74 // `sort_unstable_by_key` doesn’t allow keys borrowing from the value:
75 // https://github.com/rust-lang/rust/issues/34162
76 vec.sort_unstable_by(|(path1, _), (path2, _)| path1.cmp(path2));
77 vec
78 }
79 }
80
81 impl<'on_disk> DirstateMap<'on_disk> {
217 impl<'on_disk> DirstateMap<'on_disk> {
82 pub(super) fn empty(on_disk: &'on_disk [u8]) -> Self {
218 pub(super) fn empty(on_disk: &'on_disk [u8]) -> Self {
83 Self {
219 Self {
84 on_disk,
220 on_disk,
85 root: ChildNodes::default(),
221 root: ChildNodes::default(),
86 nodes_with_entry_count: 0,
222 nodes_with_entry_count: 0,
87 nodes_with_copy_source_count: 0,
223 nodes_with_copy_source_count: 0,
88 }
224 }
89 }
225 }
90
226
91 #[timed]
227 #[timed]
92 pub fn new_v2(
228 pub fn new_v2(
93 on_disk: &'on_disk [u8],
229 on_disk: &'on_disk [u8],
94 ) -> Result<(Self, Option<DirstateParents>), DirstateError> {
230 ) -> Result<(Self, Option<DirstateParents>), DirstateError> {
95 on_disk::read(on_disk)
231 on_disk::read(on_disk)
96 }
232 }
97
233
98 #[timed]
234 #[timed]
99 pub fn new_v1(
235 pub fn new_v1(
100 on_disk: &'on_disk [u8],
236 on_disk: &'on_disk [u8],
101 ) -> Result<(Self, Option<DirstateParents>), DirstateError> {
237 ) -> Result<(Self, Option<DirstateParents>), DirstateError> {
102 let mut map = Self::empty(on_disk);
238 let mut map = Self::empty(on_disk);
103 if map.on_disk.is_empty() {
239 if map.on_disk.is_empty() {
104 return Ok((map, None));
240 return Ok((map, None));
105 }
241 }
106
242
107 let parents = parse_dirstate_entries(
243 let parents = parse_dirstate_entries(
108 map.on_disk,
244 map.on_disk,
109 |path, entry, copy_source| {
245 |path, entry, copy_source| {
110 let tracked = entry.state.is_tracked();
246 let tracked = entry.state.is_tracked();
111 let node = Self::get_or_insert_node(
247 let node = Self::get_or_insert_node(
112 &mut map.root,
248 &mut map.root,
113 path,
249 path,
114 WithBasename::to_cow_borrowed,
250 WithBasename::to_cow_borrowed,
115 |ancestor| {
251 |ancestor| {
116 if tracked {
252 if tracked {
117 ancestor.tracked_descendants_count += 1
253 ancestor.tracked_descendants_count += 1
118 }
254 }
119 },
255 },
120 );
256 );
121 assert!(
257 assert!(
122 node.entry.is_none(),
258 node.entry.is_none(),
123 "duplicate dirstate entry in read"
259 "duplicate dirstate entry in read"
124 );
260 );
125 assert!(
261 assert!(
126 node.copy_source.is_none(),
262 node.copy_source.is_none(),
127 "duplicate dirstate entry in read"
263 "duplicate dirstate entry in read"
128 );
264 );
129 node.entry = Some(*entry);
265 node.entry = Some(*entry);
130 node.copy_source = copy_source.map(Cow::Borrowed);
266 node.copy_source = copy_source.map(Cow::Borrowed);
131 map.nodes_with_entry_count += 1;
267 map.nodes_with_entry_count += 1;
132 if copy_source.is_some() {
268 if copy_source.is_some() {
133 map.nodes_with_copy_source_count += 1
269 map.nodes_with_copy_source_count += 1
134 }
270 }
135 },
271 },
136 )?;
272 )?;
137 let parents = Some(parents.clone());
273 let parents = Some(parents.clone());
138
274
139 Ok((map, parents))
275 Ok((map, parents))
140 }
276 }
141
277
142 fn get_node(&self, path: &HgPath) -> Option<&Node> {
278 fn get_node<'tree>(
143 let mut children = &self.root;
279 &'tree self,
280 path: &HgPath,
281 ) -> Option<NodeRef<'tree, 'on_disk>> {
282 let mut children = self.root.as_ref();
144 let mut components = path.components();
283 let mut components = path.components();
145 let mut component =
284 let mut component =
146 components.next().expect("expected at least one components");
285 components.next().expect("expected at least one components");
147 loop {
286 loop {
148 let child = children.get(component)?;
287 let child = children.get(component)?;
149 if let Some(next_component) = components.next() {
288 if let Some(next_component) = components.next() {
150 component = next_component;
289 component = next_component;
151 children = &child.children;
290 children = child.children();
152 } else {
291 } else {
153 return Some(child);
292 return Some(child);
154 }
293 }
155 }
294 }
156 }
295 }
157
296
158 /// Returns a mutable reference to the node at `path` if it exists
297 /// Returns a mutable reference to the node at `path` if it exists
159 ///
298 ///
160 /// This takes `root` instead of `&mut self` so that callers can mutate
299 /// This takes `root` instead of `&mut self` so that callers can mutate
161 /// other fields while the returned borrow is still valid
300 /// other fields while the returned borrow is still valid
162 fn get_node_mut<'tree>(
301 fn get_node_mut<'tree>(
163 root: &'tree mut ChildNodes<'on_disk>,
302 root: &'tree mut ChildNodes<'on_disk>,
164 path: &HgPath,
303 path: &HgPath,
165 ) -> Option<&'tree mut Node<'on_disk>> {
304 ) -> Option<&'tree mut Node<'on_disk>> {
166 let mut children = root;
305 let mut children = root;
167 let mut components = path.components();
306 let mut components = path.components();
168 let mut component =
307 let mut component =
169 components.next().expect("expected at least one components");
308 components.next().expect("expected at least one components");
170 loop {
309 loop {
171 let child = children.get_mut(component)?;
310 let child = children.make_mut().get_mut(component)?;
172 if let Some(next_component) = components.next() {
311 if let Some(next_component) = components.next() {
173 component = next_component;
312 component = next_component;
174 children = &mut child.children;
313 children = &mut child.children;
175 } else {
314 } else {
176 return Some(child);
315 return Some(child);
177 }
316 }
178 }
317 }
179 }
318 }
180
319
181 fn get_or_insert_node<'tree, 'path>(
320 fn get_or_insert_node<'tree, 'path>(
182 root: &'tree mut ChildNodes<'on_disk>,
321 root: &'tree mut ChildNodes<'on_disk>,
183 path: &'path HgPath,
322 path: &'path HgPath,
184 to_cow: impl Fn(
323 to_cow: impl Fn(
185 WithBasename<&'path HgPath>,
324 WithBasename<&'path HgPath>,
186 ) -> WithBasename<Cow<'on_disk, HgPath>>,
325 ) -> WithBasename<Cow<'on_disk, HgPath>>,
187 mut each_ancestor: impl FnMut(&mut Node),
326 mut each_ancestor: impl FnMut(&mut Node),
188 ) -> &'tree mut Node<'on_disk> {
327 ) -> &'tree mut Node<'on_disk> {
189 let mut child_nodes = root;
328 let mut child_nodes = root;
190 let mut inclusive_ancestor_paths =
329 let mut inclusive_ancestor_paths =
191 WithBasename::inclusive_ancestors_of(path);
330 WithBasename::inclusive_ancestors_of(path);
192 let mut ancestor_path = inclusive_ancestor_paths
331 let mut ancestor_path = inclusive_ancestor_paths
193 .next()
332 .next()
194 .expect("expected at least one inclusive ancestor");
333 .expect("expected at least one inclusive ancestor");
195 loop {
334 loop {
196 // TODO: can we avoid allocating an owned key in cases where the
335 // TODO: can we avoid allocating an owned key in cases where the
197 // map already contains that key, without introducing double
336 // map already contains that key, without introducing double
198 // lookup?
337 // lookup?
199 let child_node =
338 let child_node = child_nodes
200 child_nodes.entry(to_cow(ancestor_path)).or_default();
339 .make_mut()
340 .entry(to_cow(ancestor_path))
341 .or_default();
201 if let Some(next) = inclusive_ancestor_paths.next() {
342 if let Some(next) = inclusive_ancestor_paths.next() {
202 each_ancestor(child_node);
343 each_ancestor(child_node);
203 ancestor_path = next;
344 ancestor_path = next;
204 child_nodes = &mut child_node.children;
345 child_nodes = &mut child_node.children;
205 } else {
346 } else {
206 return child_node;
347 return child_node;
207 }
348 }
208 }
349 }
209 }
350 }
210
351
211 fn add_or_remove_file(
352 fn add_or_remove_file(
212 &mut self,
353 &mut self,
213 path: &HgPath,
354 path: &HgPath,
214 old_state: EntryState,
355 old_state: EntryState,
215 new_entry: DirstateEntry,
356 new_entry: DirstateEntry,
216 ) {
357 ) {
217 let tracked_count_increment =
358 let tracked_count_increment =
218 match (old_state.is_tracked(), new_entry.state.is_tracked()) {
359 match (old_state.is_tracked(), new_entry.state.is_tracked()) {
219 (false, true) => 1,
360 (false, true) => 1,
220 (true, false) => -1,
361 (true, false) => -1,
221 _ => 0,
362 _ => 0,
222 };
363 };
223
364
224 let node = Self::get_or_insert_node(
365 let node = Self::get_or_insert_node(
225 &mut self.root,
366 &mut self.root,
226 path,
367 path,
227 WithBasename::to_cow_owned,
368 WithBasename::to_cow_owned,
228 |ancestor| {
369 |ancestor| {
229 // We can’t use `+= increment` because the counter is unsigned,
370 // We can’t use `+= increment` because the counter is unsigned,
230 // and we want debug builds to detect accidental underflow
371 // and we want debug builds to detect accidental underflow
231 // through zero
372 // through zero
232 match tracked_count_increment {
373 match tracked_count_increment {
233 1 => ancestor.tracked_descendants_count += 1,
374 1 => ancestor.tracked_descendants_count += 1,
234 -1 => ancestor.tracked_descendants_count -= 1,
375 -1 => ancestor.tracked_descendants_count -= 1,
235 _ => {}
376 _ => {}
236 }
377 }
237 },
378 },
238 );
379 );
239 if node.entry.is_none() {
380 if node.entry.is_none() {
240 self.nodes_with_entry_count += 1
381 self.nodes_with_entry_count += 1
241 }
382 }
242 node.entry = Some(new_entry)
383 node.entry = Some(new_entry)
243 }
384 }
244
385
245 fn iter_nodes<'a>(
386 fn iter_nodes<'tree>(
246 &'a self,
387 &'tree self,
247 ) -> impl Iterator<Item = (&'a Cow<'on_disk, HgPath>, &'a Node)> + 'a {
388 ) -> impl Iterator<Item = NodeRef<'tree, 'on_disk>> + 'tree {
248 // Depth first tree traversal.
389 // Depth first tree traversal.
249 //
390 //
250 // If we could afford internal iteration and recursion,
391 // If we could afford internal iteration and recursion,
251 // this would look like:
392 // this would look like:
252 //
393 //
253 // ```
394 // ```
254 // fn traverse_children(
395 // fn traverse_children(
255 // children: &ChildNodes,
396 // children: &ChildNodes,
256 // each: &mut impl FnMut(&Node),
397 // each: &mut impl FnMut(&Node),
257 // ) {
398 // ) {
258 // for child in children.values() {
399 // for child in children.values() {
259 // traverse_children(&child.children, each);
400 // traverse_children(&child.children, each);
260 // each(child);
401 // each(child);
261 // }
402 // }
262 // }
403 // }
263 // ```
404 // ```
264 //
405 //
265 // However we want an external iterator and therefore can’t use the
406 // However we want an external iterator and therefore can’t use the
266 // call stack. Use an explicit stack instead:
407 // call stack. Use an explicit stack instead:
267 let mut stack = Vec::new();
408 let mut stack = Vec::new();
268 let mut iter = self.root.iter();
409 let mut iter = self.root.as_ref().iter();
269 std::iter::from_fn(move || {
410 std::iter::from_fn(move || {
270 while let Some((key, child_node)) = iter.next() {
411 while let Some(child_node) = iter.next() {
271 // Pseudo-recursion
412 // Pseudo-recursion
272 let new_iter = child_node.children.iter();
413 let new_iter = child_node.children().iter();
273 let old_iter = std::mem::replace(&mut iter, new_iter);
414 let old_iter = std::mem::replace(&mut iter, new_iter);
274 let key = key.full_path();
415 stack.push((child_node, old_iter));
275 stack.push((key, child_node, old_iter));
276 }
416 }
277 // Found the end of a `children.iter()` iterator.
417 // Found the end of a `children.iter()` iterator.
278 if let Some((key, child_node, next_iter)) = stack.pop() {
418 if let Some((child_node, next_iter)) = stack.pop() {
279 // "Return" from pseudo-recursion by restoring state from the
419 // "Return" from pseudo-recursion by restoring state from the
280 // explicit stack
420 // explicit stack
281 iter = next_iter;
421 iter = next_iter;
282
422
283 Some((key, child_node))
423 Some(child_node)
284 } else {
424 } else {
285 // Reached the bottom of the stack, we’re done
425 // Reached the bottom of the stack, we’re done
286 None
426 None
287 }
427 }
288 })
428 })
289 }
429 }
290
430
291 fn clear_known_ambiguous_mtimes(&mut self, paths: &[impl AsRef<HgPath>]) {
431 fn clear_known_ambiguous_mtimes(&mut self, paths: &[impl AsRef<HgPath>]) {
292 for path in paths {
432 for path in paths {
293 if let Some(node) =
433 if let Some(node) =
294 Self::get_node_mut(&mut self.root, path.as_ref())
434 Self::get_node_mut(&mut self.root, path.as_ref())
295 {
435 {
296 if let Some(entry) = node.entry.as_mut() {
436 if let Some(entry) = node.entry.as_mut() {
297 entry.clear_mtime();
437 entry.clear_mtime();
298 }
438 }
299 }
439 }
300 }
440 }
301 }
441 }
302 }
442 }
303
443
304 impl<'on_disk> super::dispatch::DirstateMapMethods for DirstateMap<'on_disk> {
444 impl<'on_disk> super::dispatch::DirstateMapMethods for DirstateMap<'on_disk> {
305 fn clear(&mut self) {
445 fn clear(&mut self) {
306 self.root.clear();
446 self.root = Default::default();
307 self.nodes_with_entry_count = 0;
447 self.nodes_with_entry_count = 0;
308 self.nodes_with_copy_source_count = 0;
448 self.nodes_with_copy_source_count = 0;
309 }
449 }
310
450
311 fn add_file(
451 fn add_file(
312 &mut self,
452 &mut self,
313 filename: &HgPath,
453 filename: &HgPath,
314 old_state: EntryState,
454 old_state: EntryState,
315 entry: DirstateEntry,
455 entry: DirstateEntry,
316 ) -> Result<(), DirstateMapError> {
456 ) -> Result<(), DirstateMapError> {
317 self.add_or_remove_file(filename, old_state, entry);
457 self.add_or_remove_file(filename, old_state, entry);
318 Ok(())
458 Ok(())
319 }
459 }
320
460
321 fn remove_file(
461 fn remove_file(
322 &mut self,
462 &mut self,
323 filename: &HgPath,
463 filename: &HgPath,
324 old_state: EntryState,
464 old_state: EntryState,
325 size: i32,
465 size: i32,
326 ) -> Result<(), DirstateMapError> {
466 ) -> Result<(), DirstateMapError> {
327 let entry = DirstateEntry {
467 let entry = DirstateEntry {
328 state: EntryState::Removed,
468 state: EntryState::Removed,
329 mode: 0,
469 mode: 0,
330 size,
470 size,
331 mtime: 0,
471 mtime: 0,
332 };
472 };
333 self.add_or_remove_file(filename, old_state, entry);
473 self.add_or_remove_file(filename, old_state, entry);
334 Ok(())
474 Ok(())
335 }
475 }
336
476
337 fn drop_file(
477 fn drop_file(
338 &mut self,
478 &mut self,
339 filename: &HgPath,
479 filename: &HgPath,
340 old_state: EntryState,
480 old_state: EntryState,
341 ) -> Result<bool, DirstateMapError> {
481 ) -> Result<bool, DirstateMapError> {
342 struct Dropped {
482 struct Dropped {
343 was_tracked: bool,
483 was_tracked: bool,
344 had_entry: bool,
484 had_entry: bool,
345 had_copy_source: bool,
485 had_copy_source: bool,
346 }
486 }
347 fn recur(nodes: &mut ChildNodes, path: &HgPath) -> Option<Dropped> {
487 fn recur(nodes: &mut ChildNodes, path: &HgPath) -> Option<Dropped> {
348 let (first_path_component, rest_of_path) =
488 let (first_path_component, rest_of_path) =
349 path.split_first_component();
489 path.split_first_component();
350 let node = nodes.get_mut(first_path_component)?;
490 let node = nodes.make_mut().get_mut(first_path_component)?;
351 let dropped;
491 let dropped;
352 if let Some(rest) = rest_of_path {
492 if let Some(rest) = rest_of_path {
353 dropped = recur(&mut node.children, rest)?;
493 dropped = recur(&mut node.children, rest)?;
354 if dropped.was_tracked {
494 if dropped.was_tracked {
355 node.tracked_descendants_count -= 1;
495 node.tracked_descendants_count -= 1;
356 }
496 }
357 } else {
497 } else {
358 dropped = Dropped {
498 dropped = Dropped {
359 was_tracked: node
499 was_tracked: node
360 .entry
500 .entry
361 .as_ref()
501 .as_ref()
362 .map_or(false, |entry| entry.state.is_tracked()),
502 .map_or(false, |entry| entry.state.is_tracked()),
363 had_entry: node.entry.take().is_some(),
503 had_entry: node.entry.take().is_some(),
364 had_copy_source: node.copy_source.take().is_some(),
504 had_copy_source: node.copy_source.take().is_some(),
365 };
505 };
366 }
506 }
367 // After recursion, for both leaf (rest_of_path is None) nodes and
507 // After recursion, for both leaf (rest_of_path is None) nodes and
368 // parent nodes, remove a node if it just became empty.
508 // parent nodes, remove a node if it just became empty.
369 if node.entry.is_none()
509 if node.entry.is_none()
370 && node.copy_source.is_none()
510 && node.copy_source.is_none()
371 && node.children.is_empty()
511 && node.children.is_empty()
372 {
512 {
373 nodes.remove(first_path_component);
513 nodes.make_mut().remove(first_path_component);
374 }
514 }
375 Some(dropped)
515 Some(dropped)
376 }
516 }
377
517
378 if let Some(dropped) = recur(&mut self.root, filename) {
518 if let Some(dropped) = recur(&mut self.root, filename) {
379 if dropped.had_entry {
519 if dropped.had_entry {
380 self.nodes_with_entry_count -= 1
520 self.nodes_with_entry_count -= 1
381 }
521 }
382 if dropped.had_copy_source {
522 if dropped.had_copy_source {
383 self.nodes_with_copy_source_count -= 1
523 self.nodes_with_copy_source_count -= 1
384 }
524 }
385 Ok(dropped.had_entry)
525 Ok(dropped.had_entry)
386 } else {
526 } else {
387 debug_assert!(!old_state.is_tracked());
527 debug_assert!(!old_state.is_tracked());
388 Ok(false)
528 Ok(false)
389 }
529 }
390 }
530 }
391
531
392 fn clear_ambiguous_times(&mut self, filenames: Vec<HgPathBuf>, now: i32) {
532 fn clear_ambiguous_times(&mut self, filenames: Vec<HgPathBuf>, now: i32) {
393 for filename in filenames {
533 for filename in filenames {
394 if let Some(node) = Self::get_node_mut(&mut self.root, &filename) {
534 if let Some(node) = Self::get_node_mut(&mut self.root, &filename) {
395 if let Some(entry) = node.entry.as_mut() {
535 if let Some(entry) = node.entry.as_mut() {
396 entry.clear_ambiguous_mtime(now);
536 entry.clear_ambiguous_mtime(now);
397 }
537 }
398 }
538 }
399 }
539 }
400 }
540 }
401
541
402 fn non_normal_entries_contains(&mut self, key: &HgPath) -> bool {
542 fn non_normal_entries_contains(&mut self, key: &HgPath) -> bool {
403 self.get_node(key)
543 self.get_node(key)
404 .and_then(|node| node.entry.as_ref())
544 .and_then(|node| node.entry())
405 .map_or(false, DirstateEntry::is_non_normal)
545 .map_or(false, |entry| entry.is_non_normal())
406 }
546 }
407
547
408 fn non_normal_entries_remove(&mut self, _key: &HgPath) {
548 fn non_normal_entries_remove(&mut self, _key: &HgPath) {
409 // Do nothing, this `DirstateMap` does not have a separate "non normal
549 // Do nothing, this `DirstateMap` does not have a separate "non normal
410 // entries" set that need to be kept up to date
550 // entries" set that need to be kept up to date
411 }
551 }
412
552
413 fn non_normal_or_other_parent_paths(
553 fn non_normal_or_other_parent_paths(
414 &mut self,
554 &mut self,
415 ) -> Box<dyn Iterator<Item = &HgPath> + '_> {
555 ) -> Box<dyn Iterator<Item = &HgPath> + '_> {
416 Box::new(self.iter_nodes().filter_map(|(path, node)| {
556 Box::new(self.iter_nodes().filter_map(|node| {
417 node.entry
557 node.entry()
418 .as_ref()
419 .filter(|entry| {
558 .filter(|entry| {
420 entry.is_non_normal() || entry.is_from_other_parent()
559 entry.is_non_normal() || entry.is_from_other_parent()
421 })
560 })
422 .map(|_| &**path)
561 .map(|_| node.full_path())
423 }))
562 }))
424 }
563 }
425
564
426 fn set_non_normal_other_parent_entries(&mut self, _force: bool) {
565 fn set_non_normal_other_parent_entries(&mut self, _force: bool) {
427 // Do nothing, this `DirstateMap` does not have a separate "non normal
566 // Do nothing, this `DirstateMap` does not have a separate "non normal
428 // entries" and "from other parent" sets that need to be recomputed
567 // entries" and "from other parent" sets that need to be recomputed
429 }
568 }
430
569
431 fn iter_non_normal_paths(
570 fn iter_non_normal_paths(
432 &mut self,
571 &mut self,
433 ) -> Box<dyn Iterator<Item = &HgPath> + Send + '_> {
572 ) -> Box<dyn Iterator<Item = &HgPath> + Send + '_> {
434 self.iter_non_normal_paths_panic()
573 self.iter_non_normal_paths_panic()
435 }
574 }
436
575
437 fn iter_non_normal_paths_panic(
576 fn iter_non_normal_paths_panic(
438 &self,
577 &self,
439 ) -> Box<dyn Iterator<Item = &HgPath> + Send + '_> {
578 ) -> Box<dyn Iterator<Item = &HgPath> + Send + '_> {
440 Box::new(self.iter_nodes().filter_map(|(path, node)| {
579 Box::new(self.iter_nodes().filter_map(|node| {
441 node.entry
580 node.entry()
442 .as_ref()
443 .filter(|entry| entry.is_non_normal())
581 .filter(|entry| entry.is_non_normal())
444 .map(|_| &**path)
582 .map(|_| node.full_path())
445 }))
583 }))
446 }
584 }
447
585
448 fn iter_other_parent_paths(
586 fn iter_other_parent_paths(
449 &mut self,
587 &mut self,
450 ) -> Box<dyn Iterator<Item = &HgPath> + Send + '_> {
588 ) -> Box<dyn Iterator<Item = &HgPath> + Send + '_> {
451 Box::new(self.iter_nodes().filter_map(|(path, node)| {
589 Box::new(self.iter_nodes().filter_map(|node| {
452 node.entry
590 node.entry()
453 .as_ref()
454 .filter(|entry| entry.is_from_other_parent())
591 .filter(|entry| entry.is_from_other_parent())
455 .map(|_| &**path)
592 .map(|_| node.full_path())
456 }))
593 }))
457 }
594 }
458
595
459 fn has_tracked_dir(
596 fn has_tracked_dir(
460 &mut self,
597 &mut self,
461 directory: &HgPath,
598 directory: &HgPath,
462 ) -> Result<bool, DirstateMapError> {
599 ) -> Result<bool, DirstateMapError> {
463 if let Some(node) = self.get_node(directory) {
600 if let Some(node) = self.get_node(directory) {
464 // A node without a `DirstateEntry` was created to hold child
601 // A node without a `DirstateEntry` was created to hold child
465 // nodes, and is therefore a directory.
602 // nodes, and is therefore a directory.
466 Ok(node.entry.is_none() && node.tracked_descendants_count > 0)
603 Ok(!node.has_entry() && node.tracked_descendants_count() > 0)
467 } else {
604 } else {
468 Ok(false)
605 Ok(false)
469 }
606 }
470 }
607 }
471
608
472 fn has_dir(
609 fn has_dir(
473 &mut self,
610 &mut self,
474 directory: &HgPath,
611 directory: &HgPath,
475 ) -> Result<bool, DirstateMapError> {
612 ) -> Result<bool, DirstateMapError> {
476 if let Some(node) = self.get_node(directory) {
613 if let Some(node) = self.get_node(directory) {
477 // A node without a `DirstateEntry` was created to hold child
614 // A node without a `DirstateEntry` was created to hold child
478 // nodes, and is therefore a directory.
615 // nodes, and is therefore a directory.
479 Ok(node.entry.is_none())
616 Ok(!node.has_entry())
480 } else {
617 } else {
481 Ok(false)
618 Ok(false)
482 }
619 }
483 }
620 }
484
621
485 #[timed]
622 #[timed]
486 fn pack_v1(
623 fn pack_v1(
487 &mut self,
624 &mut self,
488 parents: DirstateParents,
625 parents: DirstateParents,
489 now: Timestamp,
626 now: Timestamp,
490 ) -> Result<Vec<u8>, DirstateError> {
627 ) -> Result<Vec<u8>, DirstateError> {
491 let now: i32 = now.0.try_into().expect("time overflow");
628 let now: i32 = now.0.try_into().expect("time overflow");
492 let mut ambiguous_mtimes = Vec::new();
629 let mut ambiguous_mtimes = Vec::new();
493 // Optizimation (to be measured?): pre-compute size to avoid `Vec`
630 // Optizimation (to be measured?): pre-compute size to avoid `Vec`
494 // reallocations
631 // reallocations
495 let mut size = parents.as_bytes().len();
632 let mut size = parents.as_bytes().len();
496 for (path, node) in self.iter_nodes() {
633 for node in self.iter_nodes() {
497 if let Some(entry) = &node.entry {
634 if let Some(entry) = node.entry() {
498 size += packed_entry_size(
635 size +=
499 path,
636 packed_entry_size(node.full_path(), node.copy_source());
500 node.copy_source.as_ref().map(|p| &**p),
501 );
502 if entry.mtime_is_ambiguous(now) {
637 if entry.mtime_is_ambiguous(now) {
503 ambiguous_mtimes.push(path.clone())
638 ambiguous_mtimes.push(node.full_path_cow())
504 }
639 }
505 }
640 }
506 }
641 }
507 self.clear_known_ambiguous_mtimes(&ambiguous_mtimes);
642 self.clear_known_ambiguous_mtimes(&ambiguous_mtimes);
508
643
509 let mut packed = Vec::with_capacity(size);
644 let mut packed = Vec::with_capacity(size);
510 packed.extend(parents.as_bytes());
645 packed.extend(parents.as_bytes());
511
646
512 for (path, node) in self.iter_nodes() {
647 for node in self.iter_nodes() {
513 if let Some(entry) = &node.entry {
648 if let Some(entry) = node.entry() {
514 pack_entry(
649 pack_entry(
515 path,
650 node.full_path(),
516 entry,
651 &entry,
517 node.copy_source.as_ref().map(|p| &**p),
652 node.copy_source(),
518 &mut packed,
653 &mut packed,
519 );
654 );
520 }
655 }
521 }
656 }
522 Ok(packed)
657 Ok(packed)
523 }
658 }
524
659
525 #[timed]
660 #[timed]
526 fn pack_v2(
661 fn pack_v2(
527 &mut self,
662 &mut self,
528 parents: DirstateParents,
663 parents: DirstateParents,
529 now: Timestamp,
664 now: Timestamp,
530 ) -> Result<Vec<u8>, DirstateError> {
665 ) -> Result<Vec<u8>, DirstateError> {
531 // TODO: how do we want to handle this in 2038?
666 // TODO: how do we want to handle this in 2038?
532 let now: i32 = now.0.try_into().expect("time overflow");
667 let now: i32 = now.0.try_into().expect("time overflow");
533 let mut paths = Vec::new();
668 let mut paths = Vec::new();
534 for (path, node) in self.iter_nodes() {
669 for node in self.iter_nodes() {
535 if let Some(entry) = &node.entry {
670 if let Some(entry) = node.entry() {
536 if entry.mtime_is_ambiguous(now) {
671 if entry.mtime_is_ambiguous(now) {
537 paths.push(path.clone())
672 paths.push(node.full_path_cow())
538 }
673 }
539 }
674 }
540 }
675 }
541 // Borrow of `self` ends here since we collect cloned paths
676 // Borrow of `self` ends here since we collect cloned paths
542
677
543 self.clear_known_ambiguous_mtimes(&paths);
678 self.clear_known_ambiguous_mtimes(&paths);
544
679
545 on_disk::write(self, parents)
680 on_disk::write(self, parents)
546 }
681 }
547
682
548 fn set_all_dirs(&mut self) -> Result<(), DirstateMapError> {
683 fn set_all_dirs(&mut self) -> Result<(), DirstateMapError> {
549 // Do nothing, this `DirstateMap` does not a separate `all_dirs` that
684 // Do nothing, this `DirstateMap` does not a separate `all_dirs` that
550 // needs to be recomputed
685 // needs to be recomputed
551 Ok(())
686 Ok(())
552 }
687 }
553
688
554 fn set_dirs(&mut self) -> Result<(), DirstateMapError> {
689 fn set_dirs(&mut self) -> Result<(), DirstateMapError> {
555 // Do nothing, this `DirstateMap` does not a separate `dirs` that needs
690 // Do nothing, this `DirstateMap` does not a separate `dirs` that needs
556 // to be recomputed
691 // to be recomputed
557 Ok(())
692 Ok(())
558 }
693 }
559
694
560 fn status<'a>(
695 fn status<'a>(
561 &'a mut self,
696 &'a mut self,
562 matcher: &'a (dyn Matcher + Sync),
697 matcher: &'a (dyn Matcher + Sync),
563 root_dir: PathBuf,
698 root_dir: PathBuf,
564 ignore_files: Vec<PathBuf>,
699 ignore_files: Vec<PathBuf>,
565 options: StatusOptions,
700 options: StatusOptions,
566 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>
701 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>
567 {
702 {
568 super::status::status(self, matcher, root_dir, ignore_files, options)
703 super::status::status(self, matcher, root_dir, ignore_files, options)
569 }
704 }
570
705
571 fn copy_map_len(&self) -> usize {
706 fn copy_map_len(&self) -> usize {
572 self.nodes_with_copy_source_count as usize
707 self.nodes_with_copy_source_count as usize
573 }
708 }
574
709
575 fn copy_map_iter(&self) -> CopyMapIter<'_> {
710 fn copy_map_iter(&self) -> CopyMapIter<'_> {
576 Box::new(self.iter_nodes().filter_map(|(path, node)| {
711 Box::new(self.iter_nodes().filter_map(|node| {
577 node.copy_source
712 node.copy_source()
578 .as_ref()
713 .map(|copy_source| (node.full_path(), copy_source))
579 .map(|copy_source| (&**path, &**copy_source))
580 }))
714 }))
581 }
715 }
582
716
583 fn copy_map_contains_key(&self, key: &HgPath) -> bool {
717 fn copy_map_contains_key(&self, key: &HgPath) -> bool {
584 if let Some(node) = self.get_node(key) {
718 if let Some(node) = self.get_node(key) {
585 node.copy_source.is_some()
719 node.copy_source().is_some()
586 } else {
720 } else {
587 false
721 false
588 }
722 }
589 }
723 }
590
724
591 fn copy_map_get(&self, key: &HgPath) -> Option<&HgPath> {
725 fn copy_map_get(&self, key: &HgPath) -> Option<&HgPath> {
592 self.get_node(key)?.copy_source.as_ref().map(|p| &**p)
726 self.get_node(key)?.copy_source()
593 }
727 }
594
728
595 fn copy_map_remove(&mut self, key: &HgPath) -> Option<HgPathBuf> {
729 fn copy_map_remove(&mut self, key: &HgPath) -> Option<HgPathBuf> {
596 let count = &mut self.nodes_with_copy_source_count;
730 let count = &mut self.nodes_with_copy_source_count;
597 Self::get_node_mut(&mut self.root, key).and_then(|node| {
731 Self::get_node_mut(&mut self.root, key).and_then(|node| {
598 if node.copy_source.is_some() {
732 if node.copy_source.is_some() {
599 *count -= 1
733 *count -= 1
600 }
734 }
601 node.copy_source.take().map(Cow::into_owned)
735 node.copy_source.take().map(Cow::into_owned)
602 })
736 })
603 }
737 }
604
738
605 fn copy_map_insert(
739 fn copy_map_insert(
606 &mut self,
740 &mut self,
607 key: HgPathBuf,
741 key: HgPathBuf,
608 value: HgPathBuf,
742 value: HgPathBuf,
609 ) -> Option<HgPathBuf> {
743 ) -> Option<HgPathBuf> {
610 let node = Self::get_or_insert_node(
744 let node = Self::get_or_insert_node(
611 &mut self.root,
745 &mut self.root,
612 &key,
746 &key,
613 WithBasename::to_cow_owned,
747 WithBasename::to_cow_owned,
614 |_ancestor| {},
748 |_ancestor| {},
615 );
749 );
616 if node.copy_source.is_none() {
750 if node.copy_source.is_none() {
617 self.nodes_with_copy_source_count += 1
751 self.nodes_with_copy_source_count += 1
618 }
752 }
619 node.copy_source.replace(value.into()).map(Cow::into_owned)
753 node.copy_source.replace(value.into()).map(Cow::into_owned)
620 }
754 }
621
755
622 fn len(&self) -> usize {
756 fn len(&self) -> usize {
623 self.nodes_with_entry_count as usize
757 self.nodes_with_entry_count as usize
624 }
758 }
625
759
626 fn contains_key(&self, key: &HgPath) -> bool {
760 fn contains_key(&self, key: &HgPath) -> bool {
627 self.get(key).is_some()
761 self.get(key).is_some()
628 }
762 }
629
763
630 fn get(&self, key: &HgPath) -> Option<DirstateEntry> {
764 fn get(&self, key: &HgPath) -> Option<DirstateEntry> {
631 self.get_node(key)?.entry
765 self.get_node(key)?.entry()
632 }
766 }
633
767
634 fn iter(&self) -> StateMapIter<'_> {
768 fn iter(&self) -> StateMapIter<'_> {
635 Box::new(self.iter_nodes().filter_map(|(path, node)| {
769 Box::new(self.iter_nodes().filter_map(|node| {
636 node.entry.map(|entry| (&**path, entry))
770 node.entry().map(|entry| (node.full_path(), entry))
637 }))
771 }))
638 }
772 }
639 }
773 }
@@ -1,326 +1,331 b''
1 //! The "version 2" disk representation of the dirstate
1 //! The "version 2" disk representation of the dirstate
2 //!
2 //!
3 //! # File format
3 //! # File format
4 //!
4 //!
5 //! The file starts with a fixed-sized header, whose layout is defined by the
5 //! The file starts with a fixed-sized header, whose layout is defined by the
6 //! `Header` struct. Its `root` field contains the slice (offset and length) to
6 //! `Header` struct. Its `root` field contains the slice (offset and length) to
7 //! the nodes representing the files and directories at the root of the
7 //! the nodes representing the files and directories at the root of the
8 //! repository. Each node is also fixed-size, defined by the `Node` struct.
8 //! repository. Each node is also fixed-size, defined by the `Node` struct.
9 //! Nodes in turn contain slices to variable-size paths, and to their own child
9 //! Nodes in turn contain slices to variable-size paths, and to their own child
10 //! nodes (if any) for nested files and directories.
10 //! nodes (if any) for nested files and directories.
11
11
12 use crate::dirstate_tree::dirstate_map::{self, DirstateMap};
12 use crate::dirstate_tree::dirstate_map::{self, DirstateMap, NodeRef};
13 use crate::dirstate_tree::path_with_basename::WithBasename;
13 use crate::dirstate_tree::path_with_basename::WithBasename;
14 use crate::errors::HgError;
14 use crate::errors::HgError;
15 use crate::utils::hg_path::HgPath;
15 use crate::utils::hg_path::HgPath;
16 use crate::DirstateEntry;
16 use crate::DirstateEntry;
17 use crate::DirstateError;
17 use crate::DirstateError;
18 use crate::DirstateParents;
18 use crate::DirstateParents;
19 use bytes_cast::unaligned::{I32Be, U32Be, U64Be};
19 use bytes_cast::unaligned::{I32Be, U32Be, U64Be};
20 use bytes_cast::BytesCast;
20 use bytes_cast::BytesCast;
21 use std::borrow::Cow;
21 use std::borrow::Cow;
22 use std::convert::{TryFrom, TryInto};
22 use std::convert::{TryFrom, TryInto};
23
23
24 /// Added at the start of `.hg/dirstate` when the "v2" format is used.
24 /// Added at the start of `.hg/dirstate` when the "v2" format is used.
25 /// This a redundant sanity check more than an actual "magic number" since
25 /// This a redundant sanity check more than an actual "magic number" since
26 /// `.hg/requires` already governs which format should be used.
26 /// `.hg/requires` already governs which format should be used.
27 pub const V2_FORMAT_MARKER: &[u8; 12] = b"dirstate-v2\n";
27 pub const V2_FORMAT_MARKER: &[u8; 12] = b"dirstate-v2\n";
28
28
29 #[derive(BytesCast)]
29 #[derive(BytesCast)]
30 #[repr(C)]
30 #[repr(C)]
31 struct Header {
31 struct Header {
32 marker: [u8; V2_FORMAT_MARKER.len()],
32 marker: [u8; V2_FORMAT_MARKER.len()],
33
33
34 /// `dirstatemap.parents()` in `mercurial/dirstate.py` relies on this
34 /// `dirstatemap.parents()` in `mercurial/dirstate.py` relies on this
35 /// `parents` field being at this offset, immediately after `marker`.
35 /// `parents` field being at this offset, immediately after `marker`.
36 parents: DirstateParents,
36 parents: DirstateParents,
37
37
38 root: ChildNodes,
38 root: ChildNodes,
39 nodes_with_entry_count: Size,
39 nodes_with_entry_count: Size,
40 nodes_with_copy_source_count: Size,
40 nodes_with_copy_source_count: Size,
41 }
41 }
42
42
43 #[derive(BytesCast)]
43 #[derive(BytesCast)]
44 #[repr(C)]
44 #[repr(C)]
45 struct Node {
45 struct Node {
46 full_path: PathSlice,
46 full_path: PathSlice,
47
47
48 /// In bytes from `self.full_path.start`
48 /// In bytes from `self.full_path.start`
49 base_name_start: Size,
49 base_name_start: Size,
50
50
51 copy_source: OptPathSlice,
51 copy_source: OptPathSlice,
52 entry: OptEntry,
52 entry: OptEntry,
53 children: ChildNodes,
53 children: ChildNodes,
54 tracked_descendants_count: Size,
54 tracked_descendants_count: Size,
55 }
55 }
56
56
57 /// Either nothing if `state == b'\0'`, or a dirstate entry like in the v1
57 /// Either nothing if `state == b'\0'`, or a dirstate entry like in the v1
58 /// format
58 /// format
59 #[derive(BytesCast)]
59 #[derive(BytesCast)]
60 #[repr(C)]
60 #[repr(C)]
61 struct OptEntry {
61 struct OptEntry {
62 state: u8,
62 state: u8,
63 mode: I32Be,
63 mode: I32Be,
64 mtime: I32Be,
64 mtime: I32Be,
65 size: I32Be,
65 size: I32Be,
66 }
66 }
67
67
68 /// Counted in bytes from the start of the file
68 /// Counted in bytes from the start of the file
69 ///
69 ///
70 /// NOTE: If we decide to never support `.hg/dirstate` files larger than 4 GiB
70 /// NOTE: If we decide to never support `.hg/dirstate` files larger than 4 GiB
71 /// we could save space by using `U32Be` instead.
71 /// we could save space by using `U32Be` instead.
72 type Offset = U64Be;
72 type Offset = U64Be;
73
73
74 /// Counted in number of items
74 /// Counted in number of items
75 ///
75 ///
76 /// NOTE: not supporting directories with more than 4 billion direct children,
76 /// NOTE: not supporting directories with more than 4 billion direct children,
77 /// or filenames more than 4 GiB.
77 /// or filenames more than 4 GiB.
78 type Size = U32Be;
78 type Size = U32Be;
79
79
80 /// Location of consecutive, fixed-size items.
80 /// Location of consecutive, fixed-size items.
81 ///
81 ///
82 /// An item can be a single byte for paths, or a struct with
82 /// An item can be a single byte for paths, or a struct with
83 /// `derive(BytesCast)`.
83 /// `derive(BytesCast)`.
84 #[derive(BytesCast, Copy, Clone)]
84 #[derive(BytesCast, Copy, Clone)]
85 #[repr(C)]
85 #[repr(C)]
86 struct Slice {
86 struct Slice {
87 start: Offset,
87 start: Offset,
88 len: Size,
88 len: Size,
89 }
89 }
90
90
91 /// A contiguous sequence of `len` times `Node`, representing the child nodes
91 /// A contiguous sequence of `len` times `Node`, representing the child nodes
92 /// of either some other node or of the repository root.
92 /// of either some other node or of the repository root.
93 ///
93 ///
94 /// Always sorted by ascending `full_path`, to allow binary search.
94 /// Always sorted by ascending `full_path`, to allow binary search.
95 /// Since nodes with the same parent nodes also have the same parent path,
95 /// Since nodes with the same parent nodes also have the same parent path,
96 /// only the `base_name`s need to be compared during binary search.
96 /// only the `base_name`s need to be compared during binary search.
97 type ChildNodes = Slice;
97 type ChildNodes = Slice;
98
98
99 /// A `HgPath` of `len` bytes
99 /// A `HgPath` of `len` bytes
100 type PathSlice = Slice;
100 type PathSlice = Slice;
101
101
102 /// Either nothing if `start == 0`, or a `HgPath` of `len` bytes
102 /// Either nothing if `start == 0`, or a `HgPath` of `len` bytes
103 type OptPathSlice = Slice;
103 type OptPathSlice = Slice;
104
104
105 /// Make sure that size-affecting changes are made knowingly
105 /// Make sure that size-affecting changes are made knowingly
106 fn _static_assert_size_of() {
106 fn _static_assert_size_of() {
107 let _ = std::mem::transmute::<Header, [u8; 72]>;
107 let _ = std::mem::transmute::<Header, [u8; 72]>;
108 let _ = std::mem::transmute::<Node, [u8; 57]>;
108 let _ = std::mem::transmute::<Node, [u8; 57]>;
109 }
109 }
110
110
111 pub(super) fn read<'on_disk>(
111 pub(super) fn read<'on_disk>(
112 on_disk: &'on_disk [u8],
112 on_disk: &'on_disk [u8],
113 ) -> Result<(DirstateMap<'on_disk>, Option<DirstateParents>), DirstateError> {
113 ) -> Result<(DirstateMap<'on_disk>, Option<DirstateParents>), DirstateError> {
114 if on_disk.is_empty() {
114 if on_disk.is_empty() {
115 return Ok((DirstateMap::empty(on_disk), None));
115 return Ok((DirstateMap::empty(on_disk), None));
116 }
116 }
117 let (header, _) = Header::from_bytes(on_disk)
117 let (header, _) = Header::from_bytes(on_disk)
118 .map_err(|_| HgError::corrupted("truncated dirstate-v2"))?;
118 .map_err(|_| HgError::corrupted("truncated dirstate-v2"))?;
119 let Header {
119 let Header {
120 marker,
120 marker,
121 parents,
121 parents,
122 root,
122 root,
123 nodes_with_entry_count,
123 nodes_with_entry_count,
124 nodes_with_copy_source_count,
124 nodes_with_copy_source_count,
125 } = header;
125 } = header;
126 if marker != V2_FORMAT_MARKER {
126 if marker != V2_FORMAT_MARKER {
127 return Err(HgError::corrupted("missing dirstated-v2 marker").into());
127 return Err(HgError::corrupted("missing dirstated-v2 marker").into());
128 }
128 }
129 let dirstate_map = DirstateMap {
129 let dirstate_map = DirstateMap {
130 on_disk,
130 on_disk,
131 root: read_nodes(on_disk, *root)?,
131 root: read_nodes(on_disk, *root)?,
132 nodes_with_entry_count: nodes_with_entry_count.get(),
132 nodes_with_entry_count: nodes_with_entry_count.get(),
133 nodes_with_copy_source_count: nodes_with_copy_source_count.get(),
133 nodes_with_copy_source_count: nodes_with_copy_source_count.get(),
134 };
134 };
135 let parents = Some(parents.clone());
135 let parents = Some(parents.clone());
136 Ok((dirstate_map, parents))
136 Ok((dirstate_map, parents))
137 }
137 }
138
138
139 impl Node {
139 impl Node {
140 pub(super) fn path<'on_disk>(
140 pub(super) fn path<'on_disk>(
141 &self,
141 &self,
142 on_disk: &'on_disk [u8],
142 on_disk: &'on_disk [u8],
143 ) -> Result<dirstate_map::NodeKey<'on_disk>, HgError> {
143 ) -> Result<dirstate_map::NodeKey<'on_disk>, HgError> {
144 let full_path = read_hg_path(on_disk, self.full_path)?;
144 let full_path = read_hg_path(on_disk, self.full_path)?;
145 let base_name_start = usize::try_from(self.base_name_start.get())
145 let base_name_start = usize::try_from(self.base_name_start.get())
146 // u32 -> usize, could only panic on a 16-bit CPU
146 // u32 -> usize, could only panic on a 16-bit CPU
147 .expect("dirstate-v2 base_name_start out of bounds");
147 .expect("dirstate-v2 base_name_start out of bounds");
148 if base_name_start < full_path.len() {
148 if base_name_start < full_path.len() {
149 Ok(WithBasename::from_raw_parts(full_path, base_name_start))
149 Ok(WithBasename::from_raw_parts(full_path, base_name_start))
150 } else {
150 } else {
151 Err(HgError::corrupted(
151 Err(HgError::corrupted(
152 "dirstate-v2 base_name_start out of bounds",
152 "dirstate-v2 base_name_start out of bounds",
153 ))
153 ))
154 }
154 }
155 }
155 }
156
156
157 pub(super) fn copy_source<'on_disk>(
157 pub(super) fn copy_source<'on_disk>(
158 &self,
158 &self,
159 on_disk: &'on_disk [u8],
159 on_disk: &'on_disk [u8],
160 ) -> Result<Option<Cow<'on_disk, HgPath>>, HgError> {
160 ) -> Result<Option<Cow<'on_disk, HgPath>>, HgError> {
161 Ok(if self.copy_source.start.get() != 0 {
161 Ok(if self.copy_source.start.get() != 0 {
162 Some(read_hg_path(on_disk, self.copy_source)?)
162 Some(read_hg_path(on_disk, self.copy_source)?)
163 } else {
163 } else {
164 None
164 None
165 })
165 })
166 }
166 }
167
167
168 pub(super) fn entry(&self) -> Result<Option<DirstateEntry>, HgError> {
168 pub(super) fn entry(&self) -> Result<Option<DirstateEntry>, HgError> {
169 Ok(if self.entry.state != b'\0' {
169 Ok(if self.entry.state != b'\0' {
170 Some(DirstateEntry {
170 Some(DirstateEntry {
171 state: self.entry.state.try_into()?,
171 state: self.entry.state.try_into()?,
172 mode: self.entry.mode.get(),
172 mode: self.entry.mode.get(),
173 mtime: self.entry.mtime.get(),
173 mtime: self.entry.mtime.get(),
174 size: self.entry.size.get(),
174 size: self.entry.size.get(),
175 })
175 })
176 } else {
176 } else {
177 None
177 None
178 })
178 })
179 }
179 }
180
180
181 pub(super) fn to_in_memory_node<'on_disk>(
181 pub(super) fn to_in_memory_node<'on_disk>(
182 &self,
182 &self,
183 on_disk: &'on_disk [u8],
183 on_disk: &'on_disk [u8],
184 ) -> Result<dirstate_map::Node<'on_disk>, HgError> {
184 ) -> Result<dirstate_map::Node<'on_disk>, HgError> {
185 Ok(dirstate_map::Node {
185 Ok(dirstate_map::Node {
186 children: read_nodes(on_disk, self.children)?,
186 children: read_nodes(on_disk, self.children)?,
187 copy_source: self.copy_source(on_disk)?,
187 copy_source: self.copy_source(on_disk)?,
188 entry: self.entry()?,
188 entry: self.entry()?,
189 tracked_descendants_count: self.tracked_descendants_count.get(),
189 tracked_descendants_count: self.tracked_descendants_count.get(),
190 })
190 })
191 }
191 }
192 }
192 }
193
193
194 fn read_nodes(
194 fn read_nodes(
195 on_disk: &[u8],
195 on_disk: &[u8],
196 slice: ChildNodes,
196 slice: ChildNodes,
197 ) -> Result<dirstate_map::ChildNodes, HgError> {
197 ) -> Result<dirstate_map::ChildNodes, HgError> {
198 read_slice::<Node>(on_disk, slice)?
198 read_slice::<Node>(on_disk, slice)?
199 .iter()
199 .iter()
200 .map(|node| {
200 .map(|node| {
201 Ok((node.path(on_disk)?, node.to_in_memory_node(on_disk)?))
201 Ok((node.path(on_disk)?, node.to_in_memory_node(on_disk)?))
202 })
202 })
203 .collect()
203 .collect::<Result<_, _>>()
204 .map(dirstate_map::ChildNodes::InMemory)
204 }
205 }
205
206
206 fn read_hg_path(on_disk: &[u8], slice: Slice) -> Result<Cow<HgPath>, HgError> {
207 fn read_hg_path(on_disk: &[u8], slice: Slice) -> Result<Cow<HgPath>, HgError> {
207 let bytes = read_slice::<u8>(on_disk, slice)?;
208 let bytes = read_slice::<u8>(on_disk, slice)?;
208 Ok(Cow::Borrowed(HgPath::new(bytes)))
209 Ok(Cow::Borrowed(HgPath::new(bytes)))
209 }
210 }
210
211
211 fn read_slice<T>(on_disk: &[u8], slice: Slice) -> Result<&[T], HgError>
212 fn read_slice<T>(on_disk: &[u8], slice: Slice) -> Result<&[T], HgError>
212 where
213 where
213 T: BytesCast,
214 T: BytesCast,
214 {
215 {
215 // Either `usize::MAX` would result in "out of bounds" error since a single
216 // Either `usize::MAX` would result in "out of bounds" error since a single
216 // `&[u8]` cannot occupy the entire addess space.
217 // `&[u8]` cannot occupy the entire addess space.
217 let start = usize::try_from(slice.start.get()).unwrap_or(std::usize::MAX);
218 let start = usize::try_from(slice.start.get()).unwrap_or(std::usize::MAX);
218 let len = usize::try_from(slice.len.get()).unwrap_or(std::usize::MAX);
219 let len = usize::try_from(slice.len.get()).unwrap_or(std::usize::MAX);
219 on_disk
220 on_disk
220 .get(start..)
221 .get(start..)
221 .and_then(|bytes| T::slice_from_bytes(bytes, len).ok())
222 .and_then(|bytes| T::slice_from_bytes(bytes, len).ok())
222 .map(|(slice, _rest)| slice)
223 .map(|(slice, _rest)| slice)
223 .ok_or_else(|| {
224 .ok_or_else(|| {
224 HgError::corrupted("dirstate v2 slice is out of bounds")
225 HgError::corrupted("dirstate v2 slice is out of bounds")
225 })
226 })
226 }
227 }
227
228
228 pub(super) fn write(
229 pub(super) fn write(
229 dirstate_map: &mut DirstateMap,
230 dirstate_map: &mut DirstateMap,
230 parents: DirstateParents,
231 parents: DirstateParents,
231 ) -> Result<Vec<u8>, DirstateError> {
232 ) -> Result<Vec<u8>, DirstateError> {
232 let header_len = std::mem::size_of::<Header>();
233 let header_len = std::mem::size_of::<Header>();
233
234
234 // This ignores the space for paths, and for nodes without an entry.
235 // This ignores the space for paths, and for nodes without an entry.
235 // TODO: better estimate? Skip the `Vec` and write to a file directly?
236 // TODO: better estimate? Skip the `Vec` and write to a file directly?
236 let size_guess = header_len
237 let size_guess = header_len
237 + std::mem::size_of::<Node>()
238 + std::mem::size_of::<Node>()
238 * dirstate_map.nodes_with_entry_count as usize;
239 * dirstate_map.nodes_with_entry_count as usize;
239 let mut out = Vec::with_capacity(size_guess);
240 let mut out = Vec::with_capacity(size_guess);
240
241
241 // Keep space for the header. We’ll fill it out at the end when we know the
242 // Keep space for the header. We’ll fill it out at the end when we know the
242 // actual offset for the root nodes.
243 // actual offset for the root nodes.
243 out.resize(header_len, 0_u8);
244 out.resize(header_len, 0_u8);
244
245
245 let root = write_nodes(&mut dirstate_map.root, &mut out)?;
246 let root = write_nodes(dirstate_map.root.as_ref(), &mut out)?;
246
247
247 let header = Header {
248 let header = Header {
248 marker: *V2_FORMAT_MARKER,
249 marker: *V2_FORMAT_MARKER,
249 parents: parents,
250 parents: parents,
250 root,
251 root,
251 nodes_with_entry_count: dirstate_map.nodes_with_entry_count.into(),
252 nodes_with_entry_count: dirstate_map.nodes_with_entry_count.into(),
252 nodes_with_copy_source_count: dirstate_map
253 nodes_with_copy_source_count: dirstate_map
253 .nodes_with_copy_source_count
254 .nodes_with_copy_source_count
254 .into(),
255 .into(),
255 };
256 };
256 out[..header_len].copy_from_slice(header.as_bytes());
257 out[..header_len].copy_from_slice(header.as_bytes());
257 Ok(out)
258 Ok(out)
258 }
259 }
259
260
260 fn write_nodes(
261 fn write_nodes(
261 nodes: &dirstate_map::ChildNodes,
262 nodes: dirstate_map::ChildNodesRef,
262 out: &mut Vec<u8>,
263 out: &mut Vec<u8>,
263 ) -> Result<ChildNodes, DirstateError> {
264 ) -> Result<ChildNodes, DirstateError> {
264 // `dirstate_map::ChildNodes` is a `HashMap` with undefined iteration
265 // `dirstate_map::ChildNodes` is a `HashMap` with undefined iteration
265 // order. Sort to enable binary search in the written file.
266 // order. Sort to enable binary search in the written file.
266 let nodes = dirstate_map::Node::sorted(nodes);
267 let nodes = nodes.sorted();
267
268
268 // First accumulate serialized nodes in a `Vec`
269 // First accumulate serialized nodes in a `Vec`
269 let mut on_disk_nodes = Vec::with_capacity(nodes.len());
270 let mut on_disk_nodes = Vec::with_capacity(nodes.len());
270 for (full_path, node) in nodes {
271 for node in nodes {
271 on_disk_nodes.push(Node {
272 let children = write_nodes(node.children(), out)?;
272 children: write_nodes(&node.children, out)?,
273 let full_path = write_slice::<u8>(node.full_path().as_bytes(), out);
273 tracked_descendants_count: node.tracked_descendants_count.into(),
274 let copy_source = if let Some(source) = node.copy_source() {
274 full_path: write_slice::<u8>(
275 write_slice::<u8>(source.as_bytes(), out)
275 full_path.full_path().as_bytes(),
276 } else {
276 out,
277 Slice {
277 ),
278 start: 0.into(),
278 base_name_start: u32::try_from(full_path.base_name_start())
279 len: 0.into(),
279 // Could only panic for paths over 4 GiB
280 }
280 .expect("dirstate-v2 offset overflow")
281 };
281 .into(),
282 on_disk_nodes.push(match node {
282 copy_source: if let Some(source) = &node.copy_source {
283 NodeRef::InMemory(path, node) => Node {
283 write_slice::<u8>(source.as_bytes(), out)
284 children,
284 } else {
285 copy_source,
285 Slice {
286 full_path,
286 start: 0.into(),
287 base_name_start: u32::try_from(path.base_name_start())
287 len: 0.into(),
288 // Could only panic for paths over 4 GiB
288 }
289 .expect("dirstate-v2 offset overflow")
289 },
290 .into(),
290 entry: if let Some(entry) = &node.entry {
291 tracked_descendants_count: node
291 OptEntry {
292 .tracked_descendants_count
292 state: entry.state.into(),
293 .into(),
293 mode: entry.mode.into(),
294 entry: if let Some(entry) = &node.entry {
294 mtime: entry.mtime.into(),
295 OptEntry {
295 size: entry.size.into(),
296 state: entry.state.into(),
296 }
297 mode: entry.mode.into(),
297 } else {
298 mtime: entry.mtime.into(),
298 OptEntry {
299 size: entry.size.into(),
299 state: b'\0',
300 }
300 mode: 0.into(),
301 } else {
301 mtime: 0.into(),
302 OptEntry {
302 size: 0.into(),
303 state: b'\0',
303 }
304 mode: 0.into(),
305 mtime: 0.into(),
306 size: 0.into(),
307 }
308 },
304 },
309 },
305 })
310 })
306 }
311 }
307 // … so we can write them contiguously
312 // … so we can write them contiguously
308 Ok(write_slice::<Node>(&on_disk_nodes, out))
313 Ok(write_slice::<Node>(&on_disk_nodes, out))
309 }
314 }
310
315
311 fn write_slice<T>(slice: &[T], out: &mut Vec<u8>) -> Slice
316 fn write_slice<T>(slice: &[T], out: &mut Vec<u8>) -> Slice
312 where
317 where
313 T: BytesCast,
318 T: BytesCast,
314 {
319 {
315 let start = u64::try_from(out.len())
320 let start = u64::try_from(out.len())
316 // Could only panic on a 128-bit CPU with a dirstate over 16 EiB
321 // Could only panic on a 128-bit CPU with a dirstate over 16 EiB
317 .expect("dirstate-v2 offset overflow")
322 .expect("dirstate-v2 offset overflow")
318 .into();
323 .into();
319 let len = u32::try_from(slice.len())
324 let len = u32::try_from(slice.len())
320 // Could only panic for paths over 4 GiB or nodes with over 4 billions
325 // Could only panic for paths over 4 GiB or nodes with over 4 billions
321 // child nodes
326 // child nodes
322 .expect("dirstate-v2 offset overflow")
327 .expect("dirstate-v2 offset overflow")
323 .into();
328 .into();
324 out.extend(slice.as_bytes());
329 out.extend(slice.as_bytes());
325 Slice { start, len }
330 Slice { start, len }
326 }
331 }
@@ -1,426 +1,413 b''
1 use crate::dirstate::status::IgnoreFnType;
1 use crate::dirstate::status::IgnoreFnType;
2 use crate::dirstate_tree::dirstate_map::ChildNodes;
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::Node;
4 use crate::dirstate_tree::dirstate_map::NodeRef;
5 use crate::matchers::get_ignore_function;
5 use crate::matchers::get_ignore_function;
6 use crate::matchers::Matcher;
6 use crate::matchers::Matcher;
7 use crate::utils::files::get_bytes_from_os_string;
7 use crate::utils::files::get_bytes_from_os_string;
8 use crate::utils::hg_path::HgPath;
8 use crate::utils::hg_path::HgPath;
9 use crate::BadMatch;
9 use crate::BadMatch;
10 use crate::DirstateStatus;
10 use crate::DirstateStatus;
11 use crate::EntryState;
11 use crate::EntryState;
12 use crate::HgPathBuf;
12 use crate::HgPathBuf;
13 use crate::PatternFileWarning;
13 use crate::PatternFileWarning;
14 use crate::StatusError;
14 use crate::StatusError;
15 use crate::StatusOptions;
15 use crate::StatusOptions;
16 use micro_timer::timed;
16 use micro_timer::timed;
17 use rayon::prelude::*;
17 use rayon::prelude::*;
18 use std::borrow::Cow;
18 use std::borrow::Cow;
19 use std::io;
19 use std::io;
20 use std::path::Path;
20 use std::path::Path;
21 use std::path::PathBuf;
21 use std::path::PathBuf;
22 use std::sync::Mutex;
22 use std::sync::Mutex;
23
23
24 /// Returns the status of the working directory compared to its parent
24 /// Returns the status of the working directory compared to its parent
25 /// changeset.
25 /// changeset.
26 ///
26 ///
27 /// This algorithm is based on traversing the filesystem tree (`fs` in function
27 /// This algorithm is based on traversing the filesystem tree (`fs` in function
28 /// and variable names) and dirstate tree at the same time. The core of this
28 /// and variable names) and dirstate tree at the same time. The core of this
29 /// traversal is the recursive `traverse_fs_directory_and_dirstate` function
29 /// traversal is the recursive `traverse_fs_directory_and_dirstate` function
30 /// and its use of `itertools::merge_join_by`. When reaching a path that only
30 /// and its use of `itertools::merge_join_by`. When reaching a path that only
31 /// exists in one of the two trees, depending on information requested by
31 /// exists in one of the two trees, depending on information requested by
32 /// `options` we may need to traverse the remaining subtree.
32 /// `options` we may need to traverse the remaining subtree.
33 #[timed]
33 #[timed]
34 pub fn status<'tree>(
34 pub fn status<'tree>(
35 dmap: &'tree mut DirstateMap,
35 dmap: &'tree mut DirstateMap,
36 matcher: &(dyn Matcher + Sync),
36 matcher: &(dyn Matcher + Sync),
37 root_dir: PathBuf,
37 root_dir: PathBuf,
38 ignore_files: Vec<PathBuf>,
38 ignore_files: Vec<PathBuf>,
39 options: StatusOptions,
39 options: StatusOptions,
40 ) -> Result<(DirstateStatus<'tree>, Vec<PatternFileWarning>), StatusError> {
40 ) -> Result<(DirstateStatus<'tree>, Vec<PatternFileWarning>), StatusError> {
41 let (ignore_fn, warnings): (IgnoreFnType, _) =
41 let (ignore_fn, warnings): (IgnoreFnType, _) =
42 if options.list_ignored || options.list_unknown {
42 if options.list_ignored || options.list_unknown {
43 get_ignore_function(ignore_files, &root_dir)?
43 get_ignore_function(ignore_files, &root_dir)?
44 } else {
44 } else {
45 (Box::new(|&_| true), vec![])
45 (Box::new(|&_| true), vec![])
46 };
46 };
47
47
48 let common = StatusCommon {
48 let common = StatusCommon {
49 options,
49 options,
50 matcher,
50 matcher,
51 ignore_fn,
51 ignore_fn,
52 outcome: Mutex::new(DirstateStatus::default()),
52 outcome: Mutex::new(DirstateStatus::default()),
53 };
53 };
54 let is_at_repo_root = true;
54 let is_at_repo_root = true;
55 let hg_path = HgPath::new("");
55 let hg_path = HgPath::new("");
56 let has_ignored_ancestor = false;
56 let has_ignored_ancestor = false;
57 common.traverse_fs_directory_and_dirstate(
57 common.traverse_fs_directory_and_dirstate(
58 has_ignored_ancestor,
58 has_ignored_ancestor,
59 &dmap.root,
59 dmap.root.as_ref(),
60 hg_path,
60 hg_path,
61 &root_dir,
61 &root_dir,
62 is_at_repo_root,
62 is_at_repo_root,
63 );
63 );
64 Ok((common.outcome.into_inner().unwrap(), warnings))
64 Ok((common.outcome.into_inner().unwrap(), warnings))
65 }
65 }
66
66
67 /// Bag of random things needed by various parts of the algorithm. Reduces the
67 /// Bag of random things needed by various parts of the algorithm. Reduces the
68 /// number of parameters passed to functions.
68 /// number of parameters passed to functions.
69 struct StatusCommon<'tree, 'a> {
69 struct StatusCommon<'tree, 'a> {
70 options: StatusOptions,
70 options: StatusOptions,
71 matcher: &'a (dyn Matcher + Sync),
71 matcher: &'a (dyn Matcher + Sync),
72 ignore_fn: IgnoreFnType<'a>,
72 ignore_fn: IgnoreFnType<'a>,
73 outcome: Mutex<DirstateStatus<'tree>>,
73 outcome: Mutex<DirstateStatus<'tree>>,
74 }
74 }
75
75
76 impl<'tree, 'a> StatusCommon<'tree, 'a> {
76 impl<'tree, 'a> StatusCommon<'tree, 'a> {
77 fn read_dir(
77 fn read_dir(
78 &self,
78 &self,
79 hg_path: &HgPath,
79 hg_path: &HgPath,
80 fs_path: &Path,
80 fs_path: &Path,
81 is_at_repo_root: bool,
81 is_at_repo_root: bool,
82 ) -> Result<Vec<DirEntry>, ()> {
82 ) -> Result<Vec<DirEntry>, ()> {
83 DirEntry::read_dir(fs_path, is_at_repo_root).map_err(|error| {
83 DirEntry::read_dir(fs_path, is_at_repo_root).map_err(|error| {
84 let errno = error.raw_os_error().expect("expected real OS error");
84 let errno = error.raw_os_error().expect("expected real OS error");
85 self.outcome
85 self.outcome
86 .lock()
86 .lock()
87 .unwrap()
87 .unwrap()
88 .bad
88 .bad
89 .push((hg_path.to_owned().into(), BadMatch::OsError(errno)))
89 .push((hg_path.to_owned().into(), BadMatch::OsError(errno)))
90 })
90 })
91 }
91 }
92
92
93 fn traverse_fs_directory_and_dirstate(
93 fn traverse_fs_directory_and_dirstate(
94 &self,
94 &self,
95 has_ignored_ancestor: bool,
95 has_ignored_ancestor: bool,
96 dirstate_nodes: &'tree ChildNodes,
96 dirstate_nodes: ChildNodesRef<'tree, '_>,
97 directory_hg_path: &'tree HgPath,
97 directory_hg_path: &'tree HgPath,
98 directory_fs_path: &Path,
98 directory_fs_path: &Path,
99 is_at_repo_root: bool,
99 is_at_repo_root: bool,
100 ) {
100 ) {
101 let mut fs_entries = if let Ok(entries) = self.read_dir(
101 let mut fs_entries = if let Ok(entries) = self.read_dir(
102 directory_hg_path,
102 directory_hg_path,
103 directory_fs_path,
103 directory_fs_path,
104 is_at_repo_root,
104 is_at_repo_root,
105 ) {
105 ) {
106 entries
106 entries
107 } else {
107 } else {
108 return;
108 return;
109 };
109 };
110
110
111 // `merge_join_by` requires both its input iterators to be sorted:
111 // `merge_join_by` requires both its input iterators to be sorted:
112
112
113 let dirstate_nodes = Node::sorted(dirstate_nodes);
113 let dirstate_nodes = dirstate_nodes.sorted();
114 // `sort_unstable_by_key` doesn’t allow keys borrowing from the value:
114 // `sort_unstable_by_key` doesn’t allow keys borrowing from the value:
115 // https://github.com/rust-lang/rust/issues/34162
115 // https://github.com/rust-lang/rust/issues/34162
116 fs_entries.sort_unstable_by(|e1, e2| e1.base_name.cmp(&e2.base_name));
116 fs_entries.sort_unstable_by(|e1, e2| e1.base_name.cmp(&e2.base_name));
117
117
118 itertools::merge_join_by(
118 itertools::merge_join_by(
119 dirstate_nodes,
119 dirstate_nodes,
120 &fs_entries,
120 &fs_entries,
121 |(full_path, _node), fs_entry| {
121 |dirstate_node, fs_entry| {
122 full_path.base_name().cmp(&fs_entry.base_name)
122 dirstate_node.base_name().cmp(&fs_entry.base_name)
123 },
123 },
124 )
124 )
125 .par_bridge()
125 .par_bridge()
126 .for_each(|pair| {
126 .for_each(|pair| {
127 use itertools::EitherOrBoth::*;
127 use itertools::EitherOrBoth::*;
128 match pair {
128 match pair {
129 Both((hg_path, dirstate_node), fs_entry) => {
129 Both(dirstate_node, fs_entry) => {
130 self.traverse_fs_and_dirstate(
130 self.traverse_fs_and_dirstate(
131 fs_entry,
131 fs_entry,
132 hg_path.full_path(),
133 dirstate_node,
132 dirstate_node,
134 has_ignored_ancestor,
133 has_ignored_ancestor,
135 );
134 );
136 }
135 }
137 Left((hg_path, dirstate_node)) => self.traverse_dirstate_only(
136 Left(dirstate_node) => {
138 hg_path.full_path(),
137 self.traverse_dirstate_only(dirstate_node)
139 dirstate_node,
138 }
140 ),
141 Right(fs_entry) => self.traverse_fs_only(
139 Right(fs_entry) => self.traverse_fs_only(
142 has_ignored_ancestor,
140 has_ignored_ancestor,
143 directory_hg_path,
141 directory_hg_path,
144 fs_entry,
142 fs_entry,
145 ),
143 ),
146 }
144 }
147 })
145 })
148 }
146 }
149
147
150 fn traverse_fs_and_dirstate(
148 fn traverse_fs_and_dirstate(
151 &self,
149 &self,
152 fs_entry: &DirEntry,
150 fs_entry: &DirEntry,
153 hg_path: &'tree HgPath,
151 dirstate_node: NodeRef<'tree, '_>,
154 dirstate_node: &'tree Node,
155 has_ignored_ancestor: bool,
152 has_ignored_ancestor: bool,
156 ) {
153 ) {
154 let hg_path = dirstate_node.full_path();
157 let file_type = fs_entry.metadata.file_type();
155 let file_type = fs_entry.metadata.file_type();
158 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
156 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
159 if !file_or_symlink {
157 if !file_or_symlink {
160 // If we previously had a file here, it was removed (with
158 // If we previously had a file here, it was removed (with
161 // `hg rm` or similar) or deleted before it could be
159 // `hg rm` or similar) or deleted before it could be
162 // replaced by a directory or something else.
160 // replaced by a directory or something else.
163 self.mark_removed_or_deleted_if_file(
161 self.mark_removed_or_deleted_if_file(
164 hg_path,
162 dirstate_node.full_path(),
165 dirstate_node.state(),
163 dirstate_node.state(),
166 );
164 );
167 }
165 }
168 if file_type.is_dir() {
166 if file_type.is_dir() {
169 if self.options.collect_traversed_dirs {
167 if self.options.collect_traversed_dirs {
170 self.outcome.lock().unwrap().traversed.push(hg_path.into())
168 self.outcome.lock().unwrap().traversed.push(hg_path.into())
171 }
169 }
172 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path);
170 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path);
173 let is_at_repo_root = false;
171 let is_at_repo_root = false;
174 self.traverse_fs_directory_and_dirstate(
172 self.traverse_fs_directory_and_dirstate(
175 is_ignored,
173 is_ignored,
176 &dirstate_node.children,
174 dirstate_node.children(),
177 hg_path,
175 hg_path,
178 &fs_entry.full_path,
176 &fs_entry.full_path,
179 is_at_repo_root,
177 is_at_repo_root,
180 );
178 );
181 } else {
179 } else {
182 if file_or_symlink && self.matcher.matches(hg_path) {
180 if file_or_symlink && self.matcher.matches(hg_path) {
183 let full_path = Cow::from(hg_path);
181 let full_path = Cow::from(hg_path);
184 if let Some(entry) = &dirstate_node.entry {
182 if let Some(state) = dirstate_node.state() {
185 match entry.state {
183 match state {
186 EntryState::Added => {
184 EntryState::Added => {
187 self.outcome.lock().unwrap().added.push(full_path)
185 self.outcome.lock().unwrap().added.push(full_path)
188 }
186 }
189 EntryState::Removed => self
187 EntryState::Removed => self
190 .outcome
188 .outcome
191 .lock()
189 .lock()
192 .unwrap()
190 .unwrap()
193 .removed
191 .removed
194 .push(full_path),
192 .push(full_path),
195 EntryState::Merged => self
193 EntryState::Merged => self
196 .outcome
194 .outcome
197 .lock()
195 .lock()
198 .unwrap()
196 .unwrap()
199 .modified
197 .modified
200 .push(full_path),
198 .push(full_path),
201 EntryState::Normal => {
199 EntryState::Normal => {
202 self.handle_normal_file(
200 self.handle_normal_file(&dirstate_node, fs_entry);
203 full_path,
204 dirstate_node,
205 entry,
206 fs_entry,
207 );
208 }
201 }
209 // This variant is not used in DirstateMap
202 // This variant is not used in DirstateMap
210 // nodes
203 // nodes
211 EntryState::Unknown => unreachable!(),
204 EntryState::Unknown => unreachable!(),
212 }
205 }
213 } else {
206 } else {
214 // `node.entry.is_none()` indicates a "directory"
207 // `node.entry.is_none()` indicates a "directory"
215 // node, but the filesystem has a file
208 // node, but the filesystem has a file
216 self.mark_unknown_or_ignored(
209 self.mark_unknown_or_ignored(
217 has_ignored_ancestor,
210 has_ignored_ancestor,
218 full_path,
211 full_path,
219 )
212 )
220 }
213 }
221 }
214 }
222
215
223 for (child_hg_path, child_node) in &dirstate_node.children {
216 for child_node in dirstate_node.children().iter() {
224 self.traverse_dirstate_only(
217 self.traverse_dirstate_only(child_node)
225 child_hg_path.full_path(),
226 child_node,
227 )
228 }
218 }
229 }
219 }
230 }
220 }
231
221
232 /// A file with `EntryState::Normal` in the dirstate was found in the
222 /// A file with `EntryState::Normal` in the dirstate was found in the
233 /// filesystem
223 /// filesystem
234 fn handle_normal_file(
224 fn handle_normal_file(
235 &self,
225 &self,
236 full_path: Cow<'tree, HgPath>,
226 dirstate_node: &NodeRef<'tree, '_>,
237 dirstate_node: &Node,
238 entry: &crate::DirstateEntry,
239 fs_entry: &DirEntry,
227 fs_entry: &DirEntry,
240 ) {
228 ) {
241 // Keep the low 31 bits
229 // Keep the low 31 bits
242 fn truncate_u64(value: u64) -> i32 {
230 fn truncate_u64(value: u64) -> i32 {
243 (value & 0x7FFF_FFFF) as i32
231 (value & 0x7FFF_FFFF) as i32
244 }
232 }
245 fn truncate_i64(value: i64) -> i32 {
233 fn truncate_i64(value: i64) -> i32 {
246 (value & 0x7FFF_FFFF) as i32
234 (value & 0x7FFF_FFFF) as i32
247 }
235 }
248
236
237 let entry = dirstate_node
238 .entry()
239 .expect("handle_normal_file called with entry-less node");
240 let full_path = Cow::from(dirstate_node.full_path());
249 let mode_changed = || {
241 let mode_changed = || {
250 self.options.check_exec && entry.mode_changed(&fs_entry.metadata)
242 self.options.check_exec && entry.mode_changed(&fs_entry.metadata)
251 };
243 };
252 let size_changed = entry.size != truncate_u64(fs_entry.metadata.len());
244 let size_changed = entry.size != truncate_u64(fs_entry.metadata.len());
253 if entry.size >= 0
245 if entry.size >= 0
254 && size_changed
246 && size_changed
255 && fs_entry.metadata.file_type().is_symlink()
247 && fs_entry.metadata.file_type().is_symlink()
256 {
248 {
257 // issue6456: Size returned may be longer due to encryption
249 // issue6456: Size returned may be longer due to encryption
258 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
250 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
259 self.outcome.lock().unwrap().unsure.push(full_path)
251 self.outcome.lock().unwrap().unsure.push(full_path)
260 } else if dirstate_node.copy_source.is_some()
252 } else if dirstate_node.copy_source().is_some()
261 || entry.is_from_other_parent()
253 || entry.is_from_other_parent()
262 || (entry.size >= 0 && (size_changed || mode_changed()))
254 || (entry.size >= 0 && (size_changed || mode_changed()))
263 {
255 {
264 self.outcome.lock().unwrap().modified.push(full_path)
256 self.outcome.lock().unwrap().modified.push(full_path)
265 } else {
257 } else {
266 let mtime = mtime_seconds(&fs_entry.metadata);
258 let mtime = mtime_seconds(&fs_entry.metadata);
267 if truncate_i64(mtime) != entry.mtime
259 if truncate_i64(mtime) != entry.mtime
268 || mtime == self.options.last_normal_time
260 || mtime == self.options.last_normal_time
269 {
261 {
270 self.outcome.lock().unwrap().unsure.push(full_path)
262 self.outcome.lock().unwrap().unsure.push(full_path)
271 } else if self.options.list_clean {
263 } else if self.options.list_clean {
272 self.outcome.lock().unwrap().clean.push(full_path)
264 self.outcome.lock().unwrap().clean.push(full_path)
273 }
265 }
274 }
266 }
275 }
267 }
276
268
277 /// A node in the dirstate tree has no corresponding filesystem entry
269 /// A node in the dirstate tree has no corresponding filesystem entry
278 fn traverse_dirstate_only(
270 fn traverse_dirstate_only(&self, dirstate_node: NodeRef<'tree, '_>) {
279 &self,
271 self.mark_removed_or_deleted_if_file(
280 hg_path: &'tree HgPath,
272 dirstate_node.full_path(),
281 dirstate_node: &'tree Node,
273 dirstate_node.state(),
282 ) {
274 );
283 self.mark_removed_or_deleted_if_file(hg_path, dirstate_node.state());
275 dirstate_node
284 dirstate_node.children.par_iter().for_each(
276 .children()
285 |(child_hg_path, child_node)| {
277 .par_iter()
286 self.traverse_dirstate_only(
278 .for_each(|child_node| self.traverse_dirstate_only(child_node))
287 child_hg_path.full_path(),
288 child_node,
289 )
290 },
291 )
292 }
279 }
293
280
294 /// A node in the dirstate tree has no corresponding *file* on the
281 /// A node in the dirstate tree has no corresponding *file* on the
295 /// filesystem
282 /// filesystem
296 ///
283 ///
297 /// Does nothing on a "directory" node
284 /// Does nothing on a "directory" node
298 fn mark_removed_or_deleted_if_file(
285 fn mark_removed_or_deleted_if_file(
299 &self,
286 &self,
300 hg_path: &'tree HgPath,
287 hg_path: &'tree HgPath,
301 dirstate_node_state: Option<EntryState>,
288 dirstate_node_state: Option<EntryState>,
302 ) {
289 ) {
303 if let Some(state) = dirstate_node_state {
290 if let Some(state) = dirstate_node_state {
304 if self.matcher.matches(hg_path) {
291 if self.matcher.matches(hg_path) {
305 if let EntryState::Removed = state {
292 if let EntryState::Removed = state {
306 self.outcome.lock().unwrap().removed.push(hg_path.into())
293 self.outcome.lock().unwrap().removed.push(hg_path.into())
307 } else {
294 } else {
308 self.outcome.lock().unwrap().deleted.push(hg_path.into())
295 self.outcome.lock().unwrap().deleted.push(hg_path.into())
309 }
296 }
310 }
297 }
311 }
298 }
312 }
299 }
313
300
314 /// Something in the filesystem has no corresponding dirstate node
301 /// Something in the filesystem has no corresponding dirstate node
315 fn traverse_fs_only(
302 fn traverse_fs_only(
316 &self,
303 &self,
317 has_ignored_ancestor: bool,
304 has_ignored_ancestor: bool,
318 directory_hg_path: &HgPath,
305 directory_hg_path: &HgPath,
319 fs_entry: &DirEntry,
306 fs_entry: &DirEntry,
320 ) {
307 ) {
321 let hg_path = directory_hg_path.join(&fs_entry.base_name);
308 let hg_path = directory_hg_path.join(&fs_entry.base_name);
322 let file_type = fs_entry.metadata.file_type();
309 let file_type = fs_entry.metadata.file_type();
323 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
310 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
324 if file_type.is_dir() {
311 if file_type.is_dir() {
325 let is_ignored =
312 let is_ignored =
326 has_ignored_ancestor || (self.ignore_fn)(&hg_path);
313 has_ignored_ancestor || (self.ignore_fn)(&hg_path);
327 let traverse_children = if is_ignored {
314 let traverse_children = if is_ignored {
328 // Descendants of an ignored directory are all ignored
315 // Descendants of an ignored directory are all ignored
329 self.options.list_ignored
316 self.options.list_ignored
330 } else {
317 } else {
331 // Descendants of an unknown directory may be either unknown or
318 // Descendants of an unknown directory may be either unknown or
332 // ignored
319 // ignored
333 self.options.list_unknown || self.options.list_ignored
320 self.options.list_unknown || self.options.list_ignored
334 };
321 };
335 if traverse_children {
322 if traverse_children {
336 let is_at_repo_root = false;
323 let is_at_repo_root = false;
337 if let Ok(children_fs_entries) = self.read_dir(
324 if let Ok(children_fs_entries) = self.read_dir(
338 &hg_path,
325 &hg_path,
339 &fs_entry.full_path,
326 &fs_entry.full_path,
340 is_at_repo_root,
327 is_at_repo_root,
341 ) {
328 ) {
342 children_fs_entries.par_iter().for_each(|child_fs_entry| {
329 children_fs_entries.par_iter().for_each(|child_fs_entry| {
343 self.traverse_fs_only(
330 self.traverse_fs_only(
344 is_ignored,
331 is_ignored,
345 &hg_path,
332 &hg_path,
346 child_fs_entry,
333 child_fs_entry,
347 )
334 )
348 })
335 })
349 }
336 }
350 }
337 }
351 if self.options.collect_traversed_dirs {
338 if self.options.collect_traversed_dirs {
352 self.outcome.lock().unwrap().traversed.push(hg_path.into())
339 self.outcome.lock().unwrap().traversed.push(hg_path.into())
353 }
340 }
354 } else if file_or_symlink && self.matcher.matches(&hg_path) {
341 } else if file_or_symlink && self.matcher.matches(&hg_path) {
355 self.mark_unknown_or_ignored(has_ignored_ancestor, hg_path.into())
342 self.mark_unknown_or_ignored(has_ignored_ancestor, hg_path.into())
356 }
343 }
357 }
344 }
358
345
359 fn mark_unknown_or_ignored(
346 fn mark_unknown_or_ignored(
360 &self,
347 &self,
361 has_ignored_ancestor: bool,
348 has_ignored_ancestor: bool,
362 hg_path: Cow<'tree, HgPath>,
349 hg_path: Cow<'tree, HgPath>,
363 ) {
350 ) {
364 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path);
351 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path);
365 if is_ignored {
352 if is_ignored {
366 if self.options.list_ignored {
353 if self.options.list_ignored {
367 self.outcome.lock().unwrap().ignored.push(hg_path)
354 self.outcome.lock().unwrap().ignored.push(hg_path)
368 }
355 }
369 } else {
356 } else {
370 if self.options.list_unknown {
357 if self.options.list_unknown {
371 self.outcome.lock().unwrap().unknown.push(hg_path)
358 self.outcome.lock().unwrap().unknown.push(hg_path)
372 }
359 }
373 }
360 }
374 }
361 }
375 }
362 }
376
363
377 #[cfg(unix)] // TODO
364 #[cfg(unix)] // TODO
378 fn mtime_seconds(metadata: &std::fs::Metadata) -> i64 {
365 fn mtime_seconds(metadata: &std::fs::Metadata) -> i64 {
379 // Going through `Metadata::modified()` would be portable, but would take
366 // Going through `Metadata::modified()` would be portable, but would take
380 // care to construct a `SystemTime` value with sub-second precision just
367 // care to construct a `SystemTime` value with sub-second precision just
381 // for us to throw that away here.
368 // for us to throw that away here.
382 use std::os::unix::fs::MetadataExt;
369 use std::os::unix::fs::MetadataExt;
383 metadata.mtime()
370 metadata.mtime()
384 }
371 }
385
372
386 struct DirEntry {
373 struct DirEntry {
387 base_name: HgPathBuf,
374 base_name: HgPathBuf,
388 full_path: PathBuf,
375 full_path: PathBuf,
389 metadata: std::fs::Metadata,
376 metadata: std::fs::Metadata,
390 }
377 }
391
378
392 impl DirEntry {
379 impl DirEntry {
393 /// Returns **unsorted** entries in the given directory, with name and
380 /// Returns **unsorted** entries in the given directory, with name and
394 /// metadata.
381 /// metadata.
395 ///
382 ///
396 /// If a `.hg` sub-directory is encountered:
383 /// If a `.hg` sub-directory is encountered:
397 ///
384 ///
398 /// * At the repository root, ignore that sub-directory
385 /// * At the repository root, ignore that sub-directory
399 /// * Elsewhere, we’re listing the content of a sub-repo. Return an empty
386 /// * Elsewhere, we’re listing the content of a sub-repo. Return an empty
400 /// list instead.
387 /// list instead.
401 fn read_dir(path: &Path, is_at_repo_root: bool) -> io::Result<Vec<Self>> {
388 fn read_dir(path: &Path, is_at_repo_root: bool) -> io::Result<Vec<Self>> {
402 let mut results = Vec::new();
389 let mut results = Vec::new();
403 for entry in path.read_dir()? {
390 for entry in path.read_dir()? {
404 let entry = entry?;
391 let entry = entry?;
405 let metadata = entry.metadata()?;
392 let metadata = entry.metadata()?;
406 let name = get_bytes_from_os_string(entry.file_name());
393 let name = get_bytes_from_os_string(entry.file_name());
407 // FIXME don't do this when cached
394 // FIXME don't do this when cached
408 if name == b".hg" {
395 if name == b".hg" {
409 if is_at_repo_root {
396 if is_at_repo_root {
410 // Skip the repo’s own .hg (might be a symlink)
397 // Skip the repo’s own .hg (might be a symlink)
411 continue;
398 continue;
412 } else if metadata.is_dir() {
399 } else if metadata.is_dir() {
413 // A .hg sub-directory at another location means a subrepo,
400 // A .hg sub-directory at another location means a subrepo,
414 // skip it entirely.
401 // skip it entirely.
415 return Ok(Vec::new());
402 return Ok(Vec::new());
416 }
403 }
417 }
404 }
418 results.push(DirEntry {
405 results.push(DirEntry {
419 base_name: name.into(),
406 base_name: name.into(),
420 full_path: entry.path(),
407 full_path: entry.path(),
421 metadata,
408 metadata,
422 })
409 })
423 }
410 }
424 Ok(results)
411 Ok(results)
425 }
412 }
426 }
413 }
General Comments 0
You need to be logged in to leave comments. Login now