##// END OF EJS Templates
rhg: Set second_ambiguous as needed in post-status fixup...
Simon Sapin -
r49272:11218471 default
parent child Browse files
Show More
@@ -1,691 +1,707
1 1 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
2 2 use crate::errors::HgError;
3 3 use bitflags::bitflags;
4 4 use std::convert::{TryFrom, TryInto};
5 5 use std::fs;
6 6 use std::io;
7 7 use std::time::{SystemTime, UNIX_EPOCH};
8 8
9 9 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
10 10 pub enum EntryState {
11 11 Normal,
12 12 Added,
13 13 Removed,
14 14 Merged,
15 15 }
16 16
17 17 /// `size` and `mtime.seconds` are truncated to 31 bits.
18 18 ///
19 19 /// TODO: double-check status algorithm correctness for files
20 20 /// larger than 2 GiB or modified after 2038.
21 21 #[derive(Debug, Copy, Clone)]
22 22 pub struct DirstateEntry {
23 23 pub(crate) flags: Flags,
24 24 mode_size: Option<(u32, u32)>,
25 25 mtime: Option<TruncatedTimestamp>,
26 26 }
27 27
28 28 bitflags! {
29 29 pub(crate) struct Flags: u8 {
30 30 const WDIR_TRACKED = 1 << 0;
31 31 const P1_TRACKED = 1 << 1;
32 32 const P2_INFO = 1 << 2;
33 33 const HAS_FALLBACK_EXEC = 1 << 3;
34 34 const FALLBACK_EXEC = 1 << 4;
35 35 const HAS_FALLBACK_SYMLINK = 1 << 5;
36 36 const FALLBACK_SYMLINK = 1 << 6;
37 37 }
38 38 }
39 39
40 40 /// A Unix timestamp with nanoseconds precision
41 41 #[derive(Debug, Copy, Clone)]
42 42 pub struct TruncatedTimestamp {
43 43 truncated_seconds: u32,
44 44 /// Always in the `0 .. 1_000_000_000` range.
45 45 nanoseconds: u32,
46 46 /// TODO this should be in DirstateEntry, but the current code needs
47 47 /// refactoring to use DirstateEntry instead of TruncatedTimestamp for
48 48 /// comparison.
49 49 pub second_ambiguous: bool,
50 50 }
51 51
52 52 impl TruncatedTimestamp {
53 53 /// Constructs from a timestamp potentially outside of the supported range,
54 54 /// and truncate the seconds components to its lower 31 bits.
55 55 ///
56 56 /// Panics if the nanoseconds components is not in the expected range.
57 57 pub fn new_truncate(
58 58 seconds: i64,
59 59 nanoseconds: u32,
60 60 second_ambiguous: bool,
61 61 ) -> Self {
62 62 assert!(nanoseconds < NSEC_PER_SEC);
63 63 Self {
64 64 truncated_seconds: seconds as u32 & RANGE_MASK_31BIT,
65 65 nanoseconds,
66 66 second_ambiguous,
67 67 }
68 68 }
69 69
70 70 /// Construct from components. Returns an error if they are not in the
71 71 /// expcted range.
72 72 pub fn from_already_truncated(
73 73 truncated_seconds: u32,
74 74 nanoseconds: u32,
75 75 second_ambiguous: bool,
76 76 ) -> Result<Self, DirstateV2ParseError> {
77 77 if truncated_seconds & !RANGE_MASK_31BIT == 0
78 78 && nanoseconds < NSEC_PER_SEC
79 79 {
80 80 Ok(Self {
81 81 truncated_seconds,
82 82 nanoseconds,
83 83 second_ambiguous,
84 84 })
85 85 } else {
86 86 Err(DirstateV2ParseError)
87 87 }
88 88 }
89 89
90 /// Returns a `TruncatedTimestamp` for the modification time of `metadata`.
91 ///
92 /// Propagates errors from `std` on platforms where modification time
93 /// is not available at all.
90 94 pub fn for_mtime_of(metadata: &fs::Metadata) -> io::Result<Self> {
91 95 #[cfg(unix)]
92 96 {
93 97 use std::os::unix::fs::MetadataExt;
94 98 let seconds = metadata.mtime();
95 99 // i64 -> u32 with value always in the `0 .. NSEC_PER_SEC` range
96 100 let nanoseconds = metadata.mtime_nsec().try_into().unwrap();
97 101 Ok(Self::new_truncate(seconds, nanoseconds, false))
98 102 }
99 103 #[cfg(not(unix))]
100 104 {
101 105 metadata.modified().map(Self::from)
102 106 }
103 107 }
104 108
105 /// Returns whether this timestamp is reliable as the "mtime" of a file.
109 /// Like `for_mtime_of`, but may return `None` or a value with
110 /// `second_ambiguous` set if the mtime is not "reliable".
106 111 ///
107 112 /// A modification time is reliable if it is older than `boundary` (or
108 113 /// sufficiently in the future).
109 114 ///
110 115 /// Otherwise a concurrent modification might happens with the same mtime.
111 pub fn is_reliable_mtime(&self, boundary: &Self) -> bool {
116 pub fn for_reliable_mtime_of(
117 metadata: &fs::Metadata,
118 boundary: &Self,
119 ) -> io::Result<Option<Self>> {
120 let mut mtime = Self::for_mtime_of(metadata)?;
112 121 // If the mtime of the ambiguous file is younger (or equal) to the
113 122 // starting point of the `status` walk, we cannot garantee that
114 123 // another, racy, write will not happen right after with the same mtime
115 124 // and we cannot cache the information.
116 125 //
117 126 // However if the mtime is far away in the future, this is likely some
118 127 // mismatch between the current clock and previous file system
119 128 // operation. So mtime more than one days in the future are considered
120 129 // fine.
121 if self.truncated_seconds == boundary.truncated_seconds {
122 self.nanoseconds != 0
130 let reliable = if mtime.truncated_seconds == boundary.truncated_seconds
131 {
132 mtime.second_ambiguous = true;
133 mtime.nanoseconds != 0
123 134 && boundary.nanoseconds != 0
124 && self.nanoseconds < boundary.nanoseconds
135 && mtime.nanoseconds < boundary.nanoseconds
125 136 } else {
126 137 // `truncated_seconds` is less than 2**31,
127 138 // so this does not overflow `u32`:
128 139 let one_day_later = boundary.truncated_seconds + 24 * 3600;
129 self.truncated_seconds < boundary.truncated_seconds
130 || self.truncated_seconds > one_day_later
140 mtime.truncated_seconds < boundary.truncated_seconds
141 || mtime.truncated_seconds > one_day_later
142 };
143 if reliable {
144 Ok(Some(mtime))
145 } else {
146 Ok(None)
131 147 }
132 148 }
133 149
134 150 /// The lower 31 bits of the number of seconds since the epoch.
135 151 pub fn truncated_seconds(&self) -> u32 {
136 152 self.truncated_seconds
137 153 }
138 154
139 155 /// The sub-second component of this timestamp, in nanoseconds.
140 156 /// Always in the `0 .. 1_000_000_000` range.
141 157 ///
142 158 /// This timestamp is after `(seconds, 0)` by this many nanoseconds.
143 159 pub fn nanoseconds(&self) -> u32 {
144 160 self.nanoseconds
145 161 }
146 162
147 163 /// Returns whether two timestamps are equal modulo 2**31 seconds.
148 164 ///
149 165 /// If this returns `true`, the original values converted from `SystemTime`
150 166 /// or given to `new_truncate` were very likely equal. A false positive is
151 167 /// possible if they were exactly a multiple of 2**31 seconds apart (around
152 168 /// 68 years). This is deemed very unlikely to happen by chance, especially
153 169 /// on filesystems that support sub-second precision.
154 170 ///
155 171 /// If someone is manipulating the modification times of some files to
156 172 /// intentionally make `hg status` return incorrect results, not truncating
157 173 /// wouldn’t help much since they can set exactly the expected timestamp.
158 174 ///
159 175 /// Sub-second precision is ignored if it is zero in either value.
160 176 /// Some APIs simply return zero when more precision is not available.
161 177 /// When comparing values from different sources, if only one is truncated
162 178 /// in that way, doing a simple comparison would cause many false
163 179 /// negatives.
164 180 pub fn likely_equal(self, other: Self) -> bool {
165 181 if self.truncated_seconds != other.truncated_seconds {
166 182 false
167 183 } else if self.nanoseconds == 0 || other.nanoseconds == 0 {
168 184 if self.second_ambiguous {
169 185 false
170 186 } else {
171 187 true
172 188 }
173 189 } else {
174 190 self.nanoseconds == other.nanoseconds
175 191 }
176 192 }
177 193
178 194 pub fn likely_equal_to_mtime_of(
179 195 self,
180 196 metadata: &fs::Metadata,
181 197 ) -> io::Result<bool> {
182 198 Ok(self.likely_equal(Self::for_mtime_of(metadata)?))
183 199 }
184 200 }
185 201
186 202 impl From<SystemTime> for TruncatedTimestamp {
187 203 fn from(system_time: SystemTime) -> Self {
188 204 // On Unix, `SystemTime` is a wrapper for the `timespec` C struct:
189 205 // https://www.gnu.org/software/libc/manual/html_node/Time-Types.html#index-struct-timespec
190 206 // We want to effectively access its fields, but the Rust standard
191 207 // library does not expose them. The best we can do is:
192 208 let seconds;
193 209 let nanoseconds;
194 210 match system_time.duration_since(UNIX_EPOCH) {
195 211 Ok(duration) => {
196 212 seconds = duration.as_secs() as i64;
197 213 nanoseconds = duration.subsec_nanos();
198 214 }
199 215 Err(error) => {
200 216 // `system_time` is before `UNIX_EPOCH`.
201 217 // We need to undo this algorithm:
202 218 // https://github.com/rust-lang/rust/blob/6bed1f0bc3cc50c10aab26d5f94b16a00776b8a5/library/std/src/sys/unix/time.rs#L40-L41
203 219 let negative = error.duration();
204 220 let negative_secs = negative.as_secs() as i64;
205 221 let negative_nanos = negative.subsec_nanos();
206 222 if negative_nanos == 0 {
207 223 seconds = -negative_secs;
208 224 nanoseconds = 0;
209 225 } else {
210 226 // For example if `system_time` was 4.3 seconds before
211 227 // the Unix epoch we get a Duration that represents
212 228 // `(-4, -0.3)` but we want `(-5, +0.7)`:
213 229 seconds = -1 - negative_secs;
214 230 nanoseconds = NSEC_PER_SEC - negative_nanos;
215 231 }
216 232 }
217 233 };
218 234 Self::new_truncate(seconds, nanoseconds, false)
219 235 }
220 236 }
221 237
222 238 const NSEC_PER_SEC: u32 = 1_000_000_000;
223 239 pub const RANGE_MASK_31BIT: u32 = 0x7FFF_FFFF;
224 240
225 241 pub const MTIME_UNSET: i32 = -1;
226 242
227 243 /// A `DirstateEntry` with a size of `-2` means that it was merged from the
228 244 /// other parent. This allows revert to pick the right status back during a
229 245 /// merge.
230 246 pub const SIZE_FROM_OTHER_PARENT: i32 = -2;
231 247 /// A special value used for internal representation of special case in
232 248 /// dirstate v1 format.
233 249 pub const SIZE_NON_NORMAL: i32 = -1;
234 250
235 251 impl DirstateEntry {
236 252 pub fn from_v2_data(
237 253 wdir_tracked: bool,
238 254 p1_tracked: bool,
239 255 p2_info: bool,
240 256 mode_size: Option<(u32, u32)>,
241 257 mtime: Option<TruncatedTimestamp>,
242 258 fallback_exec: Option<bool>,
243 259 fallback_symlink: Option<bool>,
244 260 ) -> Self {
245 261 if let Some((mode, size)) = mode_size {
246 262 // TODO: return an error for out of range values?
247 263 assert!(mode & !RANGE_MASK_31BIT == 0);
248 264 assert!(size & !RANGE_MASK_31BIT == 0);
249 265 }
250 266 let mut flags = Flags::empty();
251 267 flags.set(Flags::WDIR_TRACKED, wdir_tracked);
252 268 flags.set(Flags::P1_TRACKED, p1_tracked);
253 269 flags.set(Flags::P2_INFO, p2_info);
254 270 if let Some(exec) = fallback_exec {
255 271 flags.insert(Flags::HAS_FALLBACK_EXEC);
256 272 if exec {
257 273 flags.insert(Flags::FALLBACK_EXEC);
258 274 }
259 275 }
260 276 if let Some(exec) = fallback_symlink {
261 277 flags.insert(Flags::HAS_FALLBACK_SYMLINK);
262 278 if exec {
263 279 flags.insert(Flags::FALLBACK_SYMLINK);
264 280 }
265 281 }
266 282 Self {
267 283 flags,
268 284 mode_size,
269 285 mtime,
270 286 }
271 287 }
272 288
273 289 pub fn from_v1_data(
274 290 state: EntryState,
275 291 mode: i32,
276 292 size: i32,
277 293 mtime: i32,
278 294 ) -> Self {
279 295 match state {
280 296 EntryState::Normal => {
281 297 if size == SIZE_FROM_OTHER_PARENT {
282 298 Self {
283 299 // might be missing P1_TRACKED
284 300 flags: Flags::WDIR_TRACKED | Flags::P2_INFO,
285 301 mode_size: None,
286 302 mtime: None,
287 303 }
288 304 } else if size == SIZE_NON_NORMAL {
289 305 Self {
290 306 flags: Flags::WDIR_TRACKED | Flags::P1_TRACKED,
291 307 mode_size: None,
292 308 mtime: None,
293 309 }
294 310 } else if mtime == MTIME_UNSET {
295 311 // TODO: return an error for negative values?
296 312 let mode = u32::try_from(mode).unwrap();
297 313 let size = u32::try_from(size).unwrap();
298 314 Self {
299 315 flags: Flags::WDIR_TRACKED | Flags::P1_TRACKED,
300 316 mode_size: Some((mode, size)),
301 317 mtime: None,
302 318 }
303 319 } else {
304 320 // TODO: return an error for negative values?
305 321 let mode = u32::try_from(mode).unwrap();
306 322 let size = u32::try_from(size).unwrap();
307 323 let mtime = u32::try_from(mtime).unwrap();
308 324 let mtime = TruncatedTimestamp::from_already_truncated(
309 325 mtime, 0, false,
310 326 )
311 327 .unwrap();
312 328 Self {
313 329 flags: Flags::WDIR_TRACKED | Flags::P1_TRACKED,
314 330 mode_size: Some((mode, size)),
315 331 mtime: Some(mtime),
316 332 }
317 333 }
318 334 }
319 335 EntryState::Added => Self {
320 336 flags: Flags::WDIR_TRACKED,
321 337 mode_size: None,
322 338 mtime: None,
323 339 },
324 340 EntryState::Removed => Self {
325 341 flags: if size == SIZE_NON_NORMAL {
326 342 Flags::P1_TRACKED | Flags::P2_INFO
327 343 } else if size == SIZE_FROM_OTHER_PARENT {
328 344 // We don’t know if P1_TRACKED should be set (file history)
329 345 Flags::P2_INFO
330 346 } else {
331 347 Flags::P1_TRACKED
332 348 },
333 349 mode_size: None,
334 350 mtime: None,
335 351 },
336 352 EntryState::Merged => Self {
337 353 flags: Flags::WDIR_TRACKED
338 354 | Flags::P1_TRACKED // might not be true because of rename ?
339 355 | Flags::P2_INFO, // might not be true because of rename ?
340 356 mode_size: None,
341 357 mtime: None,
342 358 },
343 359 }
344 360 }
345 361
346 362 /// Creates a new entry in "removed" state.
347 363 ///
348 364 /// `size` is expected to be zero, `SIZE_NON_NORMAL`, or
349 365 /// `SIZE_FROM_OTHER_PARENT`
350 366 pub fn new_removed(size: i32) -> Self {
351 367 Self::from_v1_data(EntryState::Removed, 0, size, 0)
352 368 }
353 369
354 370 pub fn tracked(&self) -> bool {
355 371 self.flags.contains(Flags::WDIR_TRACKED)
356 372 }
357 373
358 374 pub fn p1_tracked(&self) -> bool {
359 375 self.flags.contains(Flags::P1_TRACKED)
360 376 }
361 377
362 378 fn in_either_parent(&self) -> bool {
363 379 self.flags.intersects(Flags::P1_TRACKED | Flags::P2_INFO)
364 380 }
365 381
366 382 pub fn removed(&self) -> bool {
367 383 self.in_either_parent() && !self.flags.contains(Flags::WDIR_TRACKED)
368 384 }
369 385
370 386 pub fn p2_info(&self) -> bool {
371 387 self.flags.contains(Flags::WDIR_TRACKED | Flags::P2_INFO)
372 388 }
373 389
374 390 pub fn added(&self) -> bool {
375 391 self.flags.contains(Flags::WDIR_TRACKED) && !self.in_either_parent()
376 392 }
377 393
378 394 pub fn maybe_clean(&self) -> bool {
379 395 if !self.flags.contains(Flags::WDIR_TRACKED) {
380 396 false
381 397 } else if !self.flags.contains(Flags::P1_TRACKED) {
382 398 false
383 399 } else if self.flags.contains(Flags::P2_INFO) {
384 400 false
385 401 } else {
386 402 true
387 403 }
388 404 }
389 405
390 406 pub fn any_tracked(&self) -> bool {
391 407 self.flags.intersects(
392 408 Flags::WDIR_TRACKED | Flags::P1_TRACKED | Flags::P2_INFO,
393 409 )
394 410 }
395 411
396 412 /// Returns `(wdir_tracked, p1_tracked, p2_info, mode_size, mtime)`
397 413 pub(crate) fn v2_data(
398 414 &self,
399 415 ) -> (
400 416 bool,
401 417 bool,
402 418 bool,
403 419 Option<(u32, u32)>,
404 420 Option<TruncatedTimestamp>,
405 421 Option<bool>,
406 422 Option<bool>,
407 423 ) {
408 424 if !self.any_tracked() {
409 425 // TODO: return an Option instead?
410 426 panic!("Accessing v1_state of an untracked DirstateEntry")
411 427 }
412 428 let wdir_tracked = self.flags.contains(Flags::WDIR_TRACKED);
413 429 let p1_tracked = self.flags.contains(Flags::P1_TRACKED);
414 430 let p2_info = self.flags.contains(Flags::P2_INFO);
415 431 let mode_size = self.mode_size;
416 432 let mtime = self.mtime;
417 433 (
418 434 wdir_tracked,
419 435 p1_tracked,
420 436 p2_info,
421 437 mode_size,
422 438 mtime,
423 439 self.get_fallback_exec(),
424 440 self.get_fallback_symlink(),
425 441 )
426 442 }
427 443
428 444 fn v1_state(&self) -> EntryState {
429 445 if !self.any_tracked() {
430 446 // TODO: return an Option instead?
431 447 panic!("Accessing v1_state of an untracked DirstateEntry")
432 448 }
433 449 if self.removed() {
434 450 EntryState::Removed
435 451 } else if self
436 452 .flags
437 453 .contains(Flags::WDIR_TRACKED | Flags::P1_TRACKED | Flags::P2_INFO)
438 454 {
439 455 EntryState::Merged
440 456 } else if self.added() {
441 457 EntryState::Added
442 458 } else {
443 459 EntryState::Normal
444 460 }
445 461 }
446 462
447 463 fn v1_mode(&self) -> i32 {
448 464 if let Some((mode, _size)) = self.mode_size {
449 465 i32::try_from(mode).unwrap()
450 466 } else {
451 467 0
452 468 }
453 469 }
454 470
455 471 fn v1_size(&self) -> i32 {
456 472 if !self.any_tracked() {
457 473 // TODO: return an Option instead?
458 474 panic!("Accessing v1_size of an untracked DirstateEntry")
459 475 }
460 476 if self.removed()
461 477 && self.flags.contains(Flags::P1_TRACKED | Flags::P2_INFO)
462 478 {
463 479 SIZE_NON_NORMAL
464 480 } else if self.flags.contains(Flags::P2_INFO) {
465 481 SIZE_FROM_OTHER_PARENT
466 482 } else if self.removed() {
467 483 0
468 484 } else if self.added() {
469 485 SIZE_NON_NORMAL
470 486 } else if let Some((_mode, size)) = self.mode_size {
471 487 i32::try_from(size).unwrap()
472 488 } else {
473 489 SIZE_NON_NORMAL
474 490 }
475 491 }
476 492
477 493 fn v1_mtime(&self) -> i32 {
478 494 if !self.any_tracked() {
479 495 // TODO: return an Option instead?
480 496 panic!("Accessing v1_mtime of an untracked DirstateEntry")
481 497 }
482 498 if self.removed() {
483 499 0
484 500 } else if self.flags.contains(Flags::P2_INFO) {
485 501 MTIME_UNSET
486 502 } else if !self.flags.contains(Flags::P1_TRACKED) {
487 503 MTIME_UNSET
488 504 } else if let Some(mtime) = self.mtime {
489 505 if mtime.second_ambiguous {
490 506 MTIME_UNSET
491 507 } else {
492 508 i32::try_from(mtime.truncated_seconds()).unwrap()
493 509 }
494 510 } else {
495 511 MTIME_UNSET
496 512 }
497 513 }
498 514
499 515 // TODO: return `Option<EntryState>`? None when `!self.any_tracked`
500 516 pub fn state(&self) -> EntryState {
501 517 self.v1_state()
502 518 }
503 519
504 520 // TODO: return Option?
505 521 pub fn mode(&self) -> i32 {
506 522 self.v1_mode()
507 523 }
508 524
509 525 // TODO: return Option?
510 526 pub fn size(&self) -> i32 {
511 527 self.v1_size()
512 528 }
513 529
514 530 // TODO: return Option?
515 531 pub fn mtime(&self) -> i32 {
516 532 self.v1_mtime()
517 533 }
518 534
519 535 pub fn get_fallback_exec(&self) -> Option<bool> {
520 536 if self.flags.contains(Flags::HAS_FALLBACK_EXEC) {
521 537 Some(self.flags.contains(Flags::FALLBACK_EXEC))
522 538 } else {
523 539 None
524 540 }
525 541 }
526 542
527 543 pub fn set_fallback_exec(&mut self, value: Option<bool>) {
528 544 match value {
529 545 None => {
530 546 self.flags.remove(Flags::HAS_FALLBACK_EXEC);
531 547 self.flags.remove(Flags::FALLBACK_EXEC);
532 548 }
533 549 Some(exec) => {
534 550 self.flags.insert(Flags::HAS_FALLBACK_EXEC);
535 551 if exec {
536 552 self.flags.insert(Flags::FALLBACK_EXEC);
537 553 }
538 554 }
539 555 }
540 556 }
541 557
542 558 pub fn get_fallback_symlink(&self) -> Option<bool> {
543 559 if self.flags.contains(Flags::HAS_FALLBACK_SYMLINK) {
544 560 Some(self.flags.contains(Flags::FALLBACK_SYMLINK))
545 561 } else {
546 562 None
547 563 }
548 564 }
549 565
550 566 pub fn set_fallback_symlink(&mut self, value: Option<bool>) {
551 567 match value {
552 568 None => {
553 569 self.flags.remove(Flags::HAS_FALLBACK_SYMLINK);
554 570 self.flags.remove(Flags::FALLBACK_SYMLINK);
555 571 }
556 572 Some(symlink) => {
557 573 self.flags.insert(Flags::HAS_FALLBACK_SYMLINK);
558 574 if symlink {
559 575 self.flags.insert(Flags::FALLBACK_SYMLINK);
560 576 }
561 577 }
562 578 }
563 579 }
564 580
565 581 pub fn truncated_mtime(&self) -> Option<TruncatedTimestamp> {
566 582 self.mtime
567 583 }
568 584
569 585 pub fn drop_merge_data(&mut self) {
570 586 if self.flags.contains(Flags::P2_INFO) {
571 587 self.flags.remove(Flags::P2_INFO);
572 588 self.mode_size = None;
573 589 self.mtime = None;
574 590 }
575 591 }
576 592
577 593 pub fn set_possibly_dirty(&mut self) {
578 594 self.mtime = None
579 595 }
580 596
581 597 pub fn set_clean(
582 598 &mut self,
583 599 mode: u32,
584 600 size: u32,
585 601 mtime: TruncatedTimestamp,
586 602 ) {
587 603 let size = size & RANGE_MASK_31BIT;
588 604 self.flags.insert(Flags::WDIR_TRACKED | Flags::P1_TRACKED);
589 605 self.mode_size = Some((mode, size));
590 606 self.mtime = Some(mtime);
591 607 }
592 608
593 609 pub fn set_tracked(&mut self) {
594 610 self.flags.insert(Flags::WDIR_TRACKED);
595 611 // `set_tracked` is replacing various `normallookup` call. So we mark
596 612 // the files as needing lookup
597 613 //
598 614 // Consider dropping this in the future in favor of something less
599 615 // broad.
600 616 self.mtime = None;
601 617 }
602 618
603 619 pub fn set_untracked(&mut self) {
604 620 self.flags.remove(Flags::WDIR_TRACKED);
605 621 self.mode_size = None;
606 622 self.mtime = None;
607 623 }
608 624
609 625 /// Returns `(state, mode, size, mtime)` for the puprose of serialization
610 626 /// in the dirstate-v1 format.
611 627 ///
612 628 /// This includes marker values such as `mtime == -1`. In the future we may
613 629 /// want to not represent these cases that way in memory, but serialization
614 630 /// will need to keep the same format.
615 631 pub fn v1_data(&self) -> (u8, i32, i32, i32) {
616 632 (
617 633 self.v1_state().into(),
618 634 self.v1_mode(),
619 635 self.v1_size(),
620 636 self.v1_mtime(),
621 637 )
622 638 }
623 639
624 640 pub(crate) fn is_from_other_parent(&self) -> bool {
625 641 self.state() == EntryState::Normal
626 642 && self.size() == SIZE_FROM_OTHER_PARENT
627 643 }
628 644
629 645 // TODO: other platforms
630 646 #[cfg(unix)]
631 647 pub fn mode_changed(
632 648 &self,
633 649 filesystem_metadata: &std::fs::Metadata,
634 650 ) -> bool {
635 651 let dirstate_exec_bit = (self.mode() as u32 & EXEC_BIT_MASK) != 0;
636 652 let fs_exec_bit = has_exec_bit(filesystem_metadata);
637 653 dirstate_exec_bit != fs_exec_bit
638 654 }
639 655
640 656 /// Returns a `(state, mode, size, mtime)` tuple as for
641 657 /// `DirstateMapMethods::debug_iter`.
642 658 pub fn debug_tuple(&self) -> (u8, i32, i32, i32) {
643 659 (self.state().into(), self.mode(), self.size(), self.mtime())
644 660 }
645 661 }
646 662
647 663 impl EntryState {
648 664 pub fn is_tracked(self) -> bool {
649 665 use EntryState::*;
650 666 match self {
651 667 Normal | Added | Merged => true,
652 668 Removed => false,
653 669 }
654 670 }
655 671 }
656 672
657 673 impl TryFrom<u8> for EntryState {
658 674 type Error = HgError;
659 675
660 676 fn try_from(value: u8) -> Result<Self, Self::Error> {
661 677 match value {
662 678 b'n' => Ok(EntryState::Normal),
663 679 b'a' => Ok(EntryState::Added),
664 680 b'r' => Ok(EntryState::Removed),
665 681 b'm' => Ok(EntryState::Merged),
666 682 _ => Err(HgError::CorruptedRepository(format!(
667 683 "Incorrect dirstate entry state {}",
668 684 value
669 685 ))),
670 686 }
671 687 }
672 688 }
673 689
674 690 impl Into<u8> for EntryState {
675 691 fn into(self) -> u8 {
676 692 match self {
677 693 EntryState::Normal => b'n',
678 694 EntryState::Added => b'a',
679 695 EntryState::Removed => b'r',
680 696 EntryState::Merged => b'm',
681 697 }
682 698 }
683 699 }
684 700
685 701 const EXEC_BIT_MASK: u32 = 0o100;
686 702
687 703 pub fn has_exec_bit(metadata: &std::fs::Metadata) -> bool {
688 704 // TODO: How to handle executable permissions on Windows?
689 705 use std::os::unix::fs::MetadataExt;
690 706 (metadata.mode() & EXEC_BIT_MASK) != 0
691 707 }
@@ -1,474 +1,478
1 1 // status.rs
2 2 //
3 3 // Copyright 2020, Georges Racinet <georges.racinets@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::error::CommandError;
9 9 use crate::ui::Ui;
10 10 use crate::utils::path_utils::relativize_paths;
11 11 use clap::{Arg, SubCommand};
12 12 use format_bytes::format_bytes;
13 13 use hg;
14 14 use hg::config::Config;
15 15 use hg::dirstate::has_exec_bit;
16 16 use hg::dirstate::TruncatedTimestamp;
17 17 use hg::dirstate::RANGE_MASK_31BIT;
18 18 use hg::errors::{HgError, IoResultExt};
19 19 use hg::lock::LockError;
20 20 use hg::manifest::Manifest;
21 21 use hg::matchers::AlwaysMatcher;
22 22 use hg::repo::Repo;
23 23 use hg::utils::files::get_bytes_from_os_string;
24 24 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
25 25 use hg::{HgPathCow, StatusOptions};
26 26 use log::{info, warn};
27 27 use std::io;
28 28
29 29 pub const HELP_TEXT: &str = "
30 30 Show changed files in the working directory
31 31
32 32 This is a pure Rust version of `hg status`.
33 33
34 34 Some options might be missing, check the list below.
35 35 ";
36 36
37 37 pub fn args() -> clap::App<'static, 'static> {
38 38 SubCommand::with_name("status")
39 39 .alias("st")
40 40 .about(HELP_TEXT)
41 41 .arg(
42 42 Arg::with_name("all")
43 43 .help("show status of all files")
44 44 .short("-A")
45 45 .long("--all"),
46 46 )
47 47 .arg(
48 48 Arg::with_name("modified")
49 49 .help("show only modified files")
50 50 .short("-m")
51 51 .long("--modified"),
52 52 )
53 53 .arg(
54 54 Arg::with_name("added")
55 55 .help("show only added files")
56 56 .short("-a")
57 57 .long("--added"),
58 58 )
59 59 .arg(
60 60 Arg::with_name("removed")
61 61 .help("show only removed files")
62 62 .short("-r")
63 63 .long("--removed"),
64 64 )
65 65 .arg(
66 66 Arg::with_name("clean")
67 67 .help("show only clean files")
68 68 .short("-c")
69 69 .long("--clean"),
70 70 )
71 71 .arg(
72 72 Arg::with_name("deleted")
73 73 .help("show only deleted files")
74 74 .short("-d")
75 75 .long("--deleted"),
76 76 )
77 77 .arg(
78 78 Arg::with_name("unknown")
79 79 .help("show only unknown (not tracked) files")
80 80 .short("-u")
81 81 .long("--unknown"),
82 82 )
83 83 .arg(
84 84 Arg::with_name("ignored")
85 85 .help("show only ignored files")
86 86 .short("-i")
87 87 .long("--ignored"),
88 88 )
89 89 .arg(
90 90 Arg::with_name("no-status")
91 91 .help("hide status prefix")
92 92 .short("-n")
93 93 .long("--no-status"),
94 94 )
95 95 }
96 96
97 97 /// Pure data type allowing the caller to specify file states to display
98 98 #[derive(Copy, Clone, Debug)]
99 99 pub struct DisplayStates {
100 100 pub modified: bool,
101 101 pub added: bool,
102 102 pub removed: bool,
103 103 pub clean: bool,
104 104 pub deleted: bool,
105 105 pub unknown: bool,
106 106 pub ignored: bool,
107 107 }
108 108
109 109 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
110 110 modified: true,
111 111 added: true,
112 112 removed: true,
113 113 clean: false,
114 114 deleted: true,
115 115 unknown: true,
116 116 ignored: false,
117 117 };
118 118
119 119 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
120 120 modified: true,
121 121 added: true,
122 122 removed: true,
123 123 clean: true,
124 124 deleted: true,
125 125 unknown: true,
126 126 ignored: true,
127 127 };
128 128
129 129 impl DisplayStates {
130 130 pub fn is_empty(&self) -> bool {
131 131 !(self.modified
132 132 || self.added
133 133 || self.removed
134 134 || self.clean
135 135 || self.deleted
136 136 || self.unknown
137 137 || self.ignored)
138 138 }
139 139 }
140 140
141 141 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
142 142 let status_enabled_default = false;
143 143 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
144 144 if !status_enabled.unwrap_or(status_enabled_default) {
145 145 return Err(CommandError::unsupported(
146 146 "status is experimental in rhg (enable it with 'rhg.status = true' \
147 147 or enable fallback with 'rhg.on-unsupported = fallback')"
148 148 ));
149 149 }
150 150
151 151 // TODO: lift these limitations
152 152 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
153 153 return Err(CommandError::unsupported(
154 154 "ui.tweakdefaults is not yet supported with rhg status",
155 155 ));
156 156 }
157 157 if invocation.config.get_bool(b"ui", b"statuscopies")? {
158 158 return Err(CommandError::unsupported(
159 159 "ui.statuscopies is not yet supported with rhg status",
160 160 ));
161 161 }
162 162 if invocation
163 163 .config
164 164 .get(b"commands", b"status.terse")
165 165 .is_some()
166 166 {
167 167 return Err(CommandError::unsupported(
168 168 "status.terse is not yet supported with rhg status",
169 169 ));
170 170 }
171 171
172 172 let ui = invocation.ui;
173 173 let config = invocation.config;
174 174 let args = invocation.subcommand_args;
175 175 let display_states = if args.is_present("all") {
176 176 // TODO when implementing `--quiet`: it excludes clean files
177 177 // from `--all`
178 178 ALL_DISPLAY_STATES
179 179 } else {
180 180 let requested = DisplayStates {
181 181 modified: args.is_present("modified"),
182 182 added: args.is_present("added"),
183 183 removed: args.is_present("removed"),
184 184 clean: args.is_present("clean"),
185 185 deleted: args.is_present("deleted"),
186 186 unknown: args.is_present("unknown"),
187 187 ignored: args.is_present("ignored"),
188 188 };
189 189 if requested.is_empty() {
190 190 DEFAULT_DISPLAY_STATES
191 191 } else {
192 192 requested
193 193 }
194 194 };
195 195 let no_status = args.is_present("no-status");
196 196
197 197 let repo = invocation.repo?;
198 198
199 199 if repo.has_sparse() || repo.has_narrow() {
200 200 return Err(CommandError::unsupported(
201 201 "rhg status is not supported for sparse checkouts or narrow clones yet"
202 202 ));
203 203 }
204 204
205 205 let mut dmap = repo.dirstate_map_mut()?;
206 206
207 207 let options = StatusOptions {
208 208 // we're currently supporting file systems with exec flags only
209 209 // anyway
210 210 check_exec: true,
211 211 list_clean: display_states.clean,
212 212 list_unknown: display_states.unknown,
213 213 list_ignored: display_states.ignored,
214 214 collect_traversed_dirs: false,
215 215 };
216 216 let ignore_file = repo.working_directory_vfs().join(".hgignore"); // TODO hardcoded
217 217 let (mut ds_status, pattern_warnings) = dmap.status(
218 218 &AlwaysMatcher,
219 219 repo.working_directory_path().to_owned(),
220 220 vec![ignore_file],
221 221 options,
222 222 )?;
223 223 if !pattern_warnings.is_empty() {
224 224 warn!("Pattern warnings: {:?}", &pattern_warnings);
225 225 }
226 226
227 227 if !ds_status.bad.is_empty() {
228 228 warn!("Bad matches {:?}", &(ds_status.bad))
229 229 }
230 230 if !ds_status.unsure.is_empty() {
231 231 info!(
232 232 "Files to be rechecked by retrieval from filelog: {:?}",
233 233 &ds_status.unsure
234 234 );
235 235 }
236 236 let mut fixup = Vec::new();
237 237 if !ds_status.unsure.is_empty()
238 238 && (display_states.modified || display_states.clean)
239 239 {
240 240 let p1 = repo.dirstate_parents()?.p1;
241 241 let manifest = repo.manifest_for_node(p1).map_err(|e| {
242 242 CommandError::from((e, &*format!("{:x}", p1.short())))
243 243 })?;
244 244 for to_check in ds_status.unsure {
245 245 if unsure_is_modified(repo, &manifest, &to_check)? {
246 246 if display_states.modified {
247 247 ds_status.modified.push(to_check);
248 248 }
249 249 } else {
250 250 if display_states.clean {
251 251 ds_status.clean.push(to_check.clone());
252 252 }
253 253 fixup.push(to_check.into_owned())
254 254 }
255 255 }
256 256 }
257 257 if display_states.modified {
258 258 display_status_paths(
259 259 ui,
260 260 repo,
261 261 config,
262 262 no_status,
263 263 &mut ds_status.modified,
264 264 b"M",
265 265 )?;
266 266 }
267 267 if display_states.added {
268 268 display_status_paths(
269 269 ui,
270 270 repo,
271 271 config,
272 272 no_status,
273 273 &mut ds_status.added,
274 274 b"A",
275 275 )?;
276 276 }
277 277 if display_states.removed {
278 278 display_status_paths(
279 279 ui,
280 280 repo,
281 281 config,
282 282 no_status,
283 283 &mut ds_status.removed,
284 284 b"R",
285 285 )?;
286 286 }
287 287 if display_states.deleted {
288 288 display_status_paths(
289 289 ui,
290 290 repo,
291 291 config,
292 292 no_status,
293 293 &mut ds_status.deleted,
294 294 b"!",
295 295 )?;
296 296 }
297 297 if display_states.unknown {
298 298 display_status_paths(
299 299 ui,
300 300 repo,
301 301 config,
302 302 no_status,
303 303 &mut ds_status.unknown,
304 304 b"?",
305 305 )?;
306 306 }
307 307 if display_states.ignored {
308 308 display_status_paths(
309 309 ui,
310 310 repo,
311 311 config,
312 312 no_status,
313 313 &mut ds_status.ignored,
314 314 b"I",
315 315 )?;
316 316 }
317 317 if display_states.clean {
318 318 display_status_paths(
319 319 ui,
320 320 repo,
321 321 config,
322 322 no_status,
323 323 &mut ds_status.clean,
324 324 b"C",
325 325 )?;
326 326 }
327 327
328 328 let mut dirstate_write_needed = ds_status.dirty;
329 329 let filesystem_time_at_status_start = ds_status
330 330 .filesystem_time_at_status_start
331 331 .map(TruncatedTimestamp::from);
332 332
333 333 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
334 334 && !dirstate_write_needed
335 335 {
336 336 // Nothing to update
337 337 return Ok(());
338 338 }
339 339
340 340 // Update the dirstate on disk if we can
341 341 let with_lock_result =
342 342 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
343 343 if let Some(mtime_boundary) = filesystem_time_at_status_start {
344 344 for hg_path in fixup {
345 345 use std::os::unix::fs::MetadataExt;
346 346 let fs_path = hg_path_to_path_buf(&hg_path)
347 347 .expect("HgPath conversion");
348 348 // Specifically do not reuse `fs_metadata` from
349 349 // `unsure_is_clean` which was needed before reading
350 350 // contents. Here we access metadata again after reading
351 351 // content, in case it changed in the meantime.
352 352 let fs_metadata = repo
353 353 .working_directory_vfs()
354 354 .symlink_metadata(&fs_path)?;
355 let mtime = TruncatedTimestamp::for_mtime_of(&fs_metadata)
356 .when_reading_file(&fs_path)?;
357 if mtime.is_reliable_mtime(&mtime_boundary) {
355 if let Some(mtime) =
356 TruncatedTimestamp::for_reliable_mtime_of(
357 &fs_metadata,
358 &mtime_boundary,
359 )
360 .when_reading_file(&fs_path)?
361 {
358 362 let mode = fs_metadata.mode();
359 363 let size = fs_metadata.len() as u32 & RANGE_MASK_31BIT;
360 364 let mut entry = dmap
361 365 .get(&hg_path)?
362 366 .expect("ambiguous file not in dirstate");
363 367 entry.set_clean(mode, size, mtime);
364 368 dmap.add_file(&hg_path, entry)?;
365 369 dirstate_write_needed = true
366 370 }
367 371 }
368 372 }
369 373 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
370 374 if dirstate_write_needed {
371 375 repo.write_dirstate()?
372 376 }
373 377 Ok(())
374 378 });
375 379 match with_lock_result {
376 380 Ok(closure_result) => closure_result?,
377 381 Err(LockError::AlreadyHeld) => {
378 382 // Not updating the dirstate is not ideal but not critical:
379 383 // don’t keep our caller waiting until some other Mercurial
380 384 // process releases the lock.
381 385 }
382 386 Err(LockError::Other(HgError::IoError { error, .. }))
383 387 if error.kind() == io::ErrorKind::PermissionDenied =>
384 388 {
385 389 // `hg status` on a read-only repository is fine
386 390 }
387 391 Err(LockError::Other(error)) => {
388 392 // Report other I/O errors
389 393 Err(error)?
390 394 }
391 395 }
392 396 Ok(())
393 397 }
394 398
395 399 // Probably more elegant to use a Deref or Borrow trait rather than
396 400 // harcode HgPathBuf, but probably not really useful at this point
397 401 fn display_status_paths(
398 402 ui: &Ui,
399 403 repo: &Repo,
400 404 config: &Config,
401 405 no_status: bool,
402 406 paths: &mut [HgPathCow],
403 407 status_prefix: &[u8],
404 408 ) -> Result<(), CommandError> {
405 409 paths.sort_unstable();
406 410 let mut relative: bool = config.get_bool(b"ui", b"relative-paths")?;
407 411 relative = config
408 412 .get_option(b"commands", b"status.relative")?
409 413 .unwrap_or(relative);
410 414 let print_path = |path: &[u8]| {
411 415 // TODO optim, probably lots of unneeded copies here, especially
412 416 // if out stream is buffered
413 417 if no_status {
414 418 ui.write_stdout(&format_bytes!(b"{}\n", path))
415 419 } else {
416 420 ui.write_stdout(&format_bytes!(b"{} {}\n", status_prefix, path))
417 421 }
418 422 };
419 423
420 424 if relative && !ui.plain() {
421 425 relativize_paths(repo, paths.iter().map(Ok), |path| {
422 426 print_path(&path)
423 427 })?;
424 428 } else {
425 429 for path in paths {
426 430 print_path(path.as_bytes())?
427 431 }
428 432 }
429 433 Ok(())
430 434 }
431 435
432 436 /// Check if a file is modified by comparing actual repo store and file system.
433 437 ///
434 438 /// This meant to be used for those that the dirstate cannot resolve, due
435 439 /// to time resolution limits.
436 440 fn unsure_is_modified(
437 441 repo: &Repo,
438 442 manifest: &Manifest,
439 443 hg_path: &HgPath,
440 444 ) -> Result<bool, HgError> {
441 445 let vfs = repo.working_directory_vfs();
442 446 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
443 447 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
444 448 let is_symlink = fs_metadata.file_type().is_symlink();
445 449 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
446 450 // dirstate
447 451 let fs_flags = if is_symlink {
448 452 Some(b'l')
449 453 } else if has_exec_bit(&fs_metadata) {
450 454 Some(b'x')
451 455 } else {
452 456 None
453 457 };
454 458
455 459 let entry = manifest
456 460 .find_file(hg_path)?
457 461 .expect("ambgious file not in p1");
458 462 if entry.flags != fs_flags {
459 463 return Ok(true);
460 464 }
461 465 let filelog = repo.filelog(hg_path)?;
462 466 let filelog_entry =
463 467 filelog.data_for_node(entry.node_id()?).map_err(|_| {
464 468 HgError::corrupted("filelog missing node from manifest")
465 469 })?;
466 470 let contents_in_p1 = filelog_entry.data()?;
467 471
468 472 let fs_contents = if is_symlink {
469 473 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
470 474 } else {
471 475 vfs.read(fs_path)?
472 476 };
473 477 Ok(contents_in_p1 != &*fs_contents)
474 478 }
General Comments 0
You need to be logged in to leave comments. Login now