##// END OF EJS Templates
rust: start plugging the dirstate tree behind a feature gate...
Raphaël Gomès -
r46185:496537c9 default
parent child Browse files
Show More
@@ -1,83 +1,90 b''
1 1 // dirstate module
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 use crate::{utils::hg_path::HgPathBuf, DirstateParseError, FastHashMap};
9 9 use std::collections::hash_map;
10 10 use std::convert::TryFrom;
11 11
12 12 pub mod dirs_multiset;
13 13 pub mod dirstate_map;
14 14 #[cfg(feature = "dirstate-tree")]
15 15 pub mod dirstate_tree;
16 16 pub mod parsers;
17 17 pub mod status;
18 18
19 19 #[derive(Debug, PartialEq, Clone)]
20 20 pub struct DirstateParents {
21 21 pub p1: [u8; 20],
22 22 pub p2: [u8; 20],
23 23 }
24 24
25 25 /// The C implementation uses all signed types. This will be an issue
26 26 /// either when 4GB+ source files are commonplace or in 2038, whichever
27 27 /// comes first.
28 28 #[derive(Debug, PartialEq, Copy, Clone)]
29 29 pub struct DirstateEntry {
30 30 pub state: EntryState,
31 31 pub mode: i32,
32 32 pub mtime: i32,
33 33 pub size: i32,
34 34 }
35 35
36 36 /// A `DirstateEntry` with a size of `-2` means that it was merged from the
37 37 /// other parent. This allows revert to pick the right status back during a
38 38 /// merge.
39 39 pub const SIZE_FROM_OTHER_PARENT: i32 = -2;
40 40
41 #[cfg(not(feature = "dirstate-tree"))]
41 42 pub type StateMap = FastHashMap<HgPathBuf, DirstateEntry>;
43 #[cfg(not(feature = "dirstate-tree"))]
42 44 pub type StateMapIter<'a> = hash_map::Iter<'a, HgPathBuf, DirstateEntry>;
45
46 #[cfg(feature = "dirstate-tree")]
47 pub type StateMap = dirstate_tree::tree::Tree;
48 #[cfg(feature = "dirstate-tree")]
49 pub type StateMapIter<'a> = dirstate_tree::iter::Iter<'a>;
43 50 pub type CopyMap = FastHashMap<HgPathBuf, HgPathBuf>;
44 51 pub type CopyMapIter<'a> = hash_map::Iter<'a, HgPathBuf, HgPathBuf>;
45 52
46 53 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
47 54 pub enum EntryState {
48 55 Normal,
49 56 Added,
50 57 Removed,
51 58 Merged,
52 59 Unknown,
53 60 }
54 61
55 62 impl TryFrom<u8> for EntryState {
56 63 type Error = DirstateParseError;
57 64
58 65 fn try_from(value: u8) -> Result<Self, Self::Error> {
59 66 match value {
60 67 b'n' => Ok(EntryState::Normal),
61 68 b'a' => Ok(EntryState::Added),
62 69 b'r' => Ok(EntryState::Removed),
63 70 b'm' => Ok(EntryState::Merged),
64 71 b'?' => Ok(EntryState::Unknown),
65 72 _ => Err(DirstateParseError::CorruptedEntry(format!(
66 73 "Incorrect entry state {}",
67 74 value
68 75 ))),
69 76 }
70 77 }
71 78 }
72 79
73 80 impl Into<u8> for EntryState {
74 81 fn into(self) -> u8 {
75 82 match self {
76 83 EntryState::Normal => b'n',
77 84 EntryState::Added => b'a',
78 85 EntryState::Removed => b'r',
79 86 EntryState::Merged => b'm',
80 87 EntryState::Unknown => b'?',
81 88 }
82 89 }
83 90 }
@@ -1,422 +1,446 b''
1 1 // dirs_multiset.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 //! A multiset of directory names.
9 9 //!
10 10 //! Used to counts the references to directories in a manifest or dirstate.
11 11 use crate::{
12 12 dirstate::EntryState,
13 13 utils::{
14 14 files,
15 15 hg_path::{HgPath, HgPathBuf, HgPathError},
16 16 },
17 DirstateEntry, DirstateMapError, FastHashMap,
17 DirstateEntry, DirstateMapError, FastHashMap, StateMap,
18 18 };
19 19 use std::collections::{hash_map, hash_map::Entry, HashMap, HashSet};
20 20
21 21 // could be encapsulated if we care API stability more seriously
22 22 pub type DirsMultisetIter<'a> = hash_map::Keys<'a, HgPathBuf, u32>;
23 23
24 24 #[derive(PartialEq, Debug)]
25 25 pub struct DirsMultiset {
26 26 inner: FastHashMap<HgPathBuf, u32>,
27 27 }
28 28
29 29 impl DirsMultiset {
30 30 /// Initializes the multiset from a dirstate.
31 31 ///
32 32 /// If `skip_state` is provided, skips dirstate entries with equal state.
33 #[cfg(not(feature = "dirstate-tree"))]
33 34 pub fn from_dirstate(
34 dirstate: &FastHashMap<HgPathBuf, DirstateEntry>,
35 dirstate: &StateMap,
35 36 skip_state: Option<EntryState>,
36 37 ) -> Result<Self, DirstateMapError> {
37 38 let mut multiset = DirsMultiset {
38 39 inner: FastHashMap::default(),
39 40 };
40
41 for (filename, DirstateEntry { state, .. }) in dirstate {
41 for (filename, DirstateEntry { state, .. }) in dirstate.iter() {
42 42 // This `if` is optimized out of the loop
43 43 if let Some(skip) = skip_state {
44 44 if skip != *state {
45 45 multiset.add_path(filename)?;
46 46 }
47 47 } else {
48 48 multiset.add_path(filename)?;
49 49 }
50 50 }
51 51
52 52 Ok(multiset)
53 53 }
54 /// Initializes the multiset from a dirstate.
55 ///
56 /// If `skip_state` is provided, skips dirstate entries with equal state.
57 #[cfg(feature = "dirstate-tree")]
58 pub fn from_dirstate(
59 dirstate: &StateMap,
60 skip_state: Option<EntryState>,
61 ) -> Result<Self, DirstateMapError> {
62 let mut multiset = DirsMultiset {
63 inner: FastHashMap::default(),
64 };
65 for (filename, DirstateEntry { state, .. }) in dirstate.iter() {
66 // This `if` is optimized out of the loop
67 if let Some(skip) = skip_state {
68 if skip != state {
69 multiset.add_path(filename)?;
70 }
71 } else {
72 multiset.add_path(filename)?;
73 }
74 }
75
76 Ok(multiset)
77 }
54 78
55 79 /// Initializes the multiset from a manifest.
56 80 pub fn from_manifest(
57 81 manifest: &[impl AsRef<HgPath>],
58 82 ) -> Result<Self, DirstateMapError> {
59 83 let mut multiset = DirsMultiset {
60 84 inner: FastHashMap::default(),
61 85 };
62 86
63 87 for filename in manifest {
64 88 multiset.add_path(filename.as_ref())?;
65 89 }
66 90
67 91 Ok(multiset)
68 92 }
69 93
70 94 /// Increases the count of deepest directory contained in the path.
71 95 ///
72 96 /// If the directory is not yet in the map, adds its parents.
73 97 pub fn add_path(
74 98 &mut self,
75 99 path: impl AsRef<HgPath>,
76 100 ) -> Result<(), DirstateMapError> {
77 101 for subpath in files::find_dirs(path.as_ref()) {
78 102 if subpath.as_bytes().last() == Some(&b'/') {
79 103 // TODO Remove this once PathAuditor is certified
80 104 // as the only entrypoint for path data
81 105 let second_slash_index = subpath.len() - 1;
82 106
83 107 return Err(DirstateMapError::InvalidPath(
84 108 HgPathError::ConsecutiveSlashes {
85 109 bytes: path.as_ref().as_bytes().to_owned(),
86 110 second_slash_index,
87 111 },
88 112 ));
89 113 }
90 114 if let Some(val) = self.inner.get_mut(subpath) {
91 115 *val += 1;
92 116 break;
93 117 }
94 118 self.inner.insert(subpath.to_owned(), 1);
95 119 }
96 120 Ok(())
97 121 }
98 122
99 123 /// Decreases the count of deepest directory contained in the path.
100 124 ///
101 125 /// If it is the only reference, decreases all parents until one is
102 126 /// removed.
103 127 /// If the directory is not in the map, something horrible has happened.
104 128 pub fn delete_path(
105 129 &mut self,
106 130 path: impl AsRef<HgPath>,
107 131 ) -> Result<(), DirstateMapError> {
108 132 for subpath in files::find_dirs(path.as_ref()) {
109 133 match self.inner.entry(subpath.to_owned()) {
110 134 Entry::Occupied(mut entry) => {
111 135 let val = *entry.get();
112 136 if val > 1 {
113 137 entry.insert(val - 1);
114 138 break;
115 139 }
116 140 entry.remove();
117 141 }
118 142 Entry::Vacant(_) => {
119 143 return Err(DirstateMapError::PathNotFound(
120 144 path.as_ref().to_owned(),
121 145 ))
122 146 }
123 147 };
124 148 }
125 149
126 150 Ok(())
127 151 }
128 152
129 153 pub fn contains(&self, key: impl AsRef<HgPath>) -> bool {
130 154 self.inner.contains_key(key.as_ref())
131 155 }
132 156
133 157 pub fn iter(&self) -> DirsMultisetIter {
134 158 self.inner.keys()
135 159 }
136 160
137 161 pub fn len(&self) -> usize {
138 162 self.inner.len()
139 163 }
140 164
141 165 pub fn is_empty(&self) -> bool {
142 166 self.len() == 0
143 167 }
144 168 }
145 169
146 170 /// This is basically a reimplementation of `DirsMultiset` that stores the
147 171 /// children instead of just a count of them, plus a small optional
148 172 /// optimization to avoid some directories we don't need.
149 173 #[derive(PartialEq, Debug)]
150 174 pub struct DirsChildrenMultiset<'a> {
151 175 inner: FastHashMap<&'a HgPath, HashSet<&'a HgPath>>,
152 176 only_include: Option<HashSet<&'a HgPath>>,
153 177 }
154 178
155 179 impl<'a> DirsChildrenMultiset<'a> {
156 180 pub fn new(
157 181 paths: impl Iterator<Item = &'a HgPathBuf>,
158 182 only_include: Option<&'a HashSet<impl AsRef<HgPath> + 'a>>,
159 183 ) -> Self {
160 184 let mut new = Self {
161 185 inner: HashMap::default(),
162 186 only_include: only_include
163 187 .map(|s| s.iter().map(AsRef::as_ref).collect()),
164 188 };
165 189
166 190 for path in paths {
167 191 new.add_path(path)
168 192 }
169 193
170 194 new
171 195 }
172 196 fn add_path(&mut self, path: &'a (impl AsRef<HgPath> + 'a)) {
173 197 if path.as_ref().is_empty() {
174 198 return;
175 199 }
176 200 for (directory, basename) in files::find_dirs_with_base(path.as_ref())
177 201 {
178 202 if !self.is_dir_included(directory) {
179 203 continue;
180 204 }
181 205 self.inner
182 206 .entry(directory)
183 207 .and_modify(|e| {
184 208 e.insert(basename);
185 209 })
186 210 .or_insert_with(|| {
187 211 let mut set = HashSet::new();
188 212 set.insert(basename);
189 213 set
190 214 });
191 215 }
192 216 }
193 217 fn is_dir_included(&self, dir: impl AsRef<HgPath>) -> bool {
194 218 match &self.only_include {
195 219 None => false,
196 220 Some(i) => i.contains(dir.as_ref()),
197 221 }
198 222 }
199 223
200 224 pub fn get(
201 225 &self,
202 226 path: impl AsRef<HgPath>,
203 227 ) -> Option<&HashSet<&'a HgPath>> {
204 228 self.inner.get(path.as_ref())
205 229 }
206 230 }
207 231
208 232 #[cfg(test)]
209 233 mod tests {
210 234 use super::*;
211 235
212 236 #[test]
213 237 fn test_delete_path_path_not_found() {
214 238 let manifest: Vec<HgPathBuf> = vec![];
215 239 let mut map = DirsMultiset::from_manifest(&manifest).unwrap();
216 240 let path = HgPathBuf::from_bytes(b"doesnotexist/");
217 241 assert_eq!(
218 242 Err(DirstateMapError::PathNotFound(path.to_owned())),
219 243 map.delete_path(&path)
220 244 );
221 245 }
222 246
223 247 #[test]
224 248 fn test_delete_path_empty_path() {
225 249 let mut map =
226 250 DirsMultiset::from_manifest(&vec![HgPathBuf::new()]).unwrap();
227 251 let path = HgPath::new(b"");
228 252 assert_eq!(Ok(()), map.delete_path(path));
229 253 assert_eq!(
230 254 Err(DirstateMapError::PathNotFound(path.to_owned())),
231 255 map.delete_path(path)
232 256 );
233 257 }
234 258
235 259 #[test]
236 260 fn test_delete_path_successful() {
237 261 let mut map = DirsMultiset {
238 262 inner: [("", 5), ("a", 3), ("a/b", 2), ("a/c", 1)]
239 263 .iter()
240 264 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
241 265 .collect(),
242 266 };
243 267
244 268 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/b/")));
245 269 eprintln!("{:?}", map);
246 270 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/b/")));
247 271 eprintln!("{:?}", map);
248 272 assert_eq!(
249 273 Err(DirstateMapError::PathNotFound(HgPathBuf::from_bytes(
250 274 b"a/b/"
251 275 ))),
252 276 map.delete_path(HgPath::new(b"a/b/"))
253 277 );
254 278
255 279 assert_eq!(2, *map.inner.get(HgPath::new(b"a")).unwrap());
256 280 assert_eq!(1, *map.inner.get(HgPath::new(b"a/c")).unwrap());
257 281 eprintln!("{:?}", map);
258 282 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/")));
259 283 eprintln!("{:?}", map);
260 284
261 285 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/c/")));
262 286 assert_eq!(
263 287 Err(DirstateMapError::PathNotFound(HgPathBuf::from_bytes(
264 288 b"a/c/"
265 289 ))),
266 290 map.delete_path(HgPath::new(b"a/c/"))
267 291 );
268 292 }
269 293
270 294 #[test]
271 295 fn test_add_path_empty_path() {
272 296 let manifest: Vec<HgPathBuf> = vec![];
273 297 let mut map = DirsMultiset::from_manifest(&manifest).unwrap();
274 298 let path = HgPath::new(b"");
275 299 map.add_path(path).unwrap();
276 300
277 301 assert_eq!(1, map.len());
278 302 }
279 303
280 304 #[test]
281 305 fn test_add_path_successful() {
282 306 let manifest: Vec<HgPathBuf> = vec![];
283 307 let mut map = DirsMultiset::from_manifest(&manifest).unwrap();
284 308
285 309 map.add_path(HgPath::new(b"a/")).unwrap();
286 310 assert_eq!(1, *map.inner.get(HgPath::new(b"a")).unwrap());
287 311 assert_eq!(1, *map.inner.get(HgPath::new(b"")).unwrap());
288 312 assert_eq!(2, map.len());
289 313
290 314 // Non directory should be ignored
291 315 map.add_path(HgPath::new(b"a")).unwrap();
292 316 assert_eq!(1, *map.inner.get(HgPath::new(b"a")).unwrap());
293 317 assert_eq!(2, map.len());
294 318
295 319 // Non directory will still add its base
296 320 map.add_path(HgPath::new(b"a/b")).unwrap();
297 321 assert_eq!(2, *map.inner.get(HgPath::new(b"a")).unwrap());
298 322 assert_eq!(2, map.len());
299 323
300 324 // Duplicate path works
301 325 map.add_path(HgPath::new(b"a/")).unwrap();
302 326 assert_eq!(3, *map.inner.get(HgPath::new(b"a")).unwrap());
303 327
304 328 // Nested dir adds to its base
305 329 map.add_path(HgPath::new(b"a/b/")).unwrap();
306 330 assert_eq!(4, *map.inner.get(HgPath::new(b"a")).unwrap());
307 331 assert_eq!(1, *map.inner.get(HgPath::new(b"a/b")).unwrap());
308 332
309 333 // but not its base's base, because it already existed
310 334 map.add_path(HgPath::new(b"a/b/c/")).unwrap();
311 335 assert_eq!(4, *map.inner.get(HgPath::new(b"a")).unwrap());
312 336 assert_eq!(2, *map.inner.get(HgPath::new(b"a/b")).unwrap());
313 337
314 338 map.add_path(HgPath::new(b"a/c/")).unwrap();
315 339 assert_eq!(1, *map.inner.get(HgPath::new(b"a/c")).unwrap());
316 340
317 341 let expected = DirsMultiset {
318 342 inner: [("", 2), ("a", 5), ("a/b", 2), ("a/b/c", 1), ("a/c", 1)]
319 343 .iter()
320 344 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
321 345 .collect(),
322 346 };
323 347 assert_eq!(map, expected);
324 348 }
325 349
326 350 #[test]
327 351 fn test_dirsmultiset_new_empty() {
328 352 let manifest: Vec<HgPathBuf> = vec![];
329 353 let new = DirsMultiset::from_manifest(&manifest).unwrap();
330 354 let expected = DirsMultiset {
331 355 inner: FastHashMap::default(),
332 356 };
333 357 assert_eq!(expected, new);
334 358
335 let new = DirsMultiset::from_dirstate(&FastHashMap::default(), None)
336 .unwrap();
359 let new =
360 DirsMultiset::from_dirstate(&StateMap::default(), None).unwrap();
337 361 let expected = DirsMultiset {
338 362 inner: FastHashMap::default(),
339 363 };
340 364 assert_eq!(expected, new);
341 365 }
342 366
343 367 #[test]
344 368 fn test_dirsmultiset_new_no_skip() {
345 369 let input_vec: Vec<HgPathBuf> = ["a/", "b/", "a/c", "a/d/"]
346 370 .iter()
347 371 .map(|e| HgPathBuf::from_bytes(e.as_bytes()))
348 372 .collect();
349 373 let expected_inner = [("", 2), ("a", 3), ("b", 1), ("a/d", 1)]
350 374 .iter()
351 375 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
352 376 .collect();
353 377
354 378 let new = DirsMultiset::from_manifest(&input_vec).unwrap();
355 379 let expected = DirsMultiset {
356 380 inner: expected_inner,
357 381 };
358 382 assert_eq!(expected, new);
359 383
360 let input_map = ["a/", "b/", "a/c", "a/d/"]
384 let input_map = ["b/x", "a/c", "a/d/x"]
361 385 .iter()
362 386 .map(|f| {
363 387 (
364 388 HgPathBuf::from_bytes(f.as_bytes()),
365 389 DirstateEntry {
366 390 state: EntryState::Normal,
367 391 mode: 0,
368 392 mtime: 0,
369 393 size: 0,
370 394 },
371 395 )
372 396 })
373 397 .collect();
374 let expected_inner = [("", 2), ("a", 3), ("b", 1), ("a/d", 1)]
398 let expected_inner = [("", 2), ("a", 2), ("b", 1), ("a/d", 1)]
375 399 .iter()
376 400 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
377 401 .collect();
378 402
379 403 let new = DirsMultiset::from_dirstate(&input_map, None).unwrap();
380 404 let expected = DirsMultiset {
381 405 inner: expected_inner,
382 406 };
383 407 assert_eq!(expected, new);
384 408 }
385 409
386 410 #[test]
387 411 fn test_dirsmultiset_new_skip() {
388 412 let input_map = [
389 413 ("a/", EntryState::Normal),
390 ("a/b/", EntryState::Normal),
414 ("a/b", EntryState::Normal),
391 415 ("a/c", EntryState::Removed),
392 ("a/d/", EntryState::Merged),
416 ("a/d", EntryState::Merged),
393 417 ]
394 418 .iter()
395 419 .map(|(f, state)| {
396 420 (
397 421 HgPathBuf::from_bytes(f.as_bytes()),
398 422 DirstateEntry {
399 423 state: *state,
400 424 mode: 0,
401 425 mtime: 0,
402 426 size: 0,
403 427 },
404 428 )
405 429 })
406 430 .collect();
407 431
408 432 // "a" incremented with "a/c" and "a/d/"
409 let expected_inner = [("", 1), ("a", 2), ("a/d", 1)]
433 let expected_inner = [("", 1), ("a", 2)]
410 434 .iter()
411 435 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
412 436 .collect();
413 437
414 438 let new =
415 439 DirsMultiset::from_dirstate(&input_map, Some(EntryState::Normal))
416 440 .unwrap();
417 441 let expected = DirsMultiset {
418 442 inner: expected_inner,
419 443 };
420 444 assert_eq!(expected, new);
421 445 }
422 446 }
@@ -1,505 +1,546 b''
1 1 // dirstate_map.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 use crate::revlog::node::NULL_NODE_ID;
9 9 use crate::{
10 10 dirstate::{parsers::PARENT_SIZE, EntryState, SIZE_FROM_OTHER_PARENT},
11 11 pack_dirstate, parse_dirstate,
12 12 utils::{
13 13 files::normalize_case,
14 14 hg_path::{HgPath, HgPathBuf},
15 15 },
16 16 CopyMap, DirsMultiset, DirstateEntry, DirstateError, DirstateMapError,
17 17 DirstateParents, DirstateParseError, FastHashMap, StateMap,
18 18 };
19 use core::borrow::Borrow;
20 19 use micro_timer::timed;
21 20 use std::collections::HashSet;
22 21 use std::convert::TryInto;
23 22 use std::iter::FromIterator;
24 23 use std::ops::Deref;
25 24 use std::time::Duration;
26 25
27 26 pub type FileFoldMap = FastHashMap<HgPathBuf, HgPathBuf>;
28 27
29 28 const MTIME_UNSET: i32 = -1;
30 29
31 30 #[derive(Default)]
32 31 pub struct DirstateMap {
33 32 state_map: StateMap,
34 33 pub copy_map: CopyMap,
35 34 file_fold_map: Option<FileFoldMap>,
36 35 pub dirs: Option<DirsMultiset>,
37 36 pub all_dirs: Option<DirsMultiset>,
38 37 non_normal_set: Option<HashSet<HgPathBuf>>,
39 38 other_parent_set: Option<HashSet<HgPathBuf>>,
40 39 parents: Option<DirstateParents>,
41 40 dirty_parents: bool,
42 41 }
43 42
44 43 /// Should only really be used in python interface code, for clarity
45 44 impl Deref for DirstateMap {
46 45 type Target = StateMap;
47 46
48 47 fn deref(&self) -> &Self::Target {
49 48 &self.state_map
50 49 }
51 50 }
52 51
53 52 impl FromIterator<(HgPathBuf, DirstateEntry)> for DirstateMap {
54 53 fn from_iter<I: IntoIterator<Item = (HgPathBuf, DirstateEntry)>>(
55 54 iter: I,
56 55 ) -> Self {
57 56 Self {
58 57 state_map: iter.into_iter().collect(),
59 58 ..Self::default()
60 59 }
61 60 }
62 61 }
63 62
64 63 impl DirstateMap {
65 64 pub fn new() -> Self {
66 65 Self::default()
67 66 }
68 67
69 68 pub fn clear(&mut self) {
70 self.state_map.clear();
69 self.state_map = StateMap::default();
71 70 self.copy_map.clear();
72 71 self.file_fold_map = None;
73 72 self.non_normal_set = None;
74 73 self.other_parent_set = None;
75 74 self.set_parents(&DirstateParents {
76 75 p1: NULL_NODE_ID,
77 76 p2: NULL_NODE_ID,
78 77 })
79 78 }
80 79
81 80 /// Add a tracked file to the dirstate
82 81 pub fn add_file(
83 82 &mut self,
84 83 filename: &HgPath,
85 84 old_state: EntryState,
86 85 entry: DirstateEntry,
87 86 ) -> Result<(), DirstateMapError> {
88 87 if old_state == EntryState::Unknown || old_state == EntryState::Removed
89 88 {
90 89 if let Some(ref mut dirs) = self.dirs {
91 90 dirs.add_path(filename)?;
92 91 }
93 92 }
94 93 if old_state == EntryState::Unknown {
95 94 if let Some(ref mut all_dirs) = self.all_dirs {
96 95 all_dirs.add_path(filename)?;
97 96 }
98 97 }
99 98 self.state_map.insert(filename.to_owned(), entry.to_owned());
100 99
101 100 if entry.state != EntryState::Normal || entry.mtime == MTIME_UNSET {
102 101 self.get_non_normal_other_parent_entries()
103 102 .0
104 103 .insert(filename.to_owned());
105 104 }
106 105
107 106 if entry.size == SIZE_FROM_OTHER_PARENT {
108 107 self.get_non_normal_other_parent_entries()
109 108 .1
110 109 .insert(filename.to_owned());
111 110 }
112 111 Ok(())
113 112 }
114 113
115 114 /// Mark a file as removed in the dirstate.
116 115 ///
117 116 /// The `size` parameter is used to store sentinel values that indicate
118 117 /// the file's previous state. In the future, we should refactor this
119 118 /// to be more explicit about what that state is.
120 119 pub fn remove_file(
121 120 &mut self,
122 121 filename: &HgPath,
123 122 old_state: EntryState,
124 123 size: i32,
125 124 ) -> Result<(), DirstateMapError> {
126 125 if old_state != EntryState::Unknown && old_state != EntryState::Removed
127 126 {
128 127 if let Some(ref mut dirs) = self.dirs {
129 128 dirs.delete_path(filename)?;
130 129 }
131 130 }
132 131 if old_state == EntryState::Unknown {
133 132 if let Some(ref mut all_dirs) = self.all_dirs {
134 133 all_dirs.add_path(filename)?;
135 134 }
136 135 }
137 136
138 137 if let Some(ref mut file_fold_map) = self.file_fold_map {
139 138 file_fold_map.remove(&normalize_case(filename));
140 139 }
141 140 self.state_map.insert(
142 141 filename.to_owned(),
143 142 DirstateEntry {
144 143 state: EntryState::Removed,
145 144 mode: 0,
146 145 size,
147 146 mtime: 0,
148 147 },
149 148 );
150 149 self.get_non_normal_other_parent_entries()
151 150 .0
152 151 .insert(filename.to_owned());
153 152 Ok(())
154 153 }
155 154
156 155 /// Remove a file from the dirstate.
157 156 /// Returns `true` if the file was previously recorded.
158 157 pub fn drop_file(
159 158 &mut self,
160 159 filename: &HgPath,
161 160 old_state: EntryState,
162 161 ) -> Result<bool, DirstateMapError> {
163 162 let exists = self.state_map.remove(filename).is_some();
164 163
165 164 if exists {
166 165 if old_state != EntryState::Removed {
167 166 if let Some(ref mut dirs) = self.dirs {
168 167 dirs.delete_path(filename)?;
169 168 }
170 169 }
171 170 if let Some(ref mut all_dirs) = self.all_dirs {
172 171 all_dirs.delete_path(filename)?;
173 172 }
174 173 }
175 174 if let Some(ref mut file_fold_map) = self.file_fold_map {
176 175 file_fold_map.remove(&normalize_case(filename));
177 176 }
178 177 self.get_non_normal_other_parent_entries()
179 178 .0
180 179 .remove(filename);
181 180
182 181 Ok(exists)
183 182 }
184 183
185 184 pub fn clear_ambiguous_times(
186 185 &mut self,
187 186 filenames: Vec<HgPathBuf>,
188 187 now: i32,
189 188 ) {
190 189 for filename in filenames {
191 190 let mut changed = false;
192 self.state_map
193 .entry(filename.to_owned())
194 .and_modify(|entry| {
195 if entry.state == EntryState::Normal && entry.mtime == now
196 {
197 changed = true;
198 *entry = DirstateEntry {
199 mtime: MTIME_UNSET,
200 ..*entry
201 };
202 }
203 });
191 if let Some(entry) = self.state_map.get_mut(&filename) {
192 if entry.state == EntryState::Normal && entry.mtime == now {
193 changed = true;
194 *entry = DirstateEntry {
195 mtime: MTIME_UNSET,
196 ..*entry
197 };
198 }
199 }
204 200 if changed {
205 201 self.get_non_normal_other_parent_entries()
206 202 .0
207 203 .insert(filename.to_owned());
208 204 }
209 205 }
210 206 }
211 207
212 208 pub fn non_normal_entries_remove(
213 209 &mut self,
214 210 key: impl AsRef<HgPath>,
215 211 ) -> bool {
216 212 self.get_non_normal_other_parent_entries()
217 213 .0
218 214 .remove(key.as_ref())
219 215 }
220 216 pub fn non_normal_entries_union(
221 217 &mut self,
222 218 other: HashSet<HgPathBuf>,
223 219 ) -> Vec<HgPathBuf> {
224 220 self.get_non_normal_other_parent_entries()
225 221 .0
226 222 .union(&other)
227 223 .map(ToOwned::to_owned)
228 224 .collect()
229 225 }
230 226
231 227 pub fn get_non_normal_other_parent_entries(
232 228 &mut self,
233 229 ) -> (&mut HashSet<HgPathBuf>, &mut HashSet<HgPathBuf>) {
234 230 self.set_non_normal_other_parent_entries(false);
235 231 (
236 232 self.non_normal_set.as_mut().unwrap(),
237 233 self.other_parent_set.as_mut().unwrap(),
238 234 )
239 235 }
240 236
241 237 /// Useful to get immutable references to those sets in contexts where
242 238 /// you only have an immutable reference to the `DirstateMap`, like when
243 239 /// sharing references with Python.
244 240 ///
245 241 /// TODO, get rid of this along with the other "setter/getter" stuff when
246 242 /// a nice typestate plan is defined.
247 243 ///
248 244 /// # Panics
249 245 ///
250 246 /// Will panic if either set is `None`.
251 247 pub fn get_non_normal_other_parent_entries_panic(
252 248 &self,
253 249 ) -> (&HashSet<HgPathBuf>, &HashSet<HgPathBuf>) {
254 250 (
255 251 self.non_normal_set.as_ref().unwrap(),
256 252 self.other_parent_set.as_ref().unwrap(),
257 253 )
258 254 }
259 255
256 #[cfg(not(feature = "dirstate-tree"))]
260 257 pub fn set_non_normal_other_parent_entries(&mut self, force: bool) {
261 258 if !force
262 259 && self.non_normal_set.is_some()
263 260 && self.other_parent_set.is_some()
264 261 {
265 262 return;
266 263 }
267 264 let mut non_normal = HashSet::new();
268 265 let mut other_parent = HashSet::new();
269 266
270 267 for (
271 268 filename,
272 269 DirstateEntry {
273 270 state, size, mtime, ..
274 271 },
275 272 ) in self.state_map.iter()
276 273 {
277 274 if *state != EntryState::Normal || *mtime == MTIME_UNSET {
278 275 non_normal.insert(filename.to_owned());
279 276 }
280 277 if *state == EntryState::Normal && *size == SIZE_FROM_OTHER_PARENT
281 278 {
282 279 other_parent.insert(filename.to_owned());
283 280 }
284 281 }
285 282 self.non_normal_set = Some(non_normal);
286 283 self.other_parent_set = Some(other_parent);
287 284 }
285 #[cfg(feature = "dirstate-tree")]
286 pub fn set_non_normal_other_parent_entries(&mut self, force: bool) {
287 if !force
288 && self.non_normal_set.is_some()
289 && self.other_parent_set.is_some()
290 {
291 return;
292 }
293 let mut non_normal = HashSet::new();
294 let mut other_parent = HashSet::new();
295
296 for (
297 filename,
298 DirstateEntry {
299 state, size, mtime, ..
300 },
301 ) in self.state_map.iter()
302 {
303 if state != EntryState::Normal || mtime == MTIME_UNSET {
304 non_normal.insert(filename.to_owned());
305 }
306 if state == EntryState::Normal && size == SIZE_FROM_OTHER_PARENT {
307 other_parent.insert(filename.to_owned());
308 }
309 }
310 self.non_normal_set = Some(non_normal);
311 self.other_parent_set = Some(other_parent);
312 }
288 313
289 314 /// Both of these setters and their uses appear to be the simplest way to
290 315 /// emulate a Python lazy property, but it is ugly and unidiomatic.
291 316 /// TODO One day, rewriting this struct using the typestate might be a
292 317 /// good idea.
293 318 pub fn set_all_dirs(&mut self) -> Result<(), DirstateMapError> {
294 319 if self.all_dirs.is_none() {
295 320 self.all_dirs =
296 321 Some(DirsMultiset::from_dirstate(&self.state_map, None)?);
297 322 }
298 323 Ok(())
299 324 }
300 325
301 326 pub fn set_dirs(&mut self) -> Result<(), DirstateMapError> {
302 327 if self.dirs.is_none() {
303 328 self.dirs = Some(DirsMultiset::from_dirstate(
304 329 &self.state_map,
305 330 Some(EntryState::Removed),
306 331 )?);
307 332 }
308 333 Ok(())
309 334 }
310 335
311 336 pub fn has_tracked_dir(
312 337 &mut self,
313 338 directory: &HgPath,
314 339 ) -> Result<bool, DirstateMapError> {
315 340 self.set_dirs()?;
316 341 Ok(self.dirs.as_ref().unwrap().contains(directory))
317 342 }
318 343
319 344 pub fn has_dir(
320 345 &mut self,
321 346 directory: &HgPath,
322 347 ) -> Result<bool, DirstateMapError> {
323 348 self.set_all_dirs()?;
324 349 Ok(self.all_dirs.as_ref().unwrap().contains(directory))
325 350 }
326 351
327 352 pub fn parents(
328 353 &mut self,
329 354 file_contents: &[u8],
330 355 ) -> Result<&DirstateParents, DirstateError> {
331 356 if let Some(ref parents) = self.parents {
332 357 return Ok(parents);
333 358 }
334 359 let parents;
335 360 if file_contents.len() == PARENT_SIZE * 2 {
336 361 parents = DirstateParents {
337 362 p1: file_contents[..PARENT_SIZE].try_into().unwrap(),
338 363 p2: file_contents[PARENT_SIZE..PARENT_SIZE * 2]
339 364 .try_into()
340 365 .unwrap(),
341 366 };
342 367 } else if file_contents.is_empty() {
343 368 parents = DirstateParents {
344 369 p1: NULL_NODE_ID,
345 370 p2: NULL_NODE_ID,
346 371 };
347 372 } else {
348 373 return Err(DirstateError::Parse(DirstateParseError::Damaged));
349 374 }
350 375
351 376 self.parents = Some(parents);
352 377 Ok(self.parents.as_ref().unwrap())
353 378 }
354 379
355 380 pub fn set_parents(&mut self, parents: &DirstateParents) {
356 381 self.parents = Some(parents.clone());
357 382 self.dirty_parents = true;
358 383 }
359 384
360 385 #[timed]
361 386 pub fn read(
362 387 &mut self,
363 388 file_contents: &[u8],
364 389 ) -> Result<Option<DirstateParents>, DirstateError> {
365 390 if file_contents.is_empty() {
366 391 return Ok(None);
367 392 }
368 393
369 394 let (parents, entries, copies) = parse_dirstate(file_contents)?;
370 395 self.state_map.extend(
371 396 entries
372 397 .into_iter()
373 398 .map(|(path, entry)| (path.to_owned(), entry)),
374 399 );
375 400 self.copy_map.extend(
376 401 copies
377 402 .into_iter()
378 403 .map(|(path, copy)| (path.to_owned(), copy.to_owned())),
379 404 );
380 405
381 406 if !self.dirty_parents {
382 407 self.set_parents(&parents);
383 408 }
384 409
385 410 Ok(Some(parents))
386 411 }
387 412
388 413 pub fn pack(
389 414 &mut self,
390 415 parents: DirstateParents,
391 416 now: Duration,
392 417 ) -> Result<Vec<u8>, DirstateError> {
393 418 let packed =
394 419 pack_dirstate(&mut self.state_map, &self.copy_map, parents, now)?;
395 420
396 421 self.dirty_parents = false;
397 422
398 423 self.set_non_normal_other_parent_entries(true);
399 424 Ok(packed)
400 425 }
401
426 #[cfg(not(feature = "dirstate-tree"))]
402 427 pub fn build_file_fold_map(&mut self) -> &FileFoldMap {
403 428 if let Some(ref file_fold_map) = self.file_fold_map {
404 429 return file_fold_map;
405 430 }
406 431 let mut new_file_fold_map = FileFoldMap::default();
407 for (filename, DirstateEntry { state, .. }) in self.state_map.borrow()
408 {
432
433 for (filename, DirstateEntry { state, .. }) in self.state_map.iter() {
409 434 if *state == EntryState::Removed {
410 435 new_file_fold_map
411 .insert(normalize_case(filename), filename.to_owned());
436 .insert(normalize_case(&filename), filename.to_owned());
437 }
438 }
439 self.file_fold_map = Some(new_file_fold_map);
440 self.file_fold_map.as_ref().unwrap()
441 }
442 #[cfg(feature = "dirstate-tree")]
443 pub fn build_file_fold_map(&mut self) -> &FileFoldMap {
444 if let Some(ref file_fold_map) = self.file_fold_map {
445 return file_fold_map;
446 }
447 let mut new_file_fold_map = FileFoldMap::default();
448
449 for (filename, DirstateEntry { state, .. }) in self.state_map.iter() {
450 if state == EntryState::Removed {
451 new_file_fold_map
452 .insert(normalize_case(&filename), filename.to_owned());
412 453 }
413 454 }
414 455 self.file_fold_map = Some(new_file_fold_map);
415 456 self.file_fold_map.as_ref().unwrap()
416 457 }
417 458 }
418 459
419 460 #[cfg(test)]
420 461 mod tests {
421 462 use super::*;
422 463
423 464 #[test]
424 465 fn test_dirs_multiset() {
425 466 let mut map = DirstateMap::new();
426 467 assert!(map.dirs.is_none());
427 468 assert!(map.all_dirs.is_none());
428 469
429 470 assert_eq!(map.has_dir(HgPath::new(b"nope")).unwrap(), false);
430 471 assert!(map.all_dirs.is_some());
431 472 assert!(map.dirs.is_none());
432 473
433 474 assert_eq!(map.has_tracked_dir(HgPath::new(b"nope")).unwrap(), false);
434 475 assert!(map.dirs.is_some());
435 476 }
436 477
437 478 #[test]
438 479 fn test_add_file() {
439 480 let mut map = DirstateMap::new();
440 481
441 482 assert_eq!(0, map.len());
442 483
443 484 map.add_file(
444 485 HgPath::new(b"meh"),
445 486 EntryState::Normal,
446 487 DirstateEntry {
447 488 state: EntryState::Normal,
448 489 mode: 1337,
449 490 mtime: 1337,
450 491 size: 1337,
451 492 },
452 493 )
453 494 .unwrap();
454 495
455 496 assert_eq!(1, map.len());
456 497 assert_eq!(0, map.get_non_normal_other_parent_entries().0.len());
457 498 assert_eq!(0, map.get_non_normal_other_parent_entries().1.len());
458 499 }
459 500
460 501 #[test]
461 502 fn test_non_normal_other_parent_entries() {
462 503 let mut map: DirstateMap = [
463 504 (b"f1", (EntryState::Removed, 1337, 1337, 1337)),
464 505 (b"f2", (EntryState::Normal, 1337, 1337, -1)),
465 506 (b"f3", (EntryState::Normal, 1337, 1337, 1337)),
466 507 (b"f4", (EntryState::Normal, 1337, -2, 1337)),
467 508 (b"f5", (EntryState::Added, 1337, 1337, 1337)),
468 509 (b"f6", (EntryState::Added, 1337, 1337, -1)),
469 510 (b"f7", (EntryState::Merged, 1337, 1337, -1)),
470 511 (b"f8", (EntryState::Merged, 1337, 1337, 1337)),
471 512 (b"f9", (EntryState::Merged, 1337, -2, 1337)),
472 513 (b"fa", (EntryState::Added, 1337, -2, 1337)),
473 514 (b"fb", (EntryState::Removed, 1337, -2, 1337)),
474 515 ]
475 516 .iter()
476 517 .map(|(fname, (state, mode, size, mtime))| {
477 518 (
478 519 HgPathBuf::from_bytes(fname.as_ref()),
479 520 DirstateEntry {
480 521 state: *state,
481 522 mode: *mode,
482 523 size: *size,
483 524 mtime: *mtime,
484 525 },
485 526 )
486 527 })
487 528 .collect();
488 529
489 530 let mut non_normal = [
490 531 b"f1", b"f2", b"f5", b"f6", b"f7", b"f8", b"f9", b"fa", b"fb",
491 532 ]
492 533 .iter()
493 534 .map(|x| HgPathBuf::from_bytes(x.as_ref()))
494 535 .collect();
495 536
496 537 let mut other_parent = HashSet::new();
497 538 other_parent.insert(HgPathBuf::from_bytes(b"f4"));
498 539 let entries = map.get_non_normal_other_parent_entries();
499 540
500 541 assert_eq!(
501 542 (&mut non_normal, &mut other_parent),
502 543 (entries.0, entries.1)
503 544 );
504 545 }
505 546 }
@@ -1,444 +1,518 b''
1 1 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
2 2 //
3 3 // This software may be used and distributed according to the terms of the
4 4 // GNU General Public License version 2 or any later version.
5 5
6 6 use crate::utils::hg_path::HgPath;
7 7 use crate::{
8 8 dirstate::{CopyMap, EntryState, StateMap},
9 9 DirstateEntry, DirstatePackError, DirstateParents, DirstateParseError,
10 10 };
11 11 use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
12 12 use micro_timer::timed;
13 13 use std::convert::{TryFrom, TryInto};
14 14 use std::io::Cursor;
15 15 use std::time::Duration;
16 16
17 17 /// Parents are stored in the dirstate as byte hashes.
18 18 pub const PARENT_SIZE: usize = 20;
19 19 /// Dirstate entries have a static part of 8 + 32 + 32 + 32 + 32 bits.
20 20 const MIN_ENTRY_SIZE: usize = 17;
21 21
22 22 type ParseResult<'a> = (
23 23 DirstateParents,
24 24 Vec<(&'a HgPath, DirstateEntry)>,
25 25 Vec<(&'a HgPath, &'a HgPath)>,
26 26 );
27 27
28 28 #[timed]
29 29 pub fn parse_dirstate(
30 30 contents: &[u8],
31 31 ) -> Result<ParseResult, DirstateParseError> {
32 32 if contents.len() < PARENT_SIZE * 2 {
33 33 return Err(DirstateParseError::TooLittleData);
34 34 }
35 35 let mut copies = vec![];
36 36 let mut entries = vec![];
37 37
38 38 let mut curr_pos = PARENT_SIZE * 2;
39 39 let parents = DirstateParents {
40 40 p1: contents[..PARENT_SIZE].try_into().unwrap(),
41 41 p2: contents[PARENT_SIZE..curr_pos].try_into().unwrap(),
42 42 };
43 43
44 44 while curr_pos < contents.len() {
45 45 if curr_pos + MIN_ENTRY_SIZE > contents.len() {
46 46 return Err(DirstateParseError::Overflow);
47 47 }
48 48 let entry_bytes = &contents[curr_pos..];
49 49
50 50 let mut cursor = Cursor::new(entry_bytes);
51 51 let state = EntryState::try_from(cursor.read_u8()?)?;
52 52 let mode = cursor.read_i32::<BigEndian>()?;
53 53 let size = cursor.read_i32::<BigEndian>()?;
54 54 let mtime = cursor.read_i32::<BigEndian>()?;
55 55 let path_len = cursor.read_i32::<BigEndian>()? as usize;
56 56
57 57 if path_len > contents.len() - curr_pos {
58 58 return Err(DirstateParseError::Overflow);
59 59 }
60 60
61 61 // Slice instead of allocating a Vec needed for `read_exact`
62 62 let path = &entry_bytes[MIN_ENTRY_SIZE..MIN_ENTRY_SIZE + (path_len)];
63 63
64 64 let (path, copy) = match memchr::memchr(0, path) {
65 65 None => (path, None),
66 66 Some(i) => (&path[..i], Some(&path[(i + 1)..])),
67 67 };
68 68
69 69 if let Some(copy_path) = copy {
70 70 copies.push((HgPath::new(path), HgPath::new(copy_path)));
71 71 };
72 72 entries.push((
73 73 HgPath::new(path),
74 74 DirstateEntry {
75 75 state,
76 76 mode,
77 77 size,
78 78 mtime,
79 79 },
80 80 ));
81 81 curr_pos = curr_pos + MIN_ENTRY_SIZE + (path_len);
82 82 }
83
84 83 Ok((parents, entries, copies))
85 84 }
86 85
87 86 /// `now` is the duration in seconds since the Unix epoch
87 #[cfg(not(feature = "dirstate-tree"))]
88 88 pub fn pack_dirstate(
89 89 state_map: &mut StateMap,
90 90 copy_map: &CopyMap,
91 91 parents: DirstateParents,
92 92 now: Duration,
93 93 ) -> Result<Vec<u8>, DirstatePackError> {
94 94 // TODO move away from i32 before 2038.
95 95 let now: i32 = now.as_secs().try_into().expect("time overflow");
96 96
97 97 let expected_size: usize = state_map
98 98 .iter()
99 99 .map(|(filename, _)| {
100 100 let mut length = MIN_ENTRY_SIZE + filename.len();
101 101 if let Some(copy) = copy_map.get(filename) {
102 102 length += copy.len() + 1;
103 103 }
104 104 length
105 105 })
106 106 .sum();
107 107 let expected_size = expected_size + PARENT_SIZE * 2;
108 108
109 109 let mut packed = Vec::with_capacity(expected_size);
110 110 let mut new_state_map = vec![];
111 111
112 112 packed.extend(&parents.p1);
113 113 packed.extend(&parents.p2);
114 114
115 115 for (filename, entry) in state_map.iter() {
116 116 let new_filename = filename.to_owned();
117 117 let mut new_mtime: i32 = entry.mtime;
118 118 if entry.state == EntryState::Normal && entry.mtime == now {
119 119 // The file was last modified "simultaneously" with the current
120 120 // write to dirstate (i.e. within the same second for file-
121 121 // systems with a granularity of 1 sec). This commonly happens
122 122 // for at least a couple of files on 'update'.
123 123 // The user could change the file without changing its size
124 124 // within the same second. Invalidate the file's mtime in
125 125 // dirstate, forcing future 'status' calls to compare the
126 126 // contents of the file if the size is the same. This prevents
127 127 // mistakenly treating such files as clean.
128 128 new_mtime = -1;
129 129 new_state_map.push((
130 130 filename.to_owned(),
131 131 DirstateEntry {
132 132 mtime: new_mtime,
133 133 ..*entry
134 134 },
135 135 ));
136 136 }
137 137 let mut new_filename = new_filename.into_vec();
138 138 if let Some(copy) = copy_map.get(filename) {
139 139 new_filename.push(b'\0');
140 140 new_filename.extend(copy.bytes());
141 141 }
142 142
143 143 packed.write_u8(entry.state.into())?;
144 144 packed.write_i32::<BigEndian>(entry.mode)?;
145 145 packed.write_i32::<BigEndian>(entry.size)?;
146 146 packed.write_i32::<BigEndian>(new_mtime)?;
147 147 packed.write_i32::<BigEndian>(new_filename.len() as i32)?;
148 148 packed.extend(new_filename)
149 149 }
150 150
151 151 if packed.len() != expected_size {
152 152 return Err(DirstatePackError::BadSize(expected_size, packed.len()));
153 153 }
154 154
155 155 state_map.extend(new_state_map);
156 156
157 157 Ok(packed)
158 158 }
159 /// `now` is the duration in seconds since the Unix epoch
160 #[cfg(feature = "dirstate-tree")]
161 pub fn pack_dirstate(
162 state_map: &mut StateMap,
163 copy_map: &CopyMap,
164 parents: DirstateParents,
165 now: Duration,
166 ) -> Result<Vec<u8>, DirstatePackError> {
167 // TODO move away from i32 before 2038.
168 let now: i32 = now.as_secs().try_into().expect("time overflow");
169
170 let expected_size: usize = state_map
171 .iter()
172 .map(|(filename, _)| {
173 let mut length = MIN_ENTRY_SIZE + filename.len();
174 if let Some(copy) = copy_map.get(&filename) {
175 length += copy.len() + 1;
176 }
177 length
178 })
179 .sum();
180 let expected_size = expected_size + PARENT_SIZE * 2;
181
182 let mut packed = Vec::with_capacity(expected_size);
183 let mut new_state_map = vec![];
184
185 packed.extend(&parents.p1);
186 packed.extend(&parents.p2);
187
188 for (filename, entry) in state_map.iter() {
189 let new_filename = filename.to_owned();
190 let mut new_mtime: i32 = entry.mtime;
191 if entry.state == EntryState::Normal && entry.mtime == now {
192 // The file was last modified "simultaneously" with the current
193 // write to dirstate (i.e. within the same second for file-
194 // systems with a granularity of 1 sec). This commonly happens
195 // for at least a couple of files on 'update'.
196 // The user could change the file without changing its size
197 // within the same second. Invalidate the file's mtime in
198 // dirstate, forcing future 'status' calls to compare the
199 // contents of the file if the size is the same. This prevents
200 // mistakenly treating such files as clean.
201 new_mtime = -1;
202 new_state_map.push((
203 filename.to_owned(),
204 DirstateEntry {
205 mtime: new_mtime,
206 ..entry
207 },
208 ));
209 }
210 let mut new_filename = new_filename.into_vec();
211 if let Some(copy) = copy_map.get(&filename) {
212 new_filename.push(b'\0');
213 new_filename.extend(copy.bytes());
214 }
215
216 packed.write_u8(entry.state.into())?;
217 packed.write_i32::<BigEndian>(entry.mode)?;
218 packed.write_i32::<BigEndian>(entry.size)?;
219 packed.write_i32::<BigEndian>(new_mtime)?;
220 packed.write_i32::<BigEndian>(new_filename.len() as i32)?;
221 packed.extend(new_filename)
222 }
223
224 if packed.len() != expected_size {
225 return Err(DirstatePackError::BadSize(expected_size, packed.len()));
226 }
227
228 state_map.extend(new_state_map);
229
230 Ok(packed)
231 }
159 232
160 233 #[cfg(test)]
161 234 mod tests {
162 235 use super::*;
163 236 use crate::{utils::hg_path::HgPathBuf, FastHashMap};
237 use pretty_assertions::assert_eq;
164 238
165 239 #[test]
166 240 fn test_pack_dirstate_empty() {
167 let mut state_map: StateMap = FastHashMap::default();
241 let mut state_map = StateMap::default();
168 242 let copymap = FastHashMap::default();
169 243 let parents = DirstateParents {
170 244 p1: *b"12345678910111213141",
171 245 p2: *b"00000000000000000000",
172 246 };
173 247 let now = Duration::new(15000000, 0);
174 248 let expected = b"1234567891011121314100000000000000000000".to_vec();
175 249
176 250 assert_eq!(
177 251 expected,
178 252 pack_dirstate(&mut state_map, &copymap, parents, now).unwrap()
179 253 );
180 254
181 255 assert!(state_map.is_empty())
182 256 }
183 257 #[test]
184 258 fn test_pack_dirstate_one_entry() {
185 259 let expected_state_map: StateMap = [(
186 260 HgPathBuf::from_bytes(b"f1"),
187 261 DirstateEntry {
188 262 state: EntryState::Normal,
189 263 mode: 0o644,
190 264 size: 0,
191 265 mtime: 791231220,
192 266 },
193 267 )]
194 268 .iter()
195 269 .cloned()
196 270 .collect();
197 271 let mut state_map = expected_state_map.clone();
198 272
199 273 let copymap = FastHashMap::default();
200 274 let parents = DirstateParents {
201 275 p1: *b"12345678910111213141",
202 276 p2: *b"00000000000000000000",
203 277 };
204 278 let now = Duration::new(15000000, 0);
205 279 let expected = [
206 280 49, 50, 51, 52, 53, 54, 55, 56, 57, 49, 48, 49, 49, 49, 50, 49,
207 281 51, 49, 52, 49, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48,
208 282 48, 48, 48, 48, 48, 48, 48, 48, 110, 0, 0, 1, 164, 0, 0, 0, 0, 47,
209 283 41, 58, 244, 0, 0, 0, 2, 102, 49,
210 284 ]
211 285 .to_vec();
212 286
213 287 assert_eq!(
214 288 expected,
215 289 pack_dirstate(&mut state_map, &copymap, parents, now).unwrap()
216 290 );
217 291
218 292 assert_eq!(expected_state_map, state_map);
219 293 }
220 294 #[test]
221 295 fn test_pack_dirstate_one_entry_with_copy() {
222 296 let expected_state_map: StateMap = [(
223 297 HgPathBuf::from_bytes(b"f1"),
224 298 DirstateEntry {
225 299 state: EntryState::Normal,
226 300 mode: 0o644,
227 301 size: 0,
228 302 mtime: 791231220,
229 303 },
230 304 )]
231 305 .iter()
232 306 .cloned()
233 307 .collect();
234 308 let mut state_map = expected_state_map.clone();
235 309 let mut copymap = FastHashMap::default();
236 310 copymap.insert(
237 311 HgPathBuf::from_bytes(b"f1"),
238 312 HgPathBuf::from_bytes(b"copyname"),
239 313 );
240 314 let parents = DirstateParents {
241 315 p1: *b"12345678910111213141",
242 316 p2: *b"00000000000000000000",
243 317 };
244 318 let now = Duration::new(15000000, 0);
245 319 let expected = [
246 320 49, 50, 51, 52, 53, 54, 55, 56, 57, 49, 48, 49, 49, 49, 50, 49,
247 321 51, 49, 52, 49, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48,
248 322 48, 48, 48, 48, 48, 48, 48, 48, 110, 0, 0, 1, 164, 0, 0, 0, 0, 47,
249 323 41, 58, 244, 0, 0, 0, 11, 102, 49, 0, 99, 111, 112, 121, 110, 97,
250 324 109, 101,
251 325 ]
252 326 .to_vec();
253 327
254 328 assert_eq!(
255 329 expected,
256 330 pack_dirstate(&mut state_map, &copymap, parents, now).unwrap()
257 331 );
258 332 assert_eq!(expected_state_map, state_map);
259 333 }
260 334
261 335 #[test]
262 336 fn test_parse_pack_one_entry_with_copy() {
263 337 let mut state_map: StateMap = [(
264 338 HgPathBuf::from_bytes(b"f1"),
265 339 DirstateEntry {
266 340 state: EntryState::Normal,
267 341 mode: 0o644,
268 342 size: 0,
269 343 mtime: 791231220,
270 344 },
271 345 )]
272 346 .iter()
273 347 .cloned()
274 348 .collect();
275 349 let mut copymap = FastHashMap::default();
276 350 copymap.insert(
277 351 HgPathBuf::from_bytes(b"f1"),
278 352 HgPathBuf::from_bytes(b"copyname"),
279 353 );
280 354 let parents = DirstateParents {
281 355 p1: *b"12345678910111213141",
282 356 p2: *b"00000000000000000000",
283 357 };
284 358 let now = Duration::new(15000000, 0);
285 359 let result =
286 360 pack_dirstate(&mut state_map, &copymap, parents.clone(), now)
287 361 .unwrap();
288 362
289 363 let (new_parents, entries, copies) =
290 364 parse_dirstate(result.as_slice()).unwrap();
291 365 let new_state_map: StateMap = entries
292 366 .into_iter()
293 367 .map(|(path, entry)| (path.to_owned(), entry))
294 368 .collect();
295 369 let new_copy_map: CopyMap = copies
296 370 .into_iter()
297 371 .map(|(path, copy)| (path.to_owned(), copy.to_owned()))
298 372 .collect();
299 373
300 374 assert_eq!(
301 375 (parents, state_map, copymap),
302 376 (new_parents, new_state_map, new_copy_map)
303 377 )
304 378 }
305 379
306 380 #[test]
307 381 fn test_parse_pack_multiple_entries_with_copy() {
308 382 let mut state_map: StateMap = [
309 383 (
310 384 HgPathBuf::from_bytes(b"f1"),
311 385 DirstateEntry {
312 386 state: EntryState::Normal,
313 387 mode: 0o644,
314 388 size: 0,
315 389 mtime: 791231220,
316 390 },
317 391 ),
318 392 (
319 393 HgPathBuf::from_bytes(b"f2"),
320 394 DirstateEntry {
321 395 state: EntryState::Merged,
322 396 mode: 0o777,
323 397 size: 1000,
324 398 mtime: 791231220,
325 399 },
326 400 ),
327 401 (
328 402 HgPathBuf::from_bytes(b"f3"),
329 403 DirstateEntry {
330 404 state: EntryState::Removed,
331 405 mode: 0o644,
332 406 size: 234553,
333 407 mtime: 791231220,
334 408 },
335 409 ),
336 410 (
337 411 HgPathBuf::from_bytes(b"f4\xF6"),
338 412 DirstateEntry {
339 413 state: EntryState::Added,
340 414 mode: 0o644,
341 415 size: -1,
342 416 mtime: -1,
343 417 },
344 418 ),
345 419 ]
346 420 .iter()
347 421 .cloned()
348 422 .collect();
349 423 let mut copymap = FastHashMap::default();
350 424 copymap.insert(
351 425 HgPathBuf::from_bytes(b"f1"),
352 426 HgPathBuf::from_bytes(b"copyname"),
353 427 );
354 428 copymap.insert(
355 429 HgPathBuf::from_bytes(b"f4\xF6"),
356 430 HgPathBuf::from_bytes(b"copyname2"),
357 431 );
358 432 let parents = DirstateParents {
359 433 p1: *b"12345678910111213141",
360 434 p2: *b"00000000000000000000",
361 435 };
362 436 let now = Duration::new(15000000, 0);
363 437 let result =
364 438 pack_dirstate(&mut state_map, &copymap, parents.clone(), now)
365 439 .unwrap();
366 440
367 441 let (new_parents, entries, copies) =
368 442 parse_dirstate(result.as_slice()).unwrap();
369 443 let new_state_map: StateMap = entries
370 444 .into_iter()
371 445 .map(|(path, entry)| (path.to_owned(), entry))
372 446 .collect();
373 447 let new_copy_map: CopyMap = copies
374 448 .into_iter()
375 449 .map(|(path, copy)| (path.to_owned(), copy.to_owned()))
376 450 .collect();
377 451
378 452 assert_eq!(
379 453 (parents, state_map, copymap),
380 454 (new_parents, new_state_map, new_copy_map)
381 455 )
382 456 }
383 457
384 458 #[test]
385 459 /// https://www.mercurial-scm.org/repo/hg/rev/af3f26b6bba4
386 460 fn test_parse_pack_one_entry_with_copy_and_time_conflict() {
387 461 let mut state_map: StateMap = [(
388 462 HgPathBuf::from_bytes(b"f1"),
389 463 DirstateEntry {
390 464 state: EntryState::Normal,
391 465 mode: 0o644,
392 466 size: 0,
393 467 mtime: 15000000,
394 468 },
395 469 )]
396 470 .iter()
397 471 .cloned()
398 472 .collect();
399 473 let mut copymap = FastHashMap::default();
400 474 copymap.insert(
401 475 HgPathBuf::from_bytes(b"f1"),
402 476 HgPathBuf::from_bytes(b"copyname"),
403 477 );
404 478 let parents = DirstateParents {
405 479 p1: *b"12345678910111213141",
406 480 p2: *b"00000000000000000000",
407 481 };
408 482 let now = Duration::new(15000000, 0);
409 483 let result =
410 484 pack_dirstate(&mut state_map, &copymap, parents.clone(), now)
411 485 .unwrap();
412 486
413 487 let (new_parents, entries, copies) =
414 488 parse_dirstate(result.as_slice()).unwrap();
415 489 let new_state_map: StateMap = entries
416 490 .into_iter()
417 491 .map(|(path, entry)| (path.to_owned(), entry))
418 492 .collect();
419 493 let new_copy_map: CopyMap = copies
420 494 .into_iter()
421 495 .map(|(path, copy)| (path.to_owned(), copy.to_owned()))
422 496 .collect();
423 497
424 498 assert_eq!(
425 499 (
426 500 parents,
427 501 [(
428 502 HgPathBuf::from_bytes(b"f1"),
429 503 DirstateEntry {
430 504 state: EntryState::Normal,
431 505 mode: 0o644,
432 506 size: 0,
433 507 mtime: -1
434 508 }
435 509 )]
436 510 .iter()
437 511 .cloned()
438 512 .collect::<StateMap>(),
439 513 copymap,
440 514 ),
441 515 (new_parents, new_state_map, new_copy_map)
442 516 )
443 517 }
444 518 }
@@ -1,906 +1,975 b''
1 1 // status.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 //! Rust implementation of dirstate.status (dirstate.py).
9 9 //! It is currently missing a lot of functionality compared to the Python one
10 10 //! and will only be triggered in narrow cases.
11 11
12 #[cfg(feature = "dirstate-tree")]
13 use crate::dirstate::dirstate_tree::iter::StatusShortcut;
14 #[cfg(not(feature = "dirstate-tree"))]
15 use crate::utils::path_auditor::PathAuditor;
12 16 use crate::{
13 17 dirstate::SIZE_FROM_OTHER_PARENT,
14 18 filepatterns::PatternFileWarning,
15 19 matchers::{get_ignore_function, Matcher, VisitChildrenSet},
16 20 utils::{
17 21 files::{find_dirs, HgMetadata},
18 22 hg_path::{
19 23 hg_path_to_path_buf, os_string_to_hg_path_buf, HgPath, HgPathBuf,
20 24 HgPathError,
21 25 },
22 path_auditor::PathAuditor,
23 26 },
24 27 CopyMap, DirstateEntry, DirstateMap, EntryState, FastHashMap,
25 28 PatternError,
26 29 };
27 30 use lazy_static::lazy_static;
28 31 use micro_timer::timed;
29 32 use rayon::prelude::*;
30 33 use std::{
31 34 borrow::Cow,
32 35 collections::HashSet,
33 36 fs::{read_dir, DirEntry},
34 37 io::ErrorKind,
35 38 ops::Deref,
36 39 path::{Path, PathBuf},
37 40 };
38 41
39 42 /// Wrong type of file from a `BadMatch`
40 43 /// Note: a lot of those don't exist on all platforms.
41 44 #[derive(Debug, Copy, Clone)]
42 45 pub enum BadType {
43 46 CharacterDevice,
44 47 BlockDevice,
45 48 FIFO,
46 49 Socket,
47 50 Directory,
48 51 Unknown,
49 52 }
50 53
51 54 impl ToString for BadType {
52 55 fn to_string(&self) -> String {
53 56 match self {
54 57 BadType::CharacterDevice => "character device",
55 58 BadType::BlockDevice => "block device",
56 59 BadType::FIFO => "fifo",
57 60 BadType::Socket => "socket",
58 61 BadType::Directory => "directory",
59 62 BadType::Unknown => "unknown",
60 63 }
61 64 .to_string()
62 65 }
63 66 }
64 67
65 68 /// Was explicitly matched but cannot be found/accessed
66 69 #[derive(Debug, Copy, Clone)]
67 70 pub enum BadMatch {
68 71 OsError(i32),
69 72 BadType(BadType),
70 73 }
71 74
72 75 /// Enum used to dispatch new status entries into the right collections.
73 76 /// Is similar to `crate::EntryState`, but represents the transient state of
74 77 /// entries during the lifetime of a command.
75 78 #[derive(Debug, Copy, Clone)]
76 79 pub enum Dispatch {
77 80 Unsure,
78 81 Modified,
79 82 Added,
80 83 Removed,
81 84 Deleted,
82 85 Clean,
83 86 Unknown,
84 87 Ignored,
85 88 /// Empty dispatch, the file is not worth listing
86 89 None,
87 90 /// Was explicitly matched but cannot be found/accessed
88 91 Bad(BadMatch),
89 92 Directory {
90 93 /// True if the directory used to be a file in the dmap so we can say
91 94 /// that it's been removed.
92 95 was_file: bool,
93 96 },
94 97 }
95 98
96 99 type IoResult<T> = std::io::Result<T>;
97 100
98 101 /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait, 'static>`, so add
99 102 /// an explicit lifetime here to not fight `'static` bounds "out of nowhere".
100 103 type IgnoreFnType<'a> = Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>;
101 104
102 105 /// We have a good mix of owned (from directory traversal) and borrowed (from
103 106 /// the dirstate/explicit) paths, this comes up a lot.
104 107 pub type HgPathCow<'a> = Cow<'a, HgPath>;
105 108
106 109 /// A path with its computed ``Dispatch`` information
107 110 type DispatchedPath<'a> = (HgPathCow<'a>, Dispatch);
108 111
109 112 /// Dates and times that are outside the 31-bit signed range are compared
110 113 /// modulo 2^31. This should prevent hg from behaving badly with very large
111 114 /// files or corrupt dates while still having a high probability of detecting
112 115 /// changes. (issue2608)
113 116 /// TODO I haven't found a way of having `b` be `Into<i32>`, since `From<u64>`
114 117 /// is not defined for `i32`, and there is no `As` trait. This forces the
115 118 /// caller to cast `b` as `i32`.
116 119 fn mod_compare(a: i32, b: i32) -> bool {
117 120 a & i32::max_value() != b & i32::max_value()
118 121 }
119 122
120 123 /// Return a sorted list containing information about the entries
121 124 /// in the directory.
122 125 ///
123 126 /// * `skip_dot_hg` - Return an empty vec if `path` contains a `.hg` directory
124 127 fn list_directory(
125 128 path: impl AsRef<Path>,
126 129 skip_dot_hg: bool,
127 130 ) -> std::io::Result<Vec<(HgPathBuf, DirEntry)>> {
128 131 let mut results = vec![];
129 132 let entries = read_dir(path.as_ref())?;
130 133
131 134 for entry in entries {
132 135 let entry = entry?;
133 136 let filename = os_string_to_hg_path_buf(entry.file_name())?;
134 137 let file_type = entry.file_type()?;
135 138 if skip_dot_hg && filename.as_bytes() == b".hg" && file_type.is_dir() {
136 139 return Ok(vec![]);
137 140 } else {
138 141 results.push((filename, entry))
139 142 }
140 143 }
141 144
142 145 results.sort_unstable_by_key(|e| e.0.clone());
143 146 Ok(results)
144 147 }
145 148
146 149 /// The file corresponding to the dirstate entry was found on the filesystem.
147 150 fn dispatch_found(
148 151 filename: impl AsRef<HgPath>,
149 152 entry: DirstateEntry,
150 153 metadata: HgMetadata,
151 154 copy_map: &CopyMap,
152 155 options: StatusOptions,
153 156 ) -> Dispatch {
154 157 let DirstateEntry {
155 158 state,
156 159 mode,
157 160 mtime,
158 161 size,
159 162 } = entry;
160 163
161 164 let HgMetadata {
162 165 st_mode,
163 166 st_size,
164 167 st_mtime,
165 168 ..
166 169 } = metadata;
167 170
168 171 match state {
169 172 EntryState::Normal => {
170 173 let size_changed = mod_compare(size, st_size as i32);
171 174 let mode_changed =
172 175 (mode ^ st_mode as i32) & 0o100 != 0o000 && options.check_exec;
173 176 let metadata_changed = size >= 0 && (size_changed || mode_changed);
174 177 let other_parent = size == SIZE_FROM_OTHER_PARENT;
175 178
176 179 if metadata_changed
177 180 || other_parent
178 181 || copy_map.contains_key(filename.as_ref())
179 182 {
180 183 Dispatch::Modified
181 184 } else if mod_compare(mtime, st_mtime as i32)
182 185 || st_mtime == options.last_normal_time
183 186 {
184 187 // the file may have just been marked as normal and
185 188 // it may have changed in the same second without
186 189 // changing its size. This can happen if we quickly
187 190 // do multiple commits. Force lookup, so we don't
188 191 // miss such a racy file change.
189 192 Dispatch::Unsure
190 193 } else if options.list_clean {
191 194 Dispatch::Clean
192 195 } else {
193 196 Dispatch::None
194 197 }
195 198 }
196 199 EntryState::Merged => Dispatch::Modified,
197 200 EntryState::Added => Dispatch::Added,
198 201 EntryState::Removed => Dispatch::Removed,
199 202 EntryState::Unknown => Dispatch::Unknown,
200 203 }
201 204 }
202 205
203 206 /// The file corresponding to this Dirstate entry is missing.
204 207 fn dispatch_missing(state: EntryState) -> Dispatch {
205 208 match state {
206 209 // File was removed from the filesystem during commands
207 210 EntryState::Normal | EntryState::Merged | EntryState::Added => {
208 211 Dispatch::Deleted
209 212 }
210 213 // File was removed, everything is normal
211 214 EntryState::Removed => Dispatch::Removed,
212 215 // File is unknown to Mercurial, everything is normal
213 216 EntryState::Unknown => Dispatch::Unknown,
214 217 }
215 218 }
216 219
217 220 lazy_static! {
218 221 static ref DEFAULT_WORK: HashSet<&'static HgPath> = {
219 222 let mut h = HashSet::new();
220 223 h.insert(HgPath::new(b""));
221 224 h
222 225 };
223 226 }
224 227
225 228 #[derive(Debug, Copy, Clone)]
226 229 pub struct StatusOptions {
227 230 /// Remember the most recent modification timeslot for status, to make
228 231 /// sure we won't miss future size-preserving file content modifications
229 232 /// that happen within the same timeslot.
230 233 pub last_normal_time: i64,
231 234 /// Whether we are on a filesystem with UNIX-like exec flags
232 235 pub check_exec: bool,
233 236 pub list_clean: bool,
234 237 pub list_unknown: bool,
235 238 pub list_ignored: bool,
236 239 /// Whether to collect traversed dirs for applying a callback later.
237 240 /// Used by `hg purge` for example.
238 241 pub collect_traversed_dirs: bool,
239 242 }
240 243
241 244 #[derive(Debug)]
242 245 pub struct DirstateStatus<'a> {
243 246 pub modified: Vec<HgPathCow<'a>>,
244 247 pub added: Vec<HgPathCow<'a>>,
245 248 pub removed: Vec<HgPathCow<'a>>,
246 249 pub deleted: Vec<HgPathCow<'a>>,
247 250 pub clean: Vec<HgPathCow<'a>>,
248 251 pub ignored: Vec<HgPathCow<'a>>,
249 252 pub unknown: Vec<HgPathCow<'a>>,
250 253 pub bad: Vec<(HgPathCow<'a>, BadMatch)>,
251 254 /// Only filled if `collect_traversed_dirs` is `true`
252 255 pub traversed: Vec<HgPathBuf>,
253 256 }
254 257
255 258 #[derive(Debug)]
256 259 pub enum StatusError {
257 260 /// Generic IO error
258 261 IO(std::io::Error),
259 262 /// An invalid path that cannot be represented in Mercurial was found
260 263 Path(HgPathError),
261 264 /// An invalid "ignore" pattern was found
262 265 Pattern(PatternError),
263 266 }
264 267
265 268 pub type StatusResult<T> = Result<T, StatusError>;
266 269
267 270 impl From<PatternError> for StatusError {
268 271 fn from(e: PatternError) -> Self {
269 272 StatusError::Pattern(e)
270 273 }
271 274 }
272 275 impl From<HgPathError> for StatusError {
273 276 fn from(e: HgPathError) -> Self {
274 277 StatusError::Path(e)
275 278 }
276 279 }
277 280 impl From<std::io::Error> for StatusError {
278 281 fn from(e: std::io::Error) -> Self {
279 282 StatusError::IO(e)
280 283 }
281 284 }
282 285
283 286 impl ToString for StatusError {
284 287 fn to_string(&self) -> String {
285 288 match self {
286 289 StatusError::IO(e) => e.to_string(),
287 290 StatusError::Path(e) => e.to_string(),
288 291 StatusError::Pattern(e) => e.to_string(),
289 292 }
290 293 }
291 294 }
292 295
293 296 /// Gives information about which files are changed in the working directory
294 297 /// and how, compared to the revision we're based on
295 298 pub struct Status<'a, M: Matcher + Sync> {
296 299 dmap: &'a DirstateMap,
297 300 pub(crate) matcher: &'a M,
298 301 root_dir: PathBuf,
299 302 pub(crate) options: StatusOptions,
300 303 ignore_fn: IgnoreFnType<'a>,
301 304 }
302 305
303 306 impl<'a, M> Status<'a, M>
304 307 where
305 308 M: Matcher + Sync,
306 309 {
307 310 pub fn new(
308 311 dmap: &'a DirstateMap,
309 312 matcher: &'a M,
310 313 root_dir: PathBuf,
311 314 ignore_files: Vec<PathBuf>,
312 315 options: StatusOptions,
313 316 ) -> StatusResult<(Self, Vec<PatternFileWarning>)> {
314 317 // Needs to outlive `dir_ignore_fn` since it's captured.
315 318
316 319 let (ignore_fn, warnings): (IgnoreFnType, _) =
317 320 if options.list_ignored || options.list_unknown {
318 321 get_ignore_function(ignore_files, &root_dir)?
319 322 } else {
320 323 (Box::new(|&_| true), vec![])
321 324 };
322 325
323 326 Ok((
324 327 Self {
325 328 dmap,
326 329 matcher,
327 330 root_dir,
328 331 options,
329 332 ignore_fn,
330 333 },
331 334 warnings,
332 335 ))
333 336 }
334 337
335 338 /// Is the path ignored?
336 339 pub fn is_ignored(&self, path: impl AsRef<HgPath>) -> bool {
337 340 (self.ignore_fn)(path.as_ref())
338 341 }
339 342
340 343 /// Is the path or one of its ancestors ignored?
341 344 pub fn dir_ignore(&self, dir: impl AsRef<HgPath>) -> bool {
342 345 // Only involve ignore mechanism if we're listing unknowns or ignored.
343 346 if self.options.list_ignored || self.options.list_unknown {
344 347 if self.is_ignored(&dir) {
345 348 true
346 349 } else {
347 350 for p in find_dirs(dir.as_ref()) {
348 351 if self.is_ignored(p) {
349 352 return true;
350 353 }
351 354 }
352 355 false
353 356 }
354 357 } else {
355 358 true
356 359 }
357 360 }
358 361
359 362 /// Get stat data about the files explicitly specified by the matcher.
360 363 /// Returns a tuple of the directories that need to be traversed and the
361 364 /// files with their corresponding `Dispatch`.
362 365 /// TODO subrepos
363 366 #[timed]
364 367 pub fn walk_explicit(
365 368 &self,
366 369 traversed_sender: crossbeam::Sender<HgPathBuf>,
367 370 ) -> (Vec<DispatchedPath<'a>>, Vec<DispatchedPath<'a>>) {
368 371 self.matcher
369 372 .file_set()
370 373 .unwrap_or(&DEFAULT_WORK)
371 374 .par_iter()
372 375 .map(|&filename| -> Option<IoResult<_>> {
373 376 // TODO normalization
374 377 let normalized = filename;
375 378
376 379 let buf = match hg_path_to_path_buf(normalized) {
377 380 Ok(x) => x,
378 381 Err(e) => return Some(Err(e.into())),
379 382 };
380 383 let target = self.root_dir.join(buf);
381 384 let st = target.symlink_metadata();
382 385 let in_dmap = self.dmap.get(normalized);
383 386 match st {
384 387 Ok(meta) => {
385 388 let file_type = meta.file_type();
386 389 return if file_type.is_file() || file_type.is_symlink()
387 390 {
388 391 if let Some(entry) = in_dmap {
389 392 return Some(Ok((
390 393 Cow::Borrowed(normalized),
391 394 dispatch_found(
392 395 &normalized,
393 396 *entry,
394 397 HgMetadata::from_metadata(meta),
395 398 &self.dmap.copy_map,
396 399 self.options,
397 400 ),
398 401 )));
399 402 }
400 403 Some(Ok((
401 404 Cow::Borrowed(normalized),
402 405 Dispatch::Unknown,
403 406 )))
404 407 } else if file_type.is_dir() {
405 408 if self.options.collect_traversed_dirs {
406 409 traversed_sender
407 410 .send(normalized.to_owned())
408 411 .expect("receiver should outlive sender");
409 412 }
410 413 Some(Ok((
411 414 Cow::Borrowed(normalized),
412 415 Dispatch::Directory {
413 416 was_file: in_dmap.is_some(),
414 417 },
415 418 )))
416 419 } else {
417 420 Some(Ok((
418 421 Cow::Borrowed(normalized),
419 422 Dispatch::Bad(BadMatch::BadType(
420 423 // TODO do more than unknown
421 424 // Support for all `BadType` variant
422 425 // varies greatly between platforms.
423 426 // So far, no tests check the type and
424 427 // this should be good enough for most
425 428 // users.
426 429 BadType::Unknown,
427 430 )),
428 431 )))
429 432 };
430 433 }
431 434 Err(_) => {
432 435 if let Some(entry) = in_dmap {
433 436 return Some(Ok((
434 437 Cow::Borrowed(normalized),
435 438 dispatch_missing(entry.state),
436 439 )));
437 440 }
438 441 }
439 442 };
440 443 None
441 444 })
442 445 .flatten()
443 446 .filter_map(Result::ok)
444 447 .partition(|(_, dispatch)| match dispatch {
445 448 Dispatch::Directory { .. } => true,
446 449 _ => false,
447 450 })
448 451 }
449 452
450 453 /// Walk the working directory recursively to look for changes compared to
451 454 /// the current `DirstateMap`.
452 455 ///
453 456 /// This takes a mutable reference to the results to account for the
454 457 /// `extend` in timings
455 458 #[timed]
456 459 pub fn traverse(
457 460 &self,
458 461 path: impl AsRef<HgPath>,
459 462 old_results: &FastHashMap<HgPathCow<'a>, Dispatch>,
460 463 results: &mut Vec<DispatchedPath<'a>>,
461 464 traversed_sender: crossbeam::Sender<HgPathBuf>,
462 465 ) -> IoResult<()> {
463 466 // The traversal is done in parallel, so use a channel to gather
464 467 // entries. `crossbeam::Sender` is `Sync`, while `mpsc::Sender`
465 468 // is not.
466 469 let (files_transmitter, files_receiver) =
467 470 crossbeam::channel::unbounded();
468 471
469 472 self.traverse_dir(
470 473 &files_transmitter,
471 474 path,
472 475 &old_results,
473 476 traversed_sender,
474 477 )?;
475 478
476 479 // Disconnect the channel so the receiver stops waiting
477 480 drop(files_transmitter);
478 481
479 482 // TODO don't collect. Find a way of replicating the behavior of
480 483 // `itertools::process_results`, but for `rayon::ParallelIterator`
481 484 let new_results: IoResult<Vec<(Cow<HgPath>, Dispatch)>> =
482 485 files_receiver
483 486 .into_iter()
484 487 .map(|item| {
485 488 let (f, d) = item?;
486 489 Ok((Cow::Owned(f), d))
487 490 })
488 491 .collect();
489 492
490 493 results.par_extend(new_results?);
491 494
492 495 Ok(())
493 496 }
494 497
495 498 /// Dispatch a single entry (file, folder, symlink...) found during
496 499 /// `traverse`. If the entry is a folder that needs to be traversed, it
497 500 /// will be handled in a separate thread.
498 501 fn handle_traversed_entry<'b>(
499 502 &'a self,
500 503 scope: &rayon::Scope<'b>,
501 504 files_sender: &'b crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
502 505 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
503 506 filename: HgPathBuf,
504 507 dir_entry: DirEntry,
505 508 traversed_sender: crossbeam::Sender<HgPathBuf>,
506 509 ) -> IoResult<()>
507 510 where
508 511 'a: 'b,
509 512 {
510 513 let file_type = dir_entry.file_type()?;
511 514 let entry_option = self.dmap.get(&filename);
512 515
513 516 if filename.as_bytes() == b".hg" {
514 517 // Could be a directory or a symlink
515 518 return Ok(());
516 519 }
517 520
518 521 if file_type.is_dir() {
519 522 self.handle_traversed_dir(
520 523 scope,
521 524 files_sender,
522 525 old_results,
523 526 entry_option,
524 527 filename,
525 528 traversed_sender,
526 529 );
527 530 } else if file_type.is_file() || file_type.is_symlink() {
528 531 if let Some(entry) = entry_option {
529 532 if self.matcher.matches_everything()
530 533 || self.matcher.matches(&filename)
531 534 {
532 535 let metadata = dir_entry.metadata()?;
533 536 files_sender
534 537 .send(Ok((
535 538 filename.to_owned(),
536 539 dispatch_found(
537 540 &filename,
538 541 *entry,
539 542 HgMetadata::from_metadata(metadata),
540 543 &self.dmap.copy_map,
541 544 self.options,
542 545 ),
543 546 )))
544 547 .unwrap();
545 548 }
546 549 } else if (self.matcher.matches_everything()
547 550 || self.matcher.matches(&filename))
548 551 && !self.is_ignored(&filename)
549 552 {
550 553 if (self.options.list_ignored
551 554 || self.matcher.exact_match(&filename))
552 555 && self.dir_ignore(&filename)
553 556 {
554 557 if self.options.list_ignored {
555 558 files_sender
556 559 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
557 560 .unwrap();
558 561 }
559 562 } else if self.options.list_unknown {
560 563 files_sender
561 564 .send(Ok((filename.to_owned(), Dispatch::Unknown)))
562 565 .unwrap();
563 566 }
564 567 } else if self.is_ignored(&filename) && self.options.list_ignored {
565 568 files_sender
566 569 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
567 570 .unwrap();
568 571 }
569 572 } else if let Some(entry) = entry_option {
570 573 // Used to be a file or a folder, now something else.
571 574 if self.matcher.matches_everything()
572 575 || self.matcher.matches(&filename)
573 576 {
574 577 files_sender
575 578 .send(Ok((
576 579 filename.to_owned(),
577 580 dispatch_missing(entry.state),
578 581 )))
579 582 .unwrap();
580 583 }
581 584 }
582 585
583 586 Ok(())
584 587 }
585 588
586 589 /// A directory was found in the filesystem and needs to be traversed
587 590 fn handle_traversed_dir<'b>(
588 591 &'a self,
589 592 scope: &rayon::Scope<'b>,
590 593 files_sender: &'b crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
591 594 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
592 595 entry_option: Option<&'a DirstateEntry>,
593 596 directory: HgPathBuf,
594 597 traversed_sender: crossbeam::Sender<HgPathBuf>,
595 598 ) where
596 599 'a: 'b,
597 600 {
598 601 scope.spawn(move |_| {
599 602 // Nested `if` until `rust-lang/rust#53668` is stable
600 603 if let Some(entry) = entry_option {
601 604 // Used to be a file, is now a folder
602 605 if self.matcher.matches_everything()
603 606 || self.matcher.matches(&directory)
604 607 {
605 608 files_sender
606 609 .send(Ok((
607 610 directory.to_owned(),
608 611 dispatch_missing(entry.state),
609 612 )))
610 613 .unwrap();
611 614 }
612 615 }
613 616 // Do we need to traverse it?
614 617 if !self.is_ignored(&directory) || self.options.list_ignored {
615 618 self.traverse_dir(
616 619 files_sender,
617 620 directory,
618 621 &old_results,
619 622 traversed_sender,
620 623 )
621 624 .unwrap_or_else(|e| files_sender.send(Err(e)).unwrap())
622 625 }
623 626 });
624 627 }
625 628
626 629 /// Decides whether the directory needs to be listed, and if so handles the
627 630 /// entries in a separate thread.
628 631 fn traverse_dir(
629 632 &self,
630 633 files_sender: &crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
631 634 directory: impl AsRef<HgPath>,
632 635 old_results: &FastHashMap<Cow<HgPath>, Dispatch>,
633 636 traversed_sender: crossbeam::Sender<HgPathBuf>,
634 637 ) -> IoResult<()> {
635 638 let directory = directory.as_ref();
636 639
637 640 if self.options.collect_traversed_dirs {
638 641 traversed_sender
639 642 .send(directory.to_owned())
640 643 .expect("receiver should outlive sender");
641 644 }
642 645
643 646 let visit_entries = match self.matcher.visit_children_set(directory) {
644 647 VisitChildrenSet::Empty => return Ok(()),
645 648 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
646 649 VisitChildrenSet::Set(set) => Some(set),
647 650 };
648 651 let buf = hg_path_to_path_buf(directory)?;
649 652 let dir_path = self.root_dir.join(buf);
650 653
651 654 let skip_dot_hg = !directory.as_bytes().is_empty();
652 655 let entries = match list_directory(dir_path, skip_dot_hg) {
653 656 Err(e) => {
654 657 return match e.kind() {
655 658 ErrorKind::NotFound | ErrorKind::PermissionDenied => {
656 659 files_sender
657 660 .send(Ok((
658 661 directory.to_owned(),
659 662 Dispatch::Bad(BadMatch::OsError(
660 663 // Unwrapping here is OK because the error
661 664 // always is a
662 665 // real os error
663 666 e.raw_os_error().unwrap(),
664 667 )),
665 668 )))
666 669 .expect("receiver should outlive sender");
667 670 Ok(())
668 671 }
669 672 _ => Err(e),
670 673 };
671 674 }
672 675 Ok(entries) => entries,
673 676 };
674 677
675 678 rayon::scope(|scope| -> IoResult<()> {
676 679 for (filename, dir_entry) in entries {
677 680 if let Some(ref set) = visit_entries {
678 681 if !set.contains(filename.deref()) {
679 682 continue;
680 683 }
681 684 }
682 685 // TODO normalize
683 686 let filename = if directory.is_empty() {
684 687 filename.to_owned()
685 688 } else {
686 689 directory.join(&filename)
687 690 };
688 691
689 692 if !old_results.contains_key(filename.deref()) {
690 693 self.handle_traversed_entry(
691 694 scope,
692 695 files_sender,
693 696 old_results,
694 697 filename,
695 698 dir_entry,
696 699 traversed_sender.clone(),
697 700 )?;
698 701 }
699 702 }
700 703 Ok(())
701 704 })
702 705 }
703 706
707 /// Add the files in the dirstate to the results.
708 ///
709 /// This takes a mutable reference to the results to account for the
710 /// `extend` in timings
711 #[cfg(feature = "dirstate-tree")]
712 #[timed]
713 pub fn extend_from_dmap(&self, results: &mut Vec<DispatchedPath<'a>>) {
714 results.par_extend(
715 self.dmap
716 .fs_iter(self.root_dir.clone())
717 .par_bridge()
718 .filter(|(path, _)| self.matcher.matches(path))
719 .flat_map(move |(filename, shortcut)| {
720 let entry = match shortcut {
721 StatusShortcut::Entry(e) => e,
722 StatusShortcut::Dispatch(d) => {
723 return Ok((Cow::Owned(filename), d))
724 }
725 };
726 let filename_as_path = hg_path_to_path_buf(&filename)?;
727 let meta = self
728 .root_dir
729 .join(filename_as_path)
730 .symlink_metadata();
731
732 match meta {
733 Ok(ref m)
734 if !(m.file_type().is_file()
735 || m.file_type().is_symlink()) =>
736 {
737 Ok((
738 Cow::Owned(filename),
739 dispatch_missing(entry.state),
740 ))
741 }
742 Ok(m) => {
743 let dispatch = dispatch_found(
744 &filename,
745 entry,
746 HgMetadata::from_metadata(m),
747 &self.dmap.copy_map,
748 self.options,
749 );
750 Ok((Cow::Owned(filename), dispatch))
751 }
752 Err(ref e)
753 if e.kind() == ErrorKind::NotFound
754 || e.raw_os_error() == Some(20) =>
755 {
756 // Rust does not yet have an `ErrorKind` for
757 // `NotADirectory` (errno 20)
758 // It happens if the dirstate contains `foo/bar`
759 // and foo is not a
760 // directory
761 Ok((
762 Cow::Owned(filename),
763 dispatch_missing(entry.state),
764 ))
765 }
766 Err(e) => Err(e),
767 }
768 }),
769 );
770 }
771
772 /// Add the files in the dirstate to the results.
773 ///
774 /// This takes a mutable reference to the results to account for the
775 /// `extend` in timings
776 #[cfg(not(feature = "dirstate-tree"))]
777 #[timed]
778 pub fn extend_from_dmap(&self, results: &mut Vec<DispatchedPath<'a>>) {
779 results.par_extend(self.dmap.par_iter().flat_map(
780 move |(filename, entry)| {
781 let filename: &HgPath = filename;
782 let filename_as_path = hg_path_to_path_buf(filename)?;
783 let meta =
784 self.root_dir.join(filename_as_path).symlink_metadata();
785 match meta {
786 Ok(ref m)
787 if !(m.file_type().is_file()
788 || m.file_type().is_symlink()) =>
789 {
790 Ok((
791 Cow::Borrowed(filename),
792 dispatch_missing(entry.state),
793 ))
794 }
795 Ok(m) => Ok((
796 Cow::Borrowed(filename),
797 dispatch_found(
798 filename,
799 *entry,
800 HgMetadata::from_metadata(m),
801 &self.dmap.copy_map,
802 self.options,
803 ),
804 )),
805 Err(ref e)
806 if e.kind() == ErrorKind::NotFound
807 || e.raw_os_error() == Some(20) =>
808 {
809 // Rust does not yet have an `ErrorKind` for
810 // `NotADirectory` (errno 20)
811 // It happens if the dirstate contains `foo/bar`
812 // and foo is not a
813 // directory
814 Ok((
815 Cow::Borrowed(filename),
816 dispatch_missing(entry.state),
817 ))
818 }
819 Err(e) => Err(e),
820 }
821 },
822 ));
823 }
824
704 825 /// Checks all files that are in the dirstate but were not found during the
705 826 /// working directory traversal. This means that the rest must
706 827 /// be either ignored, under a symlink or under a new nested repo.
707 828 ///
708 829 /// This takes a mutable reference to the results to account for the
709 830 /// `extend` in timings
831 #[cfg(not(feature = "dirstate-tree"))]
710 832 #[timed]
711 833 pub fn handle_unknowns(
712 834 &self,
713 835 results: &mut Vec<DispatchedPath<'a>>,
714 836 ) -> IoResult<()> {
715 837 let to_visit: Vec<(&HgPath, &DirstateEntry)> =
716 838 if results.is_empty() && self.matcher.matches_everything() {
717 839 self.dmap.iter().map(|(f, e)| (f.deref(), e)).collect()
718 840 } else {
719 841 // Only convert to a hashmap if needed.
720 842 let old_results: FastHashMap<_, _> =
721 843 results.iter().cloned().collect();
722 844 self.dmap
723 845 .iter()
724 846 .filter_map(move |(f, e)| {
725 847 if !old_results.contains_key(f.deref())
726 848 && self.matcher.matches(f)
727 849 {
728 850 Some((f.deref(), e))
729 851 } else {
730 852 None
731 853 }
732 854 })
733 855 .collect()
734 856 };
735 857
736 858 let path_auditor = PathAuditor::new(&self.root_dir);
737 859
738 860 // TODO don't collect. Find a way of replicating the behavior of
739 861 // `itertools::process_results`, but for `rayon::ParallelIterator`
740 862 let new_results: IoResult<Vec<_>> = to_visit
741 863 .into_par_iter()
742 864 .filter_map(|(filename, entry)| -> Option<IoResult<_>> {
743 865 // Report ignored items in the dmap as long as they are not
744 866 // under a symlink directory.
745 867 if path_auditor.check(filename) {
746 868 // TODO normalize for case-insensitive filesystems
747 869 let buf = match hg_path_to_path_buf(filename) {
748 870 Ok(x) => x,
749 871 Err(e) => return Some(Err(e.into())),
750 872 };
751 873 Some(Ok((
752 874 Cow::Borrowed(filename),
753 875 match self.root_dir.join(&buf).symlink_metadata() {
754 876 // File was just ignored, no links, and exists
755 877 Ok(meta) => {
756 878 let metadata = HgMetadata::from_metadata(meta);
757 879 dispatch_found(
758 880 filename,
759 881 *entry,
760 882 metadata,
761 883 &self.dmap.copy_map,
762 884 self.options,
763 885 )
764 886 }
765 887 // File doesn't exist
766 888 Err(_) => dispatch_missing(entry.state),
767 889 },
768 890 )))
769 891 } else {
770 892 // It's either missing or under a symlink directory which
771 893 // we, in this case, report as missing.
772 894 Some(Ok((
773 895 Cow::Borrowed(filename),
774 896 dispatch_missing(entry.state),
775 897 )))
776 898 }
777 899 })
778 900 .collect();
779 901
780 902 results.par_extend(new_results?);
781 903
782 904 Ok(())
783 905 }
784
785 /// Add the files in the dirstate to the results.
786 ///
787 /// This takes a mutable reference to the results to account for the
788 /// `extend` in timings
789 #[timed]
790 pub fn extend_from_dmap(&self, results: &mut Vec<DispatchedPath<'a>>) {
791 results.par_extend(self.dmap.par_iter().flat_map(
792 move |(filename, entry)| {
793 let filename: &HgPath = filename;
794 let filename_as_path = hg_path_to_path_buf(filename)?;
795 let meta =
796 self.root_dir.join(filename_as_path).symlink_metadata();
797
798 match meta {
799 Ok(ref m)
800 if !(m.file_type().is_file()
801 || m.file_type().is_symlink()) =>
802 {
803 Ok((
804 Cow::Borrowed(filename),
805 dispatch_missing(entry.state),
806 ))
807 }
808 Ok(m) => Ok((
809 Cow::Borrowed(filename),
810 dispatch_found(
811 filename,
812 *entry,
813 HgMetadata::from_metadata(m),
814 &self.dmap.copy_map,
815 self.options,
816 ),
817 )),
818 Err(ref e)
819 if e.kind() == ErrorKind::NotFound
820 || e.raw_os_error() == Some(20) =>
821 {
822 // Rust does not yet have an `ErrorKind` for
823 // `NotADirectory` (errno 20)
824 // It happens if the dirstate contains `foo/bar`
825 // and foo is not a
826 // directory
827 Ok((
828 Cow::Borrowed(filename),
829 dispatch_missing(entry.state),
830 ))
831 }
832 Err(e) => Err(e),
833 }
834 },
835 ));
836 }
837 906 }
838 907
839 908 #[timed]
840 909 pub fn build_response<'a>(
841 910 results: impl IntoIterator<Item = DispatchedPath<'a>>,
842 911 traversed: Vec<HgPathBuf>,
843 912 ) -> (Vec<HgPathCow<'a>>, DirstateStatus<'a>) {
844 913 let mut lookup = vec![];
845 914 let mut modified = vec![];
846 915 let mut added = vec![];
847 916 let mut removed = vec![];
848 917 let mut deleted = vec![];
849 918 let mut clean = vec![];
850 919 let mut ignored = vec![];
851 920 let mut unknown = vec![];
852 921 let mut bad = vec![];
853 922
854 923 for (filename, dispatch) in results.into_iter() {
855 924 match dispatch {
856 925 Dispatch::Unknown => unknown.push(filename),
857 926 Dispatch::Unsure => lookup.push(filename),
858 927 Dispatch::Modified => modified.push(filename),
859 928 Dispatch::Added => added.push(filename),
860 929 Dispatch::Removed => removed.push(filename),
861 930 Dispatch::Deleted => deleted.push(filename),
862 931 Dispatch::Clean => clean.push(filename),
863 932 Dispatch::Ignored => ignored.push(filename),
864 933 Dispatch::None => {}
865 934 Dispatch::Bad(reason) => bad.push((filename, reason)),
866 935 Dispatch::Directory { .. } => {}
867 936 }
868 937 }
869 938
870 939 (
871 940 lookup,
872 941 DirstateStatus {
873 942 modified,
874 943 added,
875 944 removed,
876 945 deleted,
877 946 clean,
878 947 ignored,
879 948 unknown,
880 949 bad,
881 950 traversed,
882 951 },
883 952 )
884 953 }
885 954
886 955 /// Get the status of files in the working directory.
887 956 ///
888 957 /// This is the current entry-point for `hg-core` and is realistically unusable
889 958 /// outside of a Python context because its arguments need to provide a lot of
890 959 /// information that will not be necessary in the future.
891 960 #[timed]
892 961 pub fn status<'a>(
893 962 dmap: &'a DirstateMap,
894 963 matcher: &'a (impl Matcher + Sync),
895 964 root_dir: PathBuf,
896 965 ignore_files: Vec<PathBuf>,
897 966 options: StatusOptions,
898 967 ) -> StatusResult<(
899 968 (Vec<HgPathCow<'a>>, DirstateStatus<'a>),
900 969 Vec<PatternFileWarning>,
901 970 )> {
902 971 let (status, warnings) =
903 972 Status::new(dmap, matcher, root_dir, ignore_files, options)?;
904 973
905 974 Ok((status.run()?, warnings))
906 975 }
@@ -1,73 +1,133 b''
1 1 // dirstate_status.rs
2 2 //
3 3 // Copyright 2019, Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 use crate::dirstate::status::{build_response, Dispatch, HgPathCow, Status};
9 9 use crate::matchers::Matcher;
10 10 use crate::{DirstateStatus, StatusError};
11 11
12 12 /// A tuple of the paths that need to be checked in the filelog because it's
13 13 /// ambiguous whether they've changed, and the rest of the already dispatched
14 14 /// files.
15 15 pub type LookupAndStatus<'a> = (Vec<HgPathCow<'a>>, DirstateStatus<'a>);
16 16
17 #[cfg(feature = "dirstate-tree")]
18 impl<'a, M: Matcher + Sync> Status<'a, M> {
19 pub(crate) fn run(&self) -> Result<LookupAndStatus<'a>, StatusError> {
20 let (traversed_sender, traversed_receiver) =
21 crossbeam::channel::unbounded();
22
23 // Step 1: check the files explicitly mentioned by the user
24 let (work, mut results) = self.walk_explicit(traversed_sender.clone());
25
26 // Step 2: Check files in the dirstate
27 if !self.matcher.is_exact() {
28 self.extend_from_dmap(&mut results);
29 }
30 // Step 3: Check the working directory if listing unknowns
31 if !work.is_empty() {
32 // Hashmaps are quite a bit slower to build than vecs, so only
33 // build it if needed.
34 let mut old_results = None;
35
36 // Step 2: recursively check the working directory for changes if
37 // needed
38 for (dir, dispatch) in work {
39 match dispatch {
40 Dispatch::Directory { was_file } => {
41 if was_file {
42 results.push((dir.to_owned(), Dispatch::Removed));
43 }
44 if self.options.list_ignored
45 || self.options.list_unknown
46 && !self.dir_ignore(&dir)
47 {
48 if old_results.is_none() {
49 old_results =
50 Some(results.iter().cloned().collect());
51 }
52 self.traverse(
53 &dir,
54 old_results
55 .as_ref()
56 .expect("old results should exist"),
57 &mut results,
58 traversed_sender.clone(),
59 )?;
60 }
61 }
62 _ => {
63 unreachable!("There can only be directories in `work`")
64 }
65 }
66 }
67 }
68
69 drop(traversed_sender);
70 let traversed = traversed_receiver.into_iter().collect();
71
72 Ok(build_response(results, traversed))
73 }
74 }
75
76 #[cfg(not(feature = "dirstate-tree"))]
17 77 impl<'a, M: Matcher + Sync> Status<'a, M> {
18 78 pub(crate) fn run(&self) -> Result<LookupAndStatus<'a>, StatusError> {
19 79 let (traversed_sender, traversed_receiver) =
20 80 crossbeam::channel::unbounded();
21 81
22 82 // Step 1: check the files explicitly mentioned by the user
23 83 let (work, mut results) = self.walk_explicit(traversed_sender.clone());
24 84
25 85 if !work.is_empty() {
26 86 // Hashmaps are quite a bit slower to build than vecs, so only
27 87 // build it if needed.
28 88 let old_results = results.iter().cloned().collect();
29 89
30 90 // Step 2: recursively check the working directory for changes if
31 91 // needed
32 92 for (dir, dispatch) in work {
33 93 match dispatch {
34 94 Dispatch::Directory { was_file } => {
35 95 if was_file {
36 96 results.push((dir.to_owned(), Dispatch::Removed));
37 97 }
38 98 if self.options.list_ignored
39 99 || self.options.list_unknown
40 100 && !self.dir_ignore(&dir)
41 101 {
42 102 self.traverse(
43 103 &dir,
44 104 &old_results,
45 105 &mut results,
46 106 traversed_sender.clone(),
47 107 )?;
48 108 }
49 109 }
50 110 _ => {
51 111 unreachable!("There can only be directories in `work`")
52 112 }
53 113 }
54 114 }
55 115 }
56 116
57 117 if !self.matcher.is_exact() {
58 118 if self.options.list_unknown {
59 119 self.handle_unknowns(&mut results)?;
60 120 } else {
61 121 // TODO this is incorrect, see issue6335
62 122 // This requires a fix in both Python and Rust that can happen
63 123 // with other pending changes to `status`.
64 124 self.extend_from_dmap(&mut results);
65 125 }
66 126 }
67 127
68 128 drop(traversed_sender);
69 129 let traversed = traversed_receiver.into_iter().collect();
70 130
71 131 Ok(build_response(results, traversed))
72 132 }
73 133 }
@@ -1,590 +1,610 b''
1 1 // dirstate_map.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 //! Bindings for the `hg::dirstate::dirstate_map` file provided by the
9 9 //! `hg-core` package.
10 10
11 11 use std::cell::{Ref, RefCell};
12 12 use std::convert::TryInto;
13 13 use std::time::Duration;
14 14
15 15 use cpython::{
16 16 exc, ObjectProtocol, PyBool, PyBytes, PyClone, PyDict, PyErr, PyList,
17 17 PyObject, PyResult, PyString, PyTuple, Python, PythonObject, ToPyObject,
18 18 UnsafePyLeaked,
19 19 };
20 20
21 21 use crate::{
22 22 dirstate::copymap::{CopyMap, CopyMapItemsIterator, CopyMapKeysIterator},
23 23 dirstate::non_normal_entries::{
24 24 NonNormalEntries, NonNormalEntriesIterator,
25 25 },
26 26 dirstate::{dirs_multiset::Dirs, make_dirstate_tuple},
27 27 };
28 28 use hg::{
29 29 utils::hg_path::{HgPath, HgPathBuf},
30 30 DirsMultiset, DirstateEntry, DirstateMap as RustDirstateMap,
31 31 DirstateMapError, DirstateParents, DirstateParseError, EntryState,
32 32 StateMapIter, PARENT_SIZE,
33 33 };
34 34
35 35 // TODO
36 36 // This object needs to share references to multiple members of its Rust
37 37 // inner struct, namely `copy_map`, `dirs` and `all_dirs`.
38 38 // Right now `CopyMap` is done, but it needs to have an explicit reference
39 39 // to `RustDirstateMap` which itself needs to have an encapsulation for
40 40 // every method in `CopyMap` (copymapcopy, etc.).
41 41 // This is ugly and hard to maintain.
42 42 // The same logic applies to `dirs` and `all_dirs`, however the `Dirs`
43 43 // `py_class!` is already implemented and does not mention
44 44 // `RustDirstateMap`, rightfully so.
45 45 // All attributes also have to have a separate refcount data attribute for
46 46 // leaks, with all methods that go along for reference sharing.
47 47 py_class!(pub class DirstateMap |py| {
48 48 @shared data inner: RustDirstateMap;
49 49
50 50 def __new__(_cls, _root: PyObject) -> PyResult<Self> {
51 51 let inner = RustDirstateMap::default();
52 52 Self::create_instance(py, inner)
53 53 }
54 54
55 55 def clear(&self) -> PyResult<PyObject> {
56 56 self.inner(py).borrow_mut().clear();
57 57 Ok(py.None())
58 58 }
59 59
60 60 def get(
61 61 &self,
62 62 key: PyObject,
63 63 default: Option<PyObject> = None
64 64 ) -> PyResult<Option<PyObject>> {
65 65 let key = key.extract::<PyBytes>(py)?;
66 66 match self.inner(py).borrow().get(HgPath::new(key.data(py))) {
67 67 Some(entry) => {
68 68 Ok(Some(make_dirstate_tuple(py, entry)?))
69 69 },
70 70 None => Ok(default)
71 71 }
72 72 }
73 73
74 74 def addfile(
75 75 &self,
76 76 f: PyObject,
77 77 oldstate: PyObject,
78 78 state: PyObject,
79 79 mode: PyObject,
80 80 size: PyObject,
81 81 mtime: PyObject
82 82 ) -> PyResult<PyObject> {
83 83 self.inner(py).borrow_mut().add_file(
84 84 HgPath::new(f.extract::<PyBytes>(py)?.data(py)),
85 85 oldstate.extract::<PyBytes>(py)?.data(py)[0]
86 86 .try_into()
87 87 .map_err(|e: DirstateParseError| {
88 88 PyErr::new::<exc::ValueError, _>(py, e.to_string())
89 89 })?,
90 90 DirstateEntry {
91 91 state: state.extract::<PyBytes>(py)?.data(py)[0]
92 92 .try_into()
93 93 .map_err(|e: DirstateParseError| {
94 94 PyErr::new::<exc::ValueError, _>(py, e.to_string())
95 95 })?,
96 96 mode: mode.extract(py)?,
97 97 size: size.extract(py)?,
98 98 mtime: mtime.extract(py)?,
99 99 },
100 100 ).and(Ok(py.None())).or_else(|e: DirstateMapError| {
101 101 Err(PyErr::new::<exc::ValueError, _>(py, e.to_string()))
102 102 })
103 103 }
104 104
105 105 def removefile(
106 106 &self,
107 107 f: PyObject,
108 108 oldstate: PyObject,
109 109 size: PyObject
110 110 ) -> PyResult<PyObject> {
111 111 self.inner(py).borrow_mut()
112 112 .remove_file(
113 113 HgPath::new(f.extract::<PyBytes>(py)?.data(py)),
114 114 oldstate.extract::<PyBytes>(py)?.data(py)[0]
115 115 .try_into()
116 116 .map_err(|e: DirstateParseError| {
117 117 PyErr::new::<exc::ValueError, _>(py, e.to_string())
118 118 })?,
119 119 size.extract(py)?,
120 120 )
121 121 .or_else(|_| {
122 122 Err(PyErr::new::<exc::OSError, _>(
123 123 py,
124 124 "Dirstate error".to_string(),
125 125 ))
126 126 })?;
127 127 Ok(py.None())
128 128 }
129 129
130 130 def dropfile(
131 131 &self,
132 132 f: PyObject,
133 133 oldstate: PyObject
134 134 ) -> PyResult<PyBool> {
135 135 self.inner(py).borrow_mut()
136 136 .drop_file(
137 137 HgPath::new(f.extract::<PyBytes>(py)?.data(py)),
138 138 oldstate.extract::<PyBytes>(py)?.data(py)[0]
139 139 .try_into()
140 140 .map_err(|e: DirstateParseError| {
141 141 PyErr::new::<exc::ValueError, _>(py, e.to_string())
142 142 })?,
143 143 )
144 144 .and_then(|b| Ok(b.to_py_object(py)))
145 .or_else(|_| {
145 .or_else(|e| {
146 146 Err(PyErr::new::<exc::OSError, _>(
147 147 py,
148 "Dirstate error".to_string(),
148 format!("Dirstate error: {}", e.to_string()),
149 149 ))
150 150 })
151 151 }
152 152
153 153 def clearambiguoustimes(
154 154 &self,
155 155 files: PyObject,
156 156 now: PyObject
157 157 ) -> PyResult<PyObject> {
158 158 let files: PyResult<Vec<HgPathBuf>> = files
159 159 .iter(py)?
160 160 .map(|filename| {
161 161 Ok(HgPathBuf::from_bytes(
162 162 filename?.extract::<PyBytes>(py)?.data(py),
163 163 ))
164 164 })
165 165 .collect();
166 166 self.inner(py).borrow_mut()
167 167 .clear_ambiguous_times(files?, now.extract(py)?);
168 168 Ok(py.None())
169 169 }
170 170
171 171 def other_parent_entries(&self) -> PyResult<PyObject> {
172 172 let mut inner_shared = self.inner(py).borrow_mut();
173 173 let (_, other_parent) =
174 174 inner_shared.get_non_normal_other_parent_entries();
175 175
176 176 let locals = PyDict::new(py);
177 177 locals.set_item(
178 178 py,
179 179 "other_parent",
180 180 other_parent
181 181 .iter()
182 182 .map(|v| PyBytes::new(py, v.as_bytes()))
183 183 .collect::<Vec<PyBytes>>()
184 184 .to_py_object(py),
185 185 )?;
186 186
187 187 py.eval("set(other_parent)", None, Some(&locals))
188 188 }
189 189
190 190 def non_normal_entries(&self) -> PyResult<NonNormalEntries> {
191 191 NonNormalEntries::from_inner(py, self.clone_ref(py))
192 192 }
193 193
194 194 def non_normal_entries_contains(&self, key: PyObject) -> PyResult<bool> {
195 195 let key = key.extract::<PyBytes>(py)?;
196 196 Ok(self
197 197 .inner(py)
198 198 .borrow_mut()
199 199 .get_non_normal_other_parent_entries().0
200 200 .contains(HgPath::new(key.data(py))))
201 201 }
202 202
203 203 def non_normal_entries_display(&self) -> PyResult<PyString> {
204 204 Ok(
205 205 PyString::new(
206 206 py,
207 207 &format!(
208 208 "NonNormalEntries: {:?}",
209 209 self
210 210 .inner(py)
211 211 .borrow_mut()
212 212 .get_non_normal_other_parent_entries().0
213 213 .iter().map(|o| o))
214 214 )
215 215 )
216 216 }
217 217
218 218 def non_normal_entries_remove(&self, key: PyObject) -> PyResult<PyObject> {
219 219 let key = key.extract::<PyBytes>(py)?;
220 220 self
221 221 .inner(py)
222 222 .borrow_mut()
223 223 .non_normal_entries_remove(HgPath::new(key.data(py)));
224 224 Ok(py.None())
225 225 }
226 226
227 227 def non_normal_entries_union(&self, other: PyObject) -> PyResult<PyList> {
228 228 let other: PyResult<_> = other.iter(py)?
229 229 .map(|f| {
230 230 Ok(HgPathBuf::from_bytes(
231 231 f?.extract::<PyBytes>(py)?.data(py),
232 232 ))
233 233 })
234 234 .collect();
235 235
236 236 let res = self
237 237 .inner(py)
238 238 .borrow_mut()
239 239 .non_normal_entries_union(other?);
240 240
241 241 let ret = PyList::new(py, &[]);
242 242 for filename in res.iter() {
243 243 let as_pystring = PyBytes::new(py, filename.as_bytes());
244 244 ret.append(py, as_pystring.into_object());
245 245 }
246 246 Ok(ret)
247 247 }
248 248
249 249 def non_normal_entries_iter(&self) -> PyResult<NonNormalEntriesIterator> {
250 250 // Make sure the sets are defined before we no longer have a mutable
251 251 // reference to the dmap.
252 252 self.inner(py)
253 253 .borrow_mut()
254 254 .set_non_normal_other_parent_entries(false);
255 255
256 256 let leaked_ref = self.inner(py).leak_immutable();
257 257
258 258 NonNormalEntriesIterator::from_inner(py, unsafe {
259 259 leaked_ref.map(py, |o| {
260 260 o.get_non_normal_other_parent_entries_panic().0.iter()
261 261 })
262 262 })
263 263 }
264 264
265 265 def hastrackeddir(&self, d: PyObject) -> PyResult<PyBool> {
266 266 let d = d.extract::<PyBytes>(py)?;
267 267 Ok(self.inner(py).borrow_mut()
268 268 .has_tracked_dir(HgPath::new(d.data(py)))
269 269 .map_err(|e| {
270 270 PyErr::new::<exc::ValueError, _>(py, e.to_string())
271 271 })?
272 272 .to_py_object(py))
273 273 }
274 274
275 275 def hasdir(&self, d: PyObject) -> PyResult<PyBool> {
276 276 let d = d.extract::<PyBytes>(py)?;
277 277 Ok(self.inner(py).borrow_mut()
278 278 .has_dir(HgPath::new(d.data(py)))
279 279 .map_err(|e| {
280 280 PyErr::new::<exc::ValueError, _>(py, e.to_string())
281 281 })?
282 282 .to_py_object(py))
283 283 }
284 284
285 285 def parents(&self, st: PyObject) -> PyResult<PyTuple> {
286 286 self.inner(py).borrow_mut()
287 287 .parents(st.extract::<PyBytes>(py)?.data(py))
288 288 .and_then(|d| {
289 289 Ok((PyBytes::new(py, &d.p1), PyBytes::new(py, &d.p2))
290 290 .to_py_object(py))
291 291 })
292 292 .or_else(|_| {
293 293 Err(PyErr::new::<exc::OSError, _>(
294 294 py,
295 295 "Dirstate error".to_string(),
296 296 ))
297 297 })
298 298 }
299 299
300 300 def setparents(&self, p1: PyObject, p2: PyObject) -> PyResult<PyObject> {
301 301 let p1 = extract_node_id(py, &p1)?;
302 302 let p2 = extract_node_id(py, &p2)?;
303 303
304 304 self.inner(py).borrow_mut()
305 305 .set_parents(&DirstateParents { p1, p2 });
306 306 Ok(py.None())
307 307 }
308 308
309 309 def read(&self, st: PyObject) -> PyResult<Option<PyObject>> {
310 310 match self.inner(py).borrow_mut()
311 311 .read(st.extract::<PyBytes>(py)?.data(py))
312 312 {
313 313 Ok(Some(parents)) => Ok(Some(
314 314 (PyBytes::new(py, &parents.p1), PyBytes::new(py, &parents.p2))
315 315 .to_py_object(py)
316 316 .into_object(),
317 317 )),
318 318 Ok(None) => Ok(Some(py.None())),
319 319 Err(_) => Err(PyErr::new::<exc::OSError, _>(
320 320 py,
321 321 "Dirstate error".to_string(),
322 322 )),
323 323 }
324 324 }
325 325 def write(
326 326 &self,
327 327 p1: PyObject,
328 328 p2: PyObject,
329 329 now: PyObject
330 330 ) -> PyResult<PyBytes> {
331 331 let now = Duration::new(now.extract(py)?, 0);
332 332 let parents = DirstateParents {
333 333 p1: extract_node_id(py, &p1)?,
334 334 p2: extract_node_id(py, &p2)?,
335 335 };
336 336
337 337 match self.inner(py).borrow_mut().pack(parents, now) {
338 338 Ok(packed) => Ok(PyBytes::new(py, &packed)),
339 339 Err(_) => Err(PyErr::new::<exc::OSError, _>(
340 340 py,
341 341 "Dirstate error".to_string(),
342 342 )),
343 343 }
344 344 }
345 345
346 346 def filefoldmapasdict(&self) -> PyResult<PyDict> {
347 347 let dict = PyDict::new(py);
348 348 for (key, value) in
349 349 self.inner(py).borrow_mut().build_file_fold_map().iter()
350 350 {
351 351 dict.set_item(
352 352 py,
353 353 key.as_bytes().to_vec(),
354 354 value.as_bytes().to_vec(),
355 355 )?;
356 356 }
357 357 Ok(dict)
358 358 }
359 359
360 360 def __len__(&self) -> PyResult<usize> {
361 361 Ok(self.inner(py).borrow().len())
362 362 }
363 363
364 364 def __contains__(&self, key: PyObject) -> PyResult<bool> {
365 365 let key = key.extract::<PyBytes>(py)?;
366 366 Ok(self.inner(py).borrow().contains_key(HgPath::new(key.data(py))))
367 367 }
368 368
369 369 def __getitem__(&self, key: PyObject) -> PyResult<PyObject> {
370 370 let key = key.extract::<PyBytes>(py)?;
371 371 let key = HgPath::new(key.data(py));
372 372 match self.inner(py).borrow().get(key) {
373 373 Some(entry) => {
374 374 Ok(make_dirstate_tuple(py, entry)?)
375 375 },
376 376 None => Err(PyErr::new::<exc::KeyError, _>(
377 377 py,
378 378 String::from_utf8_lossy(key.as_bytes()),
379 379 )),
380 380 }
381 381 }
382 382
383 383 def keys(&self) -> PyResult<DirstateMapKeysIterator> {
384 384 let leaked_ref = self.inner(py).leak_immutable();
385 385 DirstateMapKeysIterator::from_inner(
386 386 py,
387 387 unsafe { leaked_ref.map(py, |o| o.iter()) },
388 388 )
389 389 }
390 390
391 391 def items(&self) -> PyResult<DirstateMapItemsIterator> {
392 392 let leaked_ref = self.inner(py).leak_immutable();
393 393 DirstateMapItemsIterator::from_inner(
394 394 py,
395 395 unsafe { leaked_ref.map(py, |o| o.iter()) },
396 396 )
397 397 }
398 398
399 399 def __iter__(&self) -> PyResult<DirstateMapKeysIterator> {
400 400 let leaked_ref = self.inner(py).leak_immutable();
401 401 DirstateMapKeysIterator::from_inner(
402 402 py,
403 403 unsafe { leaked_ref.map(py, |o| o.iter()) },
404 404 )
405 405 }
406 406
407 407 def getdirs(&self) -> PyResult<Dirs> {
408 408 // TODO don't copy, share the reference
409 409 self.inner(py).borrow_mut().set_dirs()
410 410 .map_err(|e| {
411 411 PyErr::new::<exc::ValueError, _>(py, e.to_string())
412 412 })?;
413 413 Dirs::from_inner(
414 414 py,
415 415 DirsMultiset::from_dirstate(
416 416 &self.inner(py).borrow(),
417 417 Some(EntryState::Removed),
418 418 )
419 419 .map_err(|e| {
420 420 PyErr::new::<exc::ValueError, _>(py, e.to_string())
421 421 })?,
422 422 )
423 423 }
424 424 def getalldirs(&self) -> PyResult<Dirs> {
425 425 // TODO don't copy, share the reference
426 426 self.inner(py).borrow_mut().set_all_dirs()
427 427 .map_err(|e| {
428 428 PyErr::new::<exc::ValueError, _>(py, e.to_string())
429 429 })?;
430 430 Dirs::from_inner(
431 431 py,
432 432 DirsMultiset::from_dirstate(
433 433 &self.inner(py).borrow(),
434 434 None,
435 435 ).map_err(|e| {
436 436 PyErr::new::<exc::ValueError, _>(py, e.to_string())
437 437 })?,
438 438 )
439 439 }
440 440
441 441 // TODO all copymap* methods, see docstring above
442 442 def copymapcopy(&self) -> PyResult<PyDict> {
443 443 let dict = PyDict::new(py);
444 444 for (key, value) in self.inner(py).borrow().copy_map.iter() {
445 445 dict.set_item(
446 446 py,
447 447 PyBytes::new(py, key.as_bytes()),
448 448 PyBytes::new(py, value.as_bytes()),
449 449 )?;
450 450 }
451 451 Ok(dict)
452 452 }
453 453
454 454 def copymapgetitem(&self, key: PyObject) -> PyResult<PyBytes> {
455 455 let key = key.extract::<PyBytes>(py)?;
456 456 match self.inner(py).borrow().copy_map.get(HgPath::new(key.data(py))) {
457 457 Some(copy) => Ok(PyBytes::new(py, copy.as_bytes())),
458 458 None => Err(PyErr::new::<exc::KeyError, _>(
459 459 py,
460 460 String::from_utf8_lossy(key.data(py)),
461 461 )),
462 462 }
463 463 }
464 464 def copymap(&self) -> PyResult<CopyMap> {
465 465 CopyMap::from_inner(py, self.clone_ref(py))
466 466 }
467 467
468 468 def copymaplen(&self) -> PyResult<usize> {
469 469 Ok(self.inner(py).borrow().copy_map.len())
470 470 }
471 471 def copymapcontains(&self, key: PyObject) -> PyResult<bool> {
472 472 let key = key.extract::<PyBytes>(py)?;
473 473 Ok(self
474 474 .inner(py)
475 475 .borrow()
476 476 .copy_map
477 477 .contains_key(HgPath::new(key.data(py))))
478 478 }
479 479 def copymapget(
480 480 &self,
481 481 key: PyObject,
482 482 default: Option<PyObject>
483 483 ) -> PyResult<Option<PyObject>> {
484 484 let key = key.extract::<PyBytes>(py)?;
485 485 match self
486 486 .inner(py)
487 487 .borrow()
488 488 .copy_map
489 489 .get(HgPath::new(key.data(py)))
490 490 {
491 491 Some(copy) => Ok(Some(
492 492 PyBytes::new(py, copy.as_bytes()).into_object(),
493 493 )),
494 494 None => Ok(default),
495 495 }
496 496 }
497 497 def copymapsetitem(
498 498 &self,
499 499 key: PyObject,
500 500 value: PyObject
501 501 ) -> PyResult<PyObject> {
502 502 let key = key.extract::<PyBytes>(py)?;
503 503 let value = value.extract::<PyBytes>(py)?;
504 504 self.inner(py).borrow_mut().copy_map.insert(
505 505 HgPathBuf::from_bytes(key.data(py)),
506 506 HgPathBuf::from_bytes(value.data(py)),
507 507 );
508 508 Ok(py.None())
509 509 }
510 510 def copymappop(
511 511 &self,
512 512 key: PyObject,
513 513 default: Option<PyObject>
514 514 ) -> PyResult<Option<PyObject>> {
515 515 let key = key.extract::<PyBytes>(py)?;
516 516 match self
517 517 .inner(py)
518 518 .borrow_mut()
519 519 .copy_map
520 520 .remove(HgPath::new(key.data(py)))
521 521 {
522 522 Some(_) => Ok(None),
523 523 None => Ok(default),
524 524 }
525 525 }
526 526
527 527 def copymapiter(&self) -> PyResult<CopyMapKeysIterator> {
528 528 let leaked_ref = self.inner(py).leak_immutable();
529 529 CopyMapKeysIterator::from_inner(
530 530 py,
531 531 unsafe { leaked_ref.map(py, |o| o.copy_map.iter()) },
532 532 )
533 533 }
534 534
535 535 def copymapitemsiter(&self) -> PyResult<CopyMapItemsIterator> {
536 536 let leaked_ref = self.inner(py).leak_immutable();
537 537 CopyMapItemsIterator::from_inner(
538 538 py,
539 539 unsafe { leaked_ref.map(py, |o| o.copy_map.iter()) },
540 540 )
541 541 }
542 542
543 543 });
544 544
545 545 impl DirstateMap {
546 546 pub fn get_inner<'a>(
547 547 &'a self,
548 548 py: Python<'a>,
549 549 ) -> Ref<'a, RustDirstateMap> {
550 550 self.inner(py).borrow()
551 551 }
552 #[cfg(not(feature = "dirstate-tree"))]
552 553 fn translate_key(
553 554 py: Python,
554 555 res: (&HgPathBuf, &DirstateEntry),
555 556 ) -> PyResult<Option<PyBytes>> {
556 557 Ok(Some(PyBytes::new(py, res.0.as_bytes())))
557 558 }
559 #[cfg(not(feature = "dirstate-tree"))]
558 560 fn translate_key_value(
559 561 py: Python,
560 562 res: (&HgPathBuf, &DirstateEntry),
561 563 ) -> PyResult<Option<(PyBytes, PyObject)>> {
562 564 let (f, entry) = res;
563 565 Ok(Some((
564 566 PyBytes::new(py, f.as_bytes()),
565 make_dirstate_tuple(py, entry)?,
567 make_dirstate_tuple(py, &entry)?,
568 )))
569 }
570 #[cfg(feature = "dirstate-tree")]
571 fn translate_key(
572 py: Python,
573 res: (HgPathBuf, DirstateEntry),
574 ) -> PyResult<Option<PyBytes>> {
575 Ok(Some(PyBytes::new(py, res.0.as_bytes())))
576 }
577 #[cfg(feature = "dirstate-tree")]
578 fn translate_key_value(
579 py: Python,
580 res: (HgPathBuf, DirstateEntry),
581 ) -> PyResult<Option<(PyBytes, PyObject)>> {
582 let (f, entry) = res;
583 Ok(Some((
584 PyBytes::new(py, f.as_bytes()),
585 make_dirstate_tuple(py, &entry)?,
566 586 )))
567 587 }
568 588 }
569 589
570 590 py_shared_iterator!(
571 591 DirstateMapKeysIterator,
572 592 UnsafePyLeaked<StateMapIter<'static>>,
573 593 DirstateMap::translate_key,
574 594 Option<PyBytes>
575 595 );
576 596
577 597 py_shared_iterator!(
578 598 DirstateMapItemsIterator,
579 599 UnsafePyLeaked<StateMapIter<'static>>,
580 600 DirstateMap::translate_key_value,
581 601 Option<(PyBytes, PyObject)>
582 602 );
583 603
584 604 fn extract_node_id(py: Python, obj: &PyObject) -> PyResult<[u8; PARENT_SIZE]> {
585 605 let bytes = obj.extract::<PyBytes>(py)?;
586 606 match bytes.data(py).try_into() {
587 607 Ok(s) => Ok(s),
588 608 Err(e) => Err(PyErr::new::<exc::ValueError, _>(py, e.to_string())),
589 609 }
590 610 }
@@ -1,301 +1,301 b''
1 1 // status.rs
2 2 //
3 3 // Copyright 2019, Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 //! Bindings for the `hg::status` module provided by the
9 9 //! `hg-core` crate. From Python, this will be seen as
10 10 //! `rustext.dirstate.status`.
11 11
12 12 use crate::{dirstate::DirstateMap, exceptions::FallbackError};
13 13 use cpython::{
14 14 exc::ValueError, ObjectProtocol, PyBytes, PyErr, PyList, PyObject,
15 15 PyResult, PyTuple, Python, PythonObject, ToPyObject,
16 16 };
17 17 use hg::{
18 18 matchers::{AlwaysMatcher, FileMatcher, IncludeMatcher},
19 19 parse_pattern_syntax, status,
20 20 utils::{
21 21 files::{get_bytes_from_path, get_path_from_bytes},
22 22 hg_path::{HgPath, HgPathBuf},
23 23 },
24 24 BadMatch, DirstateStatus, IgnorePattern, PatternFileWarning, StatusError,
25 25 StatusOptions,
26 26 };
27 27 use std::borrow::{Borrow, Cow};
28 28
29 29 /// This will be useless once trait impls for collection are added to `PyBytes`
30 30 /// upstream.
31 31 fn collect_pybytes_list(
32 32 py: Python,
33 33 collection: &[impl AsRef<HgPath>],
34 34 ) -> PyList {
35 35 let list = PyList::new(py, &[]);
36 36
37 37 for path in collection.iter() {
38 38 list.append(
39 39 py,
40 40 PyBytes::new(py, path.as_ref().as_bytes()).into_object(),
41 41 )
42 42 }
43 43
44 44 list
45 45 }
46 46
47 47 fn collect_bad_matches(
48 48 py: Python,
49 49 collection: &[(impl AsRef<HgPath>, BadMatch)],
50 50 ) -> PyResult<PyList> {
51 51 let list = PyList::new(py, &[]);
52 52
53 53 let os = py.import("os")?;
54 54 let get_error_message = |code: i32| -> PyResult<_> {
55 55 os.call(
56 56 py,
57 57 "strerror",
58 58 PyTuple::new(py, &[code.to_py_object(py).into_object()]),
59 59 None,
60 60 )
61 61 };
62 62
63 63 for (path, bad_match) in collection.iter() {
64 64 let message = match bad_match {
65 65 BadMatch::OsError(code) => get_error_message(*code)?,
66 66 BadMatch::BadType(bad_type) => format!(
67 67 "unsupported file type (type is {})",
68 68 bad_type.to_string()
69 69 )
70 70 .to_py_object(py)
71 71 .into_object(),
72 72 };
73 73 list.append(
74 74 py,
75 75 (PyBytes::new(py, path.as_ref().as_bytes()), message)
76 76 .to_py_object(py)
77 77 .into_object(),
78 78 )
79 79 }
80 80
81 81 Ok(list)
82 82 }
83 83
84 84 fn handle_fallback(py: Python, err: StatusError) -> PyErr {
85 85 match err {
86 86 StatusError::Pattern(e) => {
87 87 let as_string = e.to_string();
88 88 log::trace!("Rust status fallback: `{}`", &as_string);
89 89
90 90 PyErr::new::<FallbackError, _>(py, &as_string)
91 91 }
92 92 e => PyErr::new::<ValueError, _>(py, e.to_string()),
93 93 }
94 94 }
95 95
96 96 pub fn status_wrapper(
97 97 py: Python,
98 98 dmap: DirstateMap,
99 99 matcher: PyObject,
100 100 root_dir: PyObject,
101 101 ignore_files: PyList,
102 102 check_exec: bool,
103 103 last_normal_time: i64,
104 104 list_clean: bool,
105 105 list_ignored: bool,
106 106 list_unknown: bool,
107 107 collect_traversed_dirs: bool,
108 108 ) -> PyResult<PyTuple> {
109 109 let bytes = root_dir.extract::<PyBytes>(py)?;
110 110 let root_dir = get_path_from_bytes(bytes.data(py));
111 111
112 112 let dmap: DirstateMap = dmap.to_py_object(py);
113 113 let dmap = dmap.get_inner(py);
114 114
115 115 let ignore_files: PyResult<Vec<_>> = ignore_files
116 116 .iter(py)
117 117 .map(|b| {
118 118 let file = b.extract::<PyBytes>(py)?;
119 119 Ok(get_path_from_bytes(file.data(py)).to_owned())
120 120 })
121 121 .collect();
122 122 let ignore_files = ignore_files?;
123 123
124 124 match matcher.get_type(py).name(py).borrow() {
125 125 "alwaysmatcher" => {
126 126 let matcher = AlwaysMatcher;
127 127 let ((lookup, status_res), warnings) = status(
128 128 &dmap,
129 129 &matcher,
130 130 root_dir.to_path_buf(),
131 131 ignore_files,
132 132 StatusOptions {
133 133 check_exec,
134 134 last_normal_time,
135 135 list_clean,
136 136 list_ignored,
137 137 list_unknown,
138 138 collect_traversed_dirs,
139 139 },
140 140 )
141 141 .map_err(|e| handle_fallback(py, e))?;
142 142 build_response(py, lookup, status_res, warnings)
143 143 }
144 144 "exactmatcher" => {
145 145 let files = matcher.call_method(
146 146 py,
147 147 "files",
148 148 PyTuple::new(py, &[]),
149 149 None,
150 150 )?;
151 151 let files: PyList = files.cast_into(py)?;
152 152 let files: PyResult<Vec<HgPathBuf>> = files
153 153 .iter(py)
154 154 .map(|f| {
155 155 Ok(HgPathBuf::from_bytes(
156 156 f.extract::<PyBytes>(py)?.data(py),
157 157 ))
158 158 })
159 159 .collect();
160 160
161 161 let files = files?;
162 let matcher = FileMatcher::new(&files)
162 let matcher = FileMatcher::new(files.as_ref())
163 163 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
164 164 let ((lookup, status_res), warnings) = status(
165 165 &dmap,
166 166 &matcher,
167 167 root_dir.to_path_buf(),
168 168 ignore_files,
169 169 StatusOptions {
170 170 check_exec,
171 171 last_normal_time,
172 172 list_clean,
173 173 list_ignored,
174 174 list_unknown,
175 175 collect_traversed_dirs,
176 176 },
177 177 )
178 178 .map_err(|e| handle_fallback(py, e))?;
179 179 build_response(py, lookup, status_res, warnings)
180 180 }
181 181 "includematcher" => {
182 182 // Get the patterns from Python even though most of them are
183 183 // redundant with those we will parse later on, as they include
184 184 // those passed from the command line.
185 185 let ignore_patterns: PyResult<Vec<_>> = matcher
186 186 .getattr(py, "_kindpats")?
187 187 .iter(py)?
188 188 .map(|k| {
189 189 let k = k?;
190 190 let syntax = parse_pattern_syntax(
191 191 &[
192 192 k.get_item(py, 0)?
193 193 .extract::<PyBytes>(py)?
194 194 .data(py),
195 195 &b":"[..],
196 196 ]
197 197 .concat(),
198 198 )
199 199 .map_err(|e| {
200 200 handle_fallback(py, StatusError::Pattern(e))
201 201 })?;
202 202 let pattern = k.get_item(py, 1)?.extract::<PyBytes>(py)?;
203 203 let pattern = pattern.data(py);
204 204 let source = k.get_item(py, 2)?.extract::<PyBytes>(py)?;
205 205 let source = get_path_from_bytes(source.data(py));
206 206 let new = IgnorePattern::new(syntax, pattern, source);
207 207 Ok(new)
208 208 })
209 209 .collect();
210 210
211 211 let ignore_patterns = ignore_patterns?;
212 212 let mut all_warnings = vec![];
213 213
214 214 let (matcher, warnings) =
215 215 IncludeMatcher::new(ignore_patterns, &root_dir)
216 216 .map_err(|e| handle_fallback(py, e.into()))?;
217 217 all_warnings.extend(warnings);
218 218
219 219 let ((lookup, status_res), warnings) = status(
220 220 &dmap,
221 221 &matcher,
222 222 root_dir.to_path_buf(),
223 223 ignore_files,
224 224 StatusOptions {
225 225 check_exec,
226 226 last_normal_time,
227 227 list_clean,
228 228 list_ignored,
229 229 list_unknown,
230 230 collect_traversed_dirs,
231 231 },
232 232 )
233 233 .map_err(|e| handle_fallback(py, e))?;
234 234
235 235 all_warnings.extend(warnings);
236 236
237 237 build_response(py, lookup, status_res, all_warnings)
238 238 }
239 239 e => Err(PyErr::new::<ValueError, _>(
240 240 py,
241 241 format!("Unsupported matcher {}", e),
242 242 )),
243 243 }
244 244 }
245 245
246 246 fn build_response(
247 247 py: Python,
248 248 lookup: Vec<Cow<HgPath>>,
249 249 status_res: DirstateStatus,
250 250 warnings: Vec<PatternFileWarning>,
251 251 ) -> PyResult<PyTuple> {
252 252 let modified = collect_pybytes_list(py, status_res.modified.as_ref());
253 253 let added = collect_pybytes_list(py, status_res.added.as_ref());
254 254 let removed = collect_pybytes_list(py, status_res.removed.as_ref());
255 255 let deleted = collect_pybytes_list(py, status_res.deleted.as_ref());
256 256 let clean = collect_pybytes_list(py, status_res.clean.as_ref());
257 257 let ignored = collect_pybytes_list(py, status_res.ignored.as_ref());
258 258 let unknown = collect_pybytes_list(py, status_res.unknown.as_ref());
259 259 let lookup = collect_pybytes_list(py, lookup.as_ref());
260 260 let bad = collect_bad_matches(py, status_res.bad.as_ref())?;
261 261 let traversed = collect_pybytes_list(py, status_res.traversed.as_ref());
262 262 let py_warnings = PyList::new(py, &[]);
263 263 for warning in warnings.iter() {
264 264 // We use duck-typing on the Python side for dispatch, good enough for
265 265 // now.
266 266 match warning {
267 267 PatternFileWarning::InvalidSyntax(file, syn) => {
268 268 py_warnings.append(
269 269 py,
270 270 (
271 271 PyBytes::new(py, &get_bytes_from_path(&file)),
272 272 PyBytes::new(py, syn),
273 273 )
274 274 .to_py_object(py)
275 275 .into_object(),
276 276 );
277 277 }
278 278 PatternFileWarning::NoSuchFile(file) => py_warnings.append(
279 279 py,
280 280 PyBytes::new(py, &get_bytes_from_path(&file)).into_object(),
281 281 ),
282 282 }
283 283 }
284 284
285 285 Ok(PyTuple::new(
286 286 py,
287 287 &[
288 288 lookup.into_object(),
289 289 modified.into_object(),
290 290 added.into_object(),
291 291 removed.into_object(),
292 292 deleted.into_object(),
293 293 clean.into_object(),
294 294 ignored.into_object(),
295 295 unknown.into_object(),
296 296 py_warnings.into_object(),
297 297 bad.into_object(),
298 298 traversed.into_object(),
299 299 ][..],
300 300 ))
301 301 }
@@ -1,181 +1,181 b''
1 1 // parsers.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 //! Bindings for the `hg::dirstate::parsers` module provided by the
9 9 //! `hg-core` package.
10 10 //!
11 11 //! From Python, this will be seen as `mercurial.rustext.parsers`
12 12 use cpython::{
13 13 exc, PyBytes, PyDict, PyErr, PyInt, PyModule, PyResult, PyTuple, Python,
14 14 PythonObject, ToPyObject,
15 15 };
16 16 use hg::{
17 17 pack_dirstate, parse_dirstate, utils::hg_path::HgPathBuf, DirstateEntry,
18 18 DirstatePackError, DirstateParents, DirstateParseError, FastHashMap,
19 19 PARENT_SIZE,
20 20 };
21 21 use std::convert::TryInto;
22 22
23 23 use crate::dirstate::{extract_dirstate, make_dirstate_tuple};
24 24 use std::time::Duration;
25 25
26 26 fn parse_dirstate_wrapper(
27 27 py: Python,
28 28 dmap: PyDict,
29 29 copymap: PyDict,
30 30 st: PyBytes,
31 31 ) -> PyResult<PyTuple> {
32 32 match parse_dirstate(st.data(py)) {
33 33 Ok((parents, entries, copies)) => {
34 34 let dirstate_map: FastHashMap<HgPathBuf, DirstateEntry> = entries
35 35 .into_iter()
36 36 .map(|(path, entry)| (path.to_owned(), entry))
37 37 .collect();
38 38 let copy_map: FastHashMap<HgPathBuf, HgPathBuf> = copies
39 39 .into_iter()
40 40 .map(|(path, copy)| (path.to_owned(), copy.to_owned()))
41 41 .collect();
42 42
43 43 for (filename, entry) in &dirstate_map {
44 44 dmap.set_item(
45 45 py,
46 46 PyBytes::new(py, filename.as_bytes()),
47 47 make_dirstate_tuple(py, entry)?,
48 48 )?;
49 49 }
50 50 for (path, copy_path) in copy_map {
51 51 copymap.set_item(
52 52 py,
53 53 PyBytes::new(py, path.as_bytes()),
54 54 PyBytes::new(py, copy_path.as_bytes()),
55 55 )?;
56 56 }
57 57 Ok(
58 58 (PyBytes::new(py, &parents.p1), PyBytes::new(py, &parents.p2))
59 59 .to_py_object(py),
60 60 )
61 61 }
62 62 Err(e) => Err(PyErr::new::<exc::ValueError, _>(
63 63 py,
64 64 match e {
65 65 DirstateParseError::TooLittleData => {
66 66 "too little data for parents".to_string()
67 67 }
68 68 DirstateParseError::Overflow => {
69 69 "overflow in dirstate".to_string()
70 70 }
71 71 DirstateParseError::CorruptedEntry(e) => e,
72 72 DirstateParseError::Damaged => {
73 73 "dirstate appears to be damaged".to_string()
74 74 }
75 75 },
76 76 )),
77 77 }
78 78 }
79 79
80 80 fn pack_dirstate_wrapper(
81 81 py: Python,
82 82 dmap: PyDict,
83 83 copymap: PyDict,
84 84 pl: PyTuple,
85 85 now: PyInt,
86 86 ) -> PyResult<PyBytes> {
87 87 let p1 = pl.get_item(py, 0).extract::<PyBytes>(py)?;
88 88 let p1: &[u8] = p1.data(py);
89 89 let p2 = pl.get_item(py, 1).extract::<PyBytes>(py)?;
90 90 let p2: &[u8] = p2.data(py);
91 91
92 92 let mut dirstate_map = extract_dirstate(py, &dmap)?;
93 93
94 94 let copies: Result<FastHashMap<HgPathBuf, HgPathBuf>, PyErr> = copymap
95 95 .items(py)
96 96 .iter()
97 97 .map(|(key, value)| {
98 98 Ok((
99 99 HgPathBuf::from_bytes(key.extract::<PyBytes>(py)?.data(py)),
100 100 HgPathBuf::from_bytes(value.extract::<PyBytes>(py)?.data(py)),
101 101 ))
102 102 })
103 103 .collect();
104 104
105 105 if p1.len() != PARENT_SIZE || p2.len() != PARENT_SIZE {
106 106 return Err(PyErr::new::<exc::ValueError, _>(
107 107 py,
108 108 "expected a 20-byte hash".to_string(),
109 109 ));
110 110 }
111 111
112 112 match pack_dirstate(
113 113 &mut dirstate_map,
114 114 &copies?,
115 115 DirstateParents {
116 116 p1: p1.try_into().unwrap(),
117 117 p2: p2.try_into().unwrap(),
118 118 },
119 119 Duration::from_secs(now.as_object().extract::<u64>(py)?),
120 120 ) {
121 121 Ok(packed) => {
122 for (filename, entry) in &dirstate_map {
122 for (filename, entry) in dirstate_map.iter() {
123 123 dmap.set_item(
124 124 py,
125 125 PyBytes::new(py, filename.as_bytes()),
126 make_dirstate_tuple(py, entry)?,
126 make_dirstate_tuple(py, &entry)?,
127 127 )?;
128 128 }
129 129 Ok(PyBytes::new(py, &packed))
130 130 }
131 131 Err(error) => Err(PyErr::new::<exc::ValueError, _>(
132 132 py,
133 133 match error {
134 134 DirstatePackError::CorruptedParent => {
135 135 "expected a 20-byte hash".to_string()
136 136 }
137 137 DirstatePackError::CorruptedEntry(e) => e,
138 138 DirstatePackError::BadSize(expected, actual) => {
139 139 format!("bad dirstate size: {} != {}", actual, expected)
140 140 }
141 141 },
142 142 )),
143 143 }
144 144 }
145 145
146 146 /// Create the module, with `__package__` given from parent
147 147 pub fn init_parsers_module(py: Python, package: &str) -> PyResult<PyModule> {
148 148 let dotted_name = &format!("{}.parsers", package);
149 149 let m = PyModule::new(py, dotted_name)?;
150 150
151 151 m.add(py, "__package__", package)?;
152 152 m.add(py, "__doc__", "Parsers - Rust implementation")?;
153 153
154 154 m.add(
155 155 py,
156 156 "parse_dirstate",
157 157 py_fn!(
158 158 py,
159 159 parse_dirstate_wrapper(dmap: PyDict, copymap: PyDict, st: PyBytes)
160 160 ),
161 161 )?;
162 162 m.add(
163 163 py,
164 164 "pack_dirstate",
165 165 py_fn!(
166 166 py,
167 167 pack_dirstate_wrapper(
168 168 dmap: PyDict,
169 169 copymap: PyDict,
170 170 pl: PyTuple,
171 171 now: PyInt
172 172 )
173 173 ),
174 174 )?;
175 175
176 176 let sys = PyModule::import(py, "sys")?;
177 177 let sys_modules: PyDict = sys.get(py, "modules")?.extract(py)?;
178 178 sys_modules.set_item(py, dotted_name, &m)?;
179 179
180 180 Ok(m)
181 181 }
General Comments 0
You need to be logged in to leave comments. Login now