##// END OF EJS Templates
rhg: Fix status desambiguation of symlinks and executable files...
Simon Sapin -
r49168:d5a91701 default
parent child Browse files
Show More
@@ -1,643 +1,649 b''
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 }
47 47
48 48 impl TruncatedTimestamp {
49 49 /// Constructs from a timestamp potentially outside of the supported range,
50 50 /// and truncate the seconds components to its lower 31 bits.
51 51 ///
52 52 /// Panics if the nanoseconds components is not in the expected range.
53 53 pub fn new_truncate(seconds: i64, nanoseconds: u32) -> Self {
54 54 assert!(nanoseconds < NSEC_PER_SEC);
55 55 Self {
56 56 truncated_seconds: seconds as u32 & RANGE_MASK_31BIT,
57 57 nanoseconds,
58 58 }
59 59 }
60 60
61 61 /// Construct from components. Returns an error if they are not in the
62 62 /// expcted range.
63 63 pub fn from_already_truncated(
64 64 truncated_seconds: u32,
65 65 nanoseconds: u32,
66 66 ) -> Result<Self, DirstateV2ParseError> {
67 67 if truncated_seconds & !RANGE_MASK_31BIT == 0
68 68 && nanoseconds < NSEC_PER_SEC
69 69 {
70 70 Ok(Self {
71 71 truncated_seconds,
72 72 nanoseconds,
73 73 })
74 74 } else {
75 75 Err(DirstateV2ParseError)
76 76 }
77 77 }
78 78
79 79 pub fn for_mtime_of(metadata: &fs::Metadata) -> io::Result<Self> {
80 80 #[cfg(unix)]
81 81 {
82 82 use std::os::unix::fs::MetadataExt;
83 83 let seconds = metadata.mtime();
84 84 // i64Β -> u32 with value always in the `0 .. NSEC_PER_SEC` range
85 85 let nanoseconds = metadata.mtime_nsec().try_into().unwrap();
86 86 Ok(Self::new_truncate(seconds, nanoseconds))
87 87 }
88 88 #[cfg(not(unix))]
89 89 {
90 90 metadata.modified().map(Self::from)
91 91 }
92 92 }
93 93
94 94 /// The lower 31 bits of the number of seconds since the epoch.
95 95 pub fn truncated_seconds(&self) -> u32 {
96 96 self.truncated_seconds
97 97 }
98 98
99 99 /// The sub-second component of this timestamp, in nanoseconds.
100 100 /// Always in the `0 .. 1_000_000_000` range.
101 101 ///
102 102 /// This timestamp is after `(seconds, 0)` by this many nanoseconds.
103 103 pub fn nanoseconds(&self) -> u32 {
104 104 self.nanoseconds
105 105 }
106 106
107 107 /// Returns whether two timestamps are equal modulo 2**31 seconds.
108 108 ///
109 109 /// If this returns `true`, the original values converted from `SystemTime`
110 110 /// or given to `new_truncate` were very likely equal. A false positive is
111 111 /// possible if they were exactly a multiple of 2**31 seconds apart (around
112 112 /// 68 years). This is deemed very unlikely to happen by chance, especially
113 113 /// on filesystems that support sub-second precision.
114 114 ///
115 115 /// If someone is manipulating the modification times of some files to
116 116 /// intentionally make `hg status` return incorrect results, not truncating
117 117 /// wouldn’t help much since they can set exactly the expected timestamp.
118 118 ///
119 119 /// Sub-second precision is ignored if it is zero in either value.
120 120 /// Some APIs simply return zero when more precision is not available.
121 121 /// When comparing values from different sources, if only one is truncated
122 122 /// in that way, doing a simple comparison would cause many false
123 123 /// negatives.
124 124 pub fn likely_equal(self, other: Self) -> bool {
125 125 self.truncated_seconds == other.truncated_seconds
126 126 && (self.nanoseconds == other.nanoseconds
127 127 || self.nanoseconds == 0
128 128 || other.nanoseconds == 0)
129 129 }
130 130
131 131 pub fn likely_equal_to_mtime_of(
132 132 self,
133 133 metadata: &fs::Metadata,
134 134 ) -> io::Result<bool> {
135 135 Ok(self.likely_equal(Self::for_mtime_of(metadata)?))
136 136 }
137 137 }
138 138
139 139 impl From<SystemTime> for TruncatedTimestamp {
140 140 fn from(system_time: SystemTime) -> Self {
141 141 // On Unix, `SystemTime` is a wrapper for the `timespec` C struct:
142 142 // https://www.gnu.org/software/libc/manual/html_node/Time-Types.html#index-struct-timespec
143 143 // We want to effectively access its fields, but the Rust standard
144 144 // library does not expose them. The best we can do is:
145 145 let seconds;
146 146 let nanoseconds;
147 147 match system_time.duration_since(UNIX_EPOCH) {
148 148 Ok(duration) => {
149 149 seconds = duration.as_secs() as i64;
150 150 nanoseconds = duration.subsec_nanos();
151 151 }
152 152 Err(error) => {
153 153 // `system_time` is before `UNIX_EPOCH`.
154 154 // We need to undo this algorithm:
155 155 // https://github.com/rust-lang/rust/blob/6bed1f0bc3cc50c10aab26d5f94b16a00776b8a5/library/std/src/sys/unix/time.rs#L40-L41
156 156 let negative = error.duration();
157 157 let negative_secs = negative.as_secs() as i64;
158 158 let negative_nanos = negative.subsec_nanos();
159 159 if negative_nanos == 0 {
160 160 seconds = -negative_secs;
161 161 nanoseconds = 0;
162 162 } else {
163 163 // For example if `system_time` was 4.3Β seconds before
164 164 // the Unix epoch we get a Duration that represents
165 165 // `(-4, -0.3)` but we want `(-5, +0.7)`:
166 166 seconds = -1 - negative_secs;
167 167 nanoseconds = NSEC_PER_SEC - negative_nanos;
168 168 }
169 169 }
170 170 };
171 171 Self::new_truncate(seconds, nanoseconds)
172 172 }
173 173 }
174 174
175 175 const NSEC_PER_SEC: u32 = 1_000_000_000;
176 176 const RANGE_MASK_31BIT: u32 = 0x7FFF_FFFF;
177 177
178 178 pub const MTIME_UNSET: i32 = -1;
179 179
180 180 /// A `DirstateEntry` with a size of `-2` means that it was merged from the
181 181 /// other parent. This allows revert to pick the right status back during a
182 182 /// merge.
183 183 pub const SIZE_FROM_OTHER_PARENT: i32 = -2;
184 184 /// A special value used for internal representation of special case in
185 185 /// dirstate v1 format.
186 186 pub const SIZE_NON_NORMAL: i32 = -1;
187 187
188 188 impl DirstateEntry {
189 189 pub fn from_v2_data(
190 190 wdir_tracked: bool,
191 191 p1_tracked: bool,
192 192 p2_info: bool,
193 193 mode_size: Option<(u32, u32)>,
194 194 mtime: Option<TruncatedTimestamp>,
195 195 fallback_exec: Option<bool>,
196 196 fallback_symlink: Option<bool>,
197 197 ) -> Self {
198 198 if let Some((mode, size)) = mode_size {
199 199 // TODO: return an error for out of range values?
200 200 assert!(mode & !RANGE_MASK_31BIT == 0);
201 201 assert!(size & !RANGE_MASK_31BIT == 0);
202 202 }
203 203 let mut flags = Flags::empty();
204 204 flags.set(Flags::WDIR_TRACKED, wdir_tracked);
205 205 flags.set(Flags::P1_TRACKED, p1_tracked);
206 206 flags.set(Flags::P2_INFO, p2_info);
207 207 if let Some(exec) = fallback_exec {
208 208 flags.insert(Flags::HAS_FALLBACK_EXEC);
209 209 if exec {
210 210 flags.insert(Flags::FALLBACK_EXEC);
211 211 }
212 212 }
213 213 if let Some(exec) = fallback_symlink {
214 214 flags.insert(Flags::HAS_FALLBACK_SYMLINK);
215 215 if exec {
216 216 flags.insert(Flags::FALLBACK_SYMLINK);
217 217 }
218 218 }
219 219 Self {
220 220 flags,
221 221 mode_size,
222 222 mtime,
223 223 }
224 224 }
225 225
226 226 pub fn from_v1_data(
227 227 state: EntryState,
228 228 mode: i32,
229 229 size: i32,
230 230 mtime: i32,
231 231 ) -> Self {
232 232 match state {
233 233 EntryState::Normal => {
234 234 if size == SIZE_FROM_OTHER_PARENT {
235 235 Self {
236 236 // might be missing P1_TRACKED
237 237 flags: Flags::WDIR_TRACKED | Flags::P2_INFO,
238 238 mode_size: None,
239 239 mtime: None,
240 240 }
241 241 } else if size == SIZE_NON_NORMAL {
242 242 Self {
243 243 flags: Flags::WDIR_TRACKED | Flags::P1_TRACKED,
244 244 mode_size: None,
245 245 mtime: None,
246 246 }
247 247 } else if mtime == MTIME_UNSET {
248 248 // TODO:Β return an error for negative values?
249 249 let mode = u32::try_from(mode).unwrap();
250 250 let size = u32::try_from(size).unwrap();
251 251 Self {
252 252 flags: Flags::WDIR_TRACKED | Flags::P1_TRACKED,
253 253 mode_size: Some((mode, size)),
254 254 mtime: None,
255 255 }
256 256 } else {
257 257 // TODO:Β return an error for negative values?
258 258 let mode = u32::try_from(mode).unwrap();
259 259 let size = u32::try_from(size).unwrap();
260 260 let mtime = u32::try_from(mtime).unwrap();
261 261 let mtime =
262 262 TruncatedTimestamp::from_already_truncated(mtime, 0)
263 263 .unwrap();
264 264 Self {
265 265 flags: Flags::WDIR_TRACKED | Flags::P1_TRACKED,
266 266 mode_size: Some((mode, size)),
267 267 mtime: Some(mtime),
268 268 }
269 269 }
270 270 }
271 271 EntryState::Added => Self {
272 272 flags: Flags::WDIR_TRACKED,
273 273 mode_size: None,
274 274 mtime: None,
275 275 },
276 276 EntryState::Removed => Self {
277 277 flags: if size == SIZE_NON_NORMAL {
278 278 Flags::P1_TRACKED | Flags::P2_INFO
279 279 } else if size == SIZE_FROM_OTHER_PARENT {
280 280 // We don’t know if P1_TRACKED should be set (file history)
281 281 Flags::P2_INFO
282 282 } else {
283 283 Flags::P1_TRACKED
284 284 },
285 285 mode_size: None,
286 286 mtime: None,
287 287 },
288 288 EntryState::Merged => Self {
289 289 flags: Flags::WDIR_TRACKED
290 290 | Flags::P1_TRACKED // might not be true because of rename ?
291 291 | Flags::P2_INFO, // might not be true because of rename ?
292 292 mode_size: None,
293 293 mtime: None,
294 294 },
295 295 }
296 296 }
297 297
298 298 /// Creates a new entry in "removed" state.
299 299 ///
300 300 /// `size` is expected to be zero, `SIZE_NON_NORMAL`, or
301 301 /// `SIZE_FROM_OTHER_PARENT`
302 302 pub fn new_removed(size: i32) -> Self {
303 303 Self::from_v1_data(EntryState::Removed, 0, size, 0)
304 304 }
305 305
306 306 pub fn tracked(&self) -> bool {
307 307 self.flags.contains(Flags::WDIR_TRACKED)
308 308 }
309 309
310 310 pub fn p1_tracked(&self) -> bool {
311 311 self.flags.contains(Flags::P1_TRACKED)
312 312 }
313 313
314 314 fn in_either_parent(&self) -> bool {
315 315 self.flags.intersects(Flags::P1_TRACKED | Flags::P2_INFO)
316 316 }
317 317
318 318 pub fn removed(&self) -> bool {
319 319 self.in_either_parent() && !self.flags.contains(Flags::WDIR_TRACKED)
320 320 }
321 321
322 322 pub fn p2_info(&self) -> bool {
323 323 self.flags.contains(Flags::WDIR_TRACKED | Flags::P2_INFO)
324 324 }
325 325
326 326 pub fn added(&self) -> bool {
327 327 self.flags.contains(Flags::WDIR_TRACKED) && !self.in_either_parent()
328 328 }
329 329
330 330 pub fn maybe_clean(&self) -> bool {
331 331 if !self.flags.contains(Flags::WDIR_TRACKED) {
332 332 false
333 333 } else if !self.flags.contains(Flags::P1_TRACKED) {
334 334 false
335 335 } else if self.flags.contains(Flags::P2_INFO) {
336 336 false
337 337 } else {
338 338 true
339 339 }
340 340 }
341 341
342 342 pub fn any_tracked(&self) -> bool {
343 343 self.flags.intersects(
344 344 Flags::WDIR_TRACKED | Flags::P1_TRACKED | Flags::P2_INFO,
345 345 )
346 346 }
347 347
348 348 /// Returns `(wdir_tracked, p1_tracked, p2_info, mode_size, mtime)`
349 349 pub(crate) fn v2_data(
350 350 &self,
351 351 ) -> (
352 352 bool,
353 353 bool,
354 354 bool,
355 355 Option<(u32, u32)>,
356 356 Option<TruncatedTimestamp>,
357 357 Option<bool>,
358 358 Option<bool>,
359 359 ) {
360 360 if !self.any_tracked() {
361 361 // TODO: return an Option instead?
362 362 panic!("Accessing v1_state of an untracked DirstateEntry")
363 363 }
364 364 let wdir_tracked = self.flags.contains(Flags::WDIR_TRACKED);
365 365 let p1_tracked = self.flags.contains(Flags::P1_TRACKED);
366 366 let p2_info = self.flags.contains(Flags::P2_INFO);
367 367 let mode_size = self.mode_size;
368 368 let mtime = self.mtime;
369 369 (
370 370 wdir_tracked,
371 371 p1_tracked,
372 372 p2_info,
373 373 mode_size,
374 374 mtime,
375 375 self.get_fallback_exec(),
376 376 self.get_fallback_symlink(),
377 377 )
378 378 }
379 379
380 380 fn v1_state(&self) -> EntryState {
381 381 if !self.any_tracked() {
382 382 // TODO: return an Option instead?
383 383 panic!("Accessing v1_state of an untracked DirstateEntry")
384 384 }
385 385 if self.removed() {
386 386 EntryState::Removed
387 387 } else if self
388 388 .flags
389 389 .contains(Flags::WDIR_TRACKED | Flags::P1_TRACKED | Flags::P2_INFO)
390 390 {
391 391 EntryState::Merged
392 392 } else if self.added() {
393 393 EntryState::Added
394 394 } else {
395 395 EntryState::Normal
396 396 }
397 397 }
398 398
399 399 fn v1_mode(&self) -> i32 {
400 400 if let Some((mode, _size)) = self.mode_size {
401 401 i32::try_from(mode).unwrap()
402 402 } else {
403 403 0
404 404 }
405 405 }
406 406
407 407 fn v1_size(&self) -> i32 {
408 408 if !self.any_tracked() {
409 409 // TODO: return an Option instead?
410 410 panic!("Accessing v1_size of an untracked DirstateEntry")
411 411 }
412 412 if self.removed()
413 413 && self.flags.contains(Flags::P1_TRACKED | Flags::P2_INFO)
414 414 {
415 415 SIZE_NON_NORMAL
416 416 } else if self.flags.contains(Flags::P2_INFO) {
417 417 SIZE_FROM_OTHER_PARENT
418 418 } else if self.removed() {
419 419 0
420 420 } else if self.added() {
421 421 SIZE_NON_NORMAL
422 422 } else if let Some((_mode, size)) = self.mode_size {
423 423 i32::try_from(size).unwrap()
424 424 } else {
425 425 SIZE_NON_NORMAL
426 426 }
427 427 }
428 428
429 429 fn v1_mtime(&self) -> i32 {
430 430 if !self.any_tracked() {
431 431 // TODO: return an Option instead?
432 432 panic!("Accessing v1_mtime of an untracked DirstateEntry")
433 433 }
434 434 if self.removed() {
435 435 0
436 436 } else if self.flags.contains(Flags::P2_INFO) {
437 437 MTIME_UNSET
438 438 } else if !self.flags.contains(Flags::P1_TRACKED) {
439 439 MTIME_UNSET
440 440 } else if let Some(mtime) = self.mtime {
441 441 i32::try_from(mtime.truncated_seconds()).unwrap()
442 442 } else {
443 443 MTIME_UNSET
444 444 }
445 445 }
446 446
447 447 // TODO: return `Option<EntryState>`? None when `!self.any_tracked`
448 448 pub fn state(&self) -> EntryState {
449 449 self.v1_state()
450 450 }
451 451
452 452 // TODO: return Option?
453 453 pub fn mode(&self) -> i32 {
454 454 self.v1_mode()
455 455 }
456 456
457 457 // TODO: return Option?
458 458 pub fn size(&self) -> i32 {
459 459 self.v1_size()
460 460 }
461 461
462 462 // TODO: return Option?
463 463 pub fn mtime(&self) -> i32 {
464 464 self.v1_mtime()
465 465 }
466 466
467 467 pub fn get_fallback_exec(&self) -> Option<bool> {
468 468 if self.flags.contains(Flags::HAS_FALLBACK_EXEC) {
469 469 Some(self.flags.contains(Flags::FALLBACK_EXEC))
470 470 } else {
471 471 None
472 472 }
473 473 }
474 474
475 475 pub fn set_fallback_exec(&mut self, value: Option<bool>) {
476 476 match value {
477 477 None => {
478 478 self.flags.remove(Flags::HAS_FALLBACK_EXEC);
479 479 self.flags.remove(Flags::FALLBACK_EXEC);
480 480 }
481 481 Some(exec) => {
482 482 self.flags.insert(Flags::HAS_FALLBACK_EXEC);
483 483 if exec {
484 484 self.flags.insert(Flags::FALLBACK_EXEC);
485 485 }
486 486 }
487 487 }
488 488 }
489 489
490 490 pub fn get_fallback_symlink(&self) -> Option<bool> {
491 491 if self.flags.contains(Flags::HAS_FALLBACK_SYMLINK) {
492 492 Some(self.flags.contains(Flags::FALLBACK_SYMLINK))
493 493 } else {
494 494 None
495 495 }
496 496 }
497 497
498 498 pub fn set_fallback_symlink(&mut self, value: Option<bool>) {
499 499 match value {
500 500 None => {
501 501 self.flags.remove(Flags::HAS_FALLBACK_SYMLINK);
502 502 self.flags.remove(Flags::FALLBACK_SYMLINK);
503 503 }
504 504 Some(symlink) => {
505 505 self.flags.insert(Flags::HAS_FALLBACK_SYMLINK);
506 506 if symlink {
507 507 self.flags.insert(Flags::FALLBACK_SYMLINK);
508 508 }
509 509 }
510 510 }
511 511 }
512 512
513 513 pub fn truncated_mtime(&self) -> Option<TruncatedTimestamp> {
514 514 self.mtime
515 515 }
516 516
517 517 pub fn drop_merge_data(&mut self) {
518 518 if self.flags.contains(Flags::P2_INFO) {
519 519 self.flags.remove(Flags::P2_INFO);
520 520 self.mode_size = None;
521 521 self.mtime = None;
522 522 }
523 523 }
524 524
525 525 pub fn set_possibly_dirty(&mut self) {
526 526 self.mtime = None
527 527 }
528 528
529 529 pub fn set_clean(
530 530 &mut self,
531 531 mode: u32,
532 532 size: u32,
533 533 mtime: TruncatedTimestamp,
534 534 ) {
535 535 let size = size & RANGE_MASK_31BIT;
536 536 self.flags.insert(Flags::WDIR_TRACKED | Flags::P1_TRACKED);
537 537 self.mode_size = Some((mode, size));
538 538 self.mtime = Some(mtime);
539 539 }
540 540
541 541 pub fn set_tracked(&mut self) {
542 542 self.flags.insert(Flags::WDIR_TRACKED);
543 543 // `set_tracked` is replacing various `normallookup` call. So we mark
544 544 // the files as needing lookup
545 545 //
546 546 // Consider dropping this in the future in favor of something less
547 547 // broad.
548 548 self.mtime = None;
549 549 }
550 550
551 551 pub fn set_untracked(&mut self) {
552 552 self.flags.remove(Flags::WDIR_TRACKED);
553 553 self.mode_size = None;
554 554 self.mtime = None;
555 555 }
556 556
557 557 /// Returns `(state, mode, size, mtime)` for the puprose of serialization
558 558 /// in the dirstate-v1 format.
559 559 ///
560 560 /// This includes marker values such as `mtime == -1`. In the future we may
561 561 /// want to not represent these cases that way in memory, but serialization
562 562 /// will need to keep the same format.
563 563 pub fn v1_data(&self) -> (u8, i32, i32, i32) {
564 564 (
565 565 self.v1_state().into(),
566 566 self.v1_mode(),
567 567 self.v1_size(),
568 568 self.v1_mtime(),
569 569 )
570 570 }
571 571
572 572 pub(crate) fn is_from_other_parent(&self) -> bool {
573 573 self.state() == EntryState::Normal
574 574 && self.size() == SIZE_FROM_OTHER_PARENT
575 575 }
576 576
577 577 // TODO: other platforms
578 578 #[cfg(unix)]
579 579 pub fn mode_changed(
580 580 &self,
581 581 filesystem_metadata: &std::fs::Metadata,
582 582 ) -> bool {
583 use std::os::unix::fs::MetadataExt;
584 const EXEC_BIT_MASK: u32 = 0o100;
585 let dirstate_exec_bit = (self.mode() as u32) & EXEC_BIT_MASK;
586 let fs_exec_bit = filesystem_metadata.mode() & EXEC_BIT_MASK;
583 let dirstate_exec_bit = (self.mode() as u32 & EXEC_BIT_MASK) != 0;
584 let fs_exec_bit = has_exec_bit(filesystem_metadata);
587 585 dirstate_exec_bit != fs_exec_bit
588 586 }
589 587
590 588 /// Returns a `(state, mode, size, mtime)` tuple as for
591 589 /// `DirstateMapMethods::debug_iter`.
592 590 pub fn debug_tuple(&self) -> (u8, i32, i32, i32) {
593 591 (self.state().into(), self.mode(), self.size(), self.mtime())
594 592 }
595 593
596 594 /// True if the stored mtime would be ambiguous with the current time
597 595 pub fn need_delay(&self, now: TruncatedTimestamp) -> bool {
598 596 if let Some(mtime) = self.mtime {
599 597 self.state() == EntryState::Normal
600 598 && mtime.truncated_seconds() == now.truncated_seconds()
601 599 } else {
602 600 false
603 601 }
604 602 }
605 603 }
606 604
607 605 impl EntryState {
608 606 pub fn is_tracked(self) -> bool {
609 607 use EntryState::*;
610 608 match self {
611 609 Normal | Added | Merged => true,
612 610 Removed => false,
613 611 }
614 612 }
615 613 }
616 614
617 615 impl TryFrom<u8> for EntryState {
618 616 type Error = HgError;
619 617
620 618 fn try_from(value: u8) -> Result<Self, Self::Error> {
621 619 match value {
622 620 b'n' => Ok(EntryState::Normal),
623 621 b'a' => Ok(EntryState::Added),
624 622 b'r' => Ok(EntryState::Removed),
625 623 b'm' => Ok(EntryState::Merged),
626 624 _ => Err(HgError::CorruptedRepository(format!(
627 625 "Incorrect dirstate entry state {}",
628 626 value
629 627 ))),
630 628 }
631 629 }
632 630 }
633 631
634 632 impl Into<u8> for EntryState {
635 633 fn into(self) -> u8 {
636 634 match self {
637 635 EntryState::Normal => b'n',
638 636 EntryState::Added => b'a',
639 637 EntryState::Removed => b'r',
640 638 EntryState::Merged => b'm',
641 639 }
642 640 }
643 641 }
642
643 const EXEC_BIT_MASK: u32 = 0o100;
644
645 pub fn has_exec_bit(metadata: &std::fs::Metadata) -> bool {
646 // TODO: How to handle executable permissions on Windows?
647 use std::os::unix::fs::MetadataExt;
648 (metadata.mode() & EXEC_BIT_MASK) != 0
649 }
@@ -1,100 +1,116 b''
1 1 use crate::errors::{HgError, IoErrorContext, IoResultExt};
2 2 use memmap2::{Mmap, MmapOptions};
3 3 use std::io::ErrorKind;
4 4 use std::path::{Path, PathBuf};
5 5
6 6 /// Filesystem access abstraction for the contents of a given "base" diretory
7 7 #[derive(Clone, Copy)]
8 8 pub struct Vfs<'a> {
9 9 pub(crate) base: &'a Path,
10 10 }
11 11
12 12 struct FileNotFound(std::io::Error, PathBuf);
13 13
14 14 impl Vfs<'_> {
15 15 pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
16 16 self.base.join(relative_path)
17 17 }
18 18
19 pub fn symlink_metadata(
20 &self,
21 relative_path: impl AsRef<Path>,
22 ) -> Result<std::fs::Metadata, HgError> {
23 let path = self.join(relative_path);
24 std::fs::symlink_metadata(&path).when_reading_file(&path)
25 }
26
27 pub fn read_link(
28 &self,
29 relative_path: impl AsRef<Path>,
30 ) -> Result<PathBuf, HgError> {
31 let path = self.join(relative_path);
32 std::fs::read_link(&path).when_reading_file(&path)
33 }
34
19 35 pub fn read(
20 36 &self,
21 37 relative_path: impl AsRef<Path>,
22 38 ) -> Result<Vec<u8>, HgError> {
23 39 let path = self.join(relative_path);
24 40 std::fs::read(&path).when_reading_file(&path)
25 41 }
26 42
27 43 fn mmap_open_gen(
28 44 &self,
29 45 relative_path: impl AsRef<Path>,
30 46 ) -> Result<Result<Mmap, FileNotFound>, HgError> {
31 47 let path = self.join(relative_path);
32 48 let file = match std::fs::File::open(&path) {
33 49 Err(err) => {
34 50 if let ErrorKind::NotFound = err.kind() {
35 51 return Ok(Err(FileNotFound(err, path)));
36 52 };
37 53 return (Err(err)).when_reading_file(&path);
38 54 }
39 55 Ok(file) => file,
40 56 };
41 57 // TODO: what are the safety requirements here?
42 58 let mmap = unsafe { MmapOptions::new().map(&file) }
43 59 .when_reading_file(&path)?;
44 60 Ok(Ok(mmap))
45 61 }
46 62
47 63 pub fn mmap_open_opt(
48 64 &self,
49 65 relative_path: impl AsRef<Path>,
50 66 ) -> Result<Option<Mmap>, HgError> {
51 67 self.mmap_open_gen(relative_path).map(|res| res.ok())
52 68 }
53 69
54 70 pub fn mmap_open(
55 71 &self,
56 72 relative_path: impl AsRef<Path>,
57 73 ) -> Result<Mmap, HgError> {
58 74 match self.mmap_open_gen(relative_path)? {
59 75 Err(FileNotFound(err, path)) => Err(err).when_reading_file(&path),
60 76 Ok(res) => Ok(res),
61 77 }
62 78 }
63 79
64 80 pub fn rename(
65 81 &self,
66 82 relative_from: impl AsRef<Path>,
67 83 relative_to: impl AsRef<Path>,
68 84 ) -> Result<(), HgError> {
69 85 let from = self.join(relative_from);
70 86 let to = self.join(relative_to);
71 87 std::fs::rename(&from, &to)
72 88 .with_context(|| IoErrorContext::RenamingFile { from, to })
73 89 }
74 90 }
75 91
76 92 fn fs_metadata(
77 93 path: impl AsRef<Path>,
78 94 ) -> Result<Option<std::fs::Metadata>, HgError> {
79 95 let path = path.as_ref();
80 96 match std::fs::metadata(path) {
81 97 Ok(meta) => Ok(Some(meta)),
82 98 Err(error) => match error.kind() {
83 99 // TODO: when we require a Rust version where `NotADirectory` is
84 100 // stable, invert this logic and return None for it and `NotFound`
85 101 // and propagate any other error.
86 102 ErrorKind::PermissionDenied => Err(error).with_context(|| {
87 103 IoErrorContext::ReadingMetadata(path.to_owned())
88 104 }),
89 105 _ => Ok(None),
90 106 },
91 107 }
92 108 }
93 109
94 110 pub(crate) fn is_dir(path: impl AsRef<Path>) -> Result<bool, HgError> {
95 111 Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_dir()))
96 112 }
97 113
98 114 pub(crate) fn is_file(path: impl AsRef<Path>) -> Result<bool, HgError> {
99 115 Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_file()))
100 116 }
@@ -1,325 +1,343 b''
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, UiError};
10 10 use crate::utils::path_utils::relativize_paths;
11 11 use clap::{Arg, SubCommand};
12 12 use hg;
13 13 use hg::config::Config;
14 use hg::dirstate::TruncatedTimestamp;
14 use hg::dirstate::{has_exec_bit, TruncatedTimestamp};
15 15 use hg::errors::HgError;
16 16 use hg::manifest::Manifest;
17 17 use hg::matchers::AlwaysMatcher;
18 18 use hg::repo::Repo;
19 use hg::utils::files::get_bytes_from_os_string;
19 20 use hg::utils::hg_path::{hg_path_to_os_string, HgPath};
20 21 use hg::{HgPathCow, StatusOptions};
21 22 use log::{info, warn};
22 23 use std::borrow::Cow;
23 24
24 25 pub const HELP_TEXT: &str = "
25 26 Show changed files in the working directory
26 27
27 28 This is a pure Rust version of `hg status`.
28 29
29 30 Some options might be missing, check the list below.
30 31 ";
31 32
32 33 pub fn args() -> clap::App<'static, 'static> {
33 34 SubCommand::with_name("status")
34 35 .alias("st")
35 36 .about(HELP_TEXT)
36 37 .arg(
37 38 Arg::with_name("all")
38 39 .help("show status of all files")
39 40 .short("-A")
40 41 .long("--all"),
41 42 )
42 43 .arg(
43 44 Arg::with_name("modified")
44 45 .help("show only modified files")
45 46 .short("-m")
46 47 .long("--modified"),
47 48 )
48 49 .arg(
49 50 Arg::with_name("added")
50 51 .help("show only added files")
51 52 .short("-a")
52 53 .long("--added"),
53 54 )
54 55 .arg(
55 56 Arg::with_name("removed")
56 57 .help("show only removed files")
57 58 .short("-r")
58 59 .long("--removed"),
59 60 )
60 61 .arg(
61 62 Arg::with_name("clean")
62 63 .help("show only clean files")
63 64 .short("-c")
64 65 .long("--clean"),
65 66 )
66 67 .arg(
67 68 Arg::with_name("deleted")
68 69 .help("show only deleted files")
69 70 .short("-d")
70 71 .long("--deleted"),
71 72 )
72 73 .arg(
73 74 Arg::with_name("unknown")
74 75 .help("show only unknown (not tracked) files")
75 76 .short("-u")
76 77 .long("--unknown"),
77 78 )
78 79 .arg(
79 80 Arg::with_name("ignored")
80 81 .help("show only ignored files")
81 82 .short("-i")
82 83 .long("--ignored"),
83 84 )
84 85 }
85 86
86 87 /// Pure data type allowing the caller to specify file states to display
87 88 #[derive(Copy, Clone, Debug)]
88 89 pub struct DisplayStates {
89 90 pub modified: bool,
90 91 pub added: bool,
91 92 pub removed: bool,
92 93 pub clean: bool,
93 94 pub deleted: bool,
94 95 pub unknown: bool,
95 96 pub ignored: bool,
96 97 }
97 98
98 99 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
99 100 modified: true,
100 101 added: true,
101 102 removed: true,
102 103 clean: false,
103 104 deleted: true,
104 105 unknown: true,
105 106 ignored: false,
106 107 };
107 108
108 109 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
109 110 modified: true,
110 111 added: true,
111 112 removed: true,
112 113 clean: true,
113 114 deleted: true,
114 115 unknown: true,
115 116 ignored: true,
116 117 };
117 118
118 119 impl DisplayStates {
119 120 pub fn is_empty(&self) -> bool {
120 121 !(self.modified
121 122 || self.added
122 123 || self.removed
123 124 || self.clean
124 125 || self.deleted
125 126 || self.unknown
126 127 || self.ignored)
127 128 }
128 129 }
129 130
130 131 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
131 132 let status_enabled_default = false;
132 133 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
133 134 if !status_enabled.unwrap_or(status_enabled_default) {
134 135 return Err(CommandError::unsupported(
135 136 "status is experimental in rhg (enable it with 'rhg.status = true' \
136 137 or enable fallback with 'rhg.on-unsupported = fallback')"
137 138 ));
138 139 }
139 140
140 141 // TODO: lift these limitations
141 142 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
142 143 return Err(CommandError::unsupported(
143 144 "ui.tweakdefaults is not yet supported with rhg status",
144 145 ));
145 146 }
146 147 if invocation.config.get_bool(b"ui", b"statuscopies")? {
147 148 return Err(CommandError::unsupported(
148 149 "ui.statuscopies is not yet supported with rhg status",
149 150 ));
150 151 }
151 152 if invocation
152 153 .config
153 154 .get(b"commands", b"status.terse")
154 155 .is_some()
155 156 {
156 157 return Err(CommandError::unsupported(
157 158 "status.terse is not yet supported with rhg status",
158 159 ));
159 160 }
160 161
161 162 let ui = invocation.ui;
162 163 let config = invocation.config;
163 164 let args = invocation.subcommand_args;
164 165 let display_states = if args.is_present("all") {
165 166 // TODO when implementing `--quiet`: it excludes clean files
166 167 // from `--all`
167 168 ALL_DISPLAY_STATES
168 169 } else {
169 170 let requested = DisplayStates {
170 171 modified: args.is_present("modified"),
171 172 added: args.is_present("added"),
172 173 removed: args.is_present("removed"),
173 174 clean: args.is_present("clean"),
174 175 deleted: args.is_present("deleted"),
175 176 unknown: args.is_present("unknown"),
176 177 ignored: args.is_present("ignored"),
177 178 };
178 179 if requested.is_empty() {
179 180 DEFAULT_DISPLAY_STATES
180 181 } else {
181 182 requested
182 183 }
183 184 };
184 185
185 186 let repo = invocation.repo?;
186 187 let mut dmap = repo.dirstate_map_mut()?;
187 188
188 189 let options = StatusOptions {
189 190 // TODO should be provided by the dirstate parsing and
190 191 // hence be stored on dmap. Using a value that assumes we aren't
191 192 // below the time resolution granularity of the FS and the
192 193 // dirstate.
193 194 last_normal_time: TruncatedTimestamp::new_truncate(0, 0),
194 195 // we're currently supporting file systems with exec flags only
195 196 // anyway
196 197 check_exec: true,
197 198 list_clean: display_states.clean,
198 199 list_unknown: display_states.unknown,
199 200 list_ignored: display_states.ignored,
200 201 collect_traversed_dirs: false,
201 202 };
202 203 let ignore_file = repo.working_directory_vfs().join(".hgignore"); // TODO hardcoded
203 204 let (mut ds_status, pattern_warnings) = dmap.status(
204 205 &AlwaysMatcher,
205 206 repo.working_directory_path().to_owned(),
206 207 vec![ignore_file],
207 208 options,
208 209 )?;
209 210 if !pattern_warnings.is_empty() {
210 211 warn!("Pattern warnings: {:?}", &pattern_warnings);
211 212 }
212 213
213 214 if !ds_status.bad.is_empty() {
214 215 warn!("Bad matches {:?}", &(ds_status.bad))
215 216 }
216 217 if !ds_status.unsure.is_empty() {
217 218 info!(
218 219 "Files to be rechecked by retrieval from filelog: {:?}",
219 220 &ds_status.unsure
220 221 );
221 222 }
222 223 if !ds_status.unsure.is_empty()
223 224 && (display_states.modified || display_states.clean)
224 225 {
225 226 let p1 = repo.dirstate_parents()?.p1;
226 227 let manifest = repo.manifest_for_node(p1).map_err(|e| {
227 228 CommandError::from((e, &*format!("{:x}", p1.short())))
228 229 })?;
229 230 for to_check in ds_status.unsure {
230 231 if unsure_is_modified(repo, &manifest, &to_check)? {
231 232 if display_states.modified {
232 233 ds_status.modified.push(to_check);
233 234 }
234 235 } else {
235 236 if display_states.clean {
236 237 ds_status.clean.push(to_check);
237 238 }
238 239 }
239 240 }
240 241 }
241 242 if display_states.modified {
242 243 display_status_paths(ui, repo, config, &mut ds_status.modified, b"M")?;
243 244 }
244 245 if display_states.added {
245 246 display_status_paths(ui, repo, config, &mut ds_status.added, b"A")?;
246 247 }
247 248 if display_states.removed {
248 249 display_status_paths(ui, repo, config, &mut ds_status.removed, b"R")?;
249 250 }
250 251 if display_states.deleted {
251 252 display_status_paths(ui, repo, config, &mut ds_status.deleted, b"!")?;
252 253 }
253 254 if display_states.unknown {
254 255 display_status_paths(ui, repo, config, &mut ds_status.unknown, b"?")?;
255 256 }
256 257 if display_states.ignored {
257 258 display_status_paths(ui, repo, config, &mut ds_status.ignored, b"I")?;
258 259 }
259 260 if display_states.clean {
260 261 display_status_paths(ui, repo, config, &mut ds_status.clean, b"C")?;
261 262 }
262 263 Ok(())
263 264 }
264 265
265 266 // Probably more elegant to use a Deref or Borrow trait rather than
266 267 // harcode HgPathBuf, but probably not really useful at this point
267 268 fn display_status_paths(
268 269 ui: &Ui,
269 270 repo: &Repo,
270 271 config: &Config,
271 272 paths: &mut [HgPathCow],
272 273 status_prefix: &[u8],
273 274 ) -> Result<(), CommandError> {
274 275 paths.sort_unstable();
275 276 let mut relative: bool = config.get_bool(b"ui", b"relative-paths")?;
276 277 relative = config
277 278 .get_option(b"commands", b"status.relative")?
278 279 .unwrap_or(relative);
279 280 if relative && !ui.plain() {
280 281 relativize_paths(
281 282 repo,
282 283 paths.iter().map(Ok),
283 284 |path: Cow<[u8]>| -> Result<(), UiError> {
284 285 ui.write_stdout(
285 286 &[status_prefix, b" ", path.as_ref(), b"\n"].concat(),
286 287 )
287 288 },
288 289 )?;
289 290 } else {
290 291 for path in paths {
291 292 // Same TODO as in commands::root
292 293 let bytes: &[u8] = path.as_bytes();
293 294 // TODO optim, probably lots of unneeded copies here, especially
294 295 // if out stream is buffered
295 296 ui.write_stdout(&[status_prefix, b" ", bytes, b"\n"].concat())?;
296 297 }
297 298 }
298 299 Ok(())
299 300 }
300 301
301 302 /// Check if a file is modified by comparing actual repo store and file system.
302 303 ///
303 304 /// This meant to be used for those that the dirstate cannot resolve, due
304 305 /// to time resolution limits.
305 ///
306 /// TODO: detect permission bits and similar metadata modifications
307 306 fn unsure_is_modified(
308 307 repo: &Repo,
309 308 manifest: &Manifest,
310 309 hg_path: &HgPath,
311 310 ) -> Result<bool, HgError> {
311 let vfs = repo.working_directory_vfs();
312 let fs_path = hg_path_to_os_string(hg_path).expect("HgPath conversion");
313 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
314 let is_symlink = fs_metadata.file_type().is_symlink();
315 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the dirstate
316 let fs_flags = if is_symlink {
317 Some(b'l')
318 } else if has_exec_bit(&fs_metadata) {
319 Some(b'x')
320 } else {
321 None
322 };
323
312 324 let entry = manifest
313 325 .find_file(hg_path)?
314 326 .expect("ambgious file not in p1");
327 if entry.flags != fs_flags {
328 return Ok(true);
329 }
315 330 let filelog = repo.filelog(hg_path)?;
316 331 let filelog_entry =
317 332 filelog.data_for_node(entry.node_id()?).map_err(|_| {
318 333 HgError::corrupted("filelog missing node from manifest")
319 334 })?;
320 335 let contents_in_p1 = filelog_entry.data()?;
321 336
322 let fs_path = hg_path_to_os_string(hg_path).expect("HgPath conversion");
323 let fs_contents = repo.working_directory_vfs().read(fs_path)?;
337 let fs_contents = if is_symlink {
338 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
339 } else {
340 vfs.read(fs_path)?
341 };
324 342 return Ok(contents_in_p1 != &*fs_contents);
325 343 }
@@ -1,32 +1,28 b''
1 1 #require execbit
2 2
3 TODO: fix rhg bugs that make this test fail when status is enabled
4 $ unset RHG_STATUS
5
6
7 3 $ hg init
8 4 $ echo a > a
9 5 $ hg ci -Am'not executable'
10 6 adding a
11 7
12 8 $ chmod +x a
13 9 $ hg ci -m'executable'
14 10 $ hg id
15 11 79abf14474dc tip
16 12
17 13 Make sure we notice the change of mode if the cached size == -1:
18 14
19 15 $ hg rm a
20 16 $ hg revert -r 0 a
21 17 $ hg debugstate
22 18 n 0 -1 unset a
23 19 $ hg status
24 20 M a
25 21
26 22 $ hg up 0
27 23 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
28 24 $ hg id
29 25 d69afc33ff8a
30 26 $ test -x a && echo executable -- bad || echo not executable -- good
31 27 not executable -- good
32 28
@@ -1,437 +1,433 b''
1 1 ===============================================
2 2 Testing merge involving change to the exec flag
3 3 ===============================================
4 4
5 5 #require execbit
6 6
7 TODO: fix rhg bugs that make this test fail when status is enabled
8 $ unset RHG_STATUS
9
10
11 7 Initial setup
12 8 ==============
13 9
14 10
15 11 $ hg init base-repo
16 12 $ cd base-repo
17 13 $ cat << EOF > a
18 14 > 1
19 15 > 2
20 16 > 3
21 17 > 4
22 18 > 5
23 19 > 6
24 20 > 7
25 21 > 8
26 22 > 9
27 23 > EOF
28 24 $ touch b
29 25 $ hg add a b
30 26 $ hg commit -m "initial commit"
31 27 $ cd ..
32 28
33 29 $ hg init base-exec
34 30 $ cd base-exec
35 31 $ cat << EOF > a
36 32 > 1
37 33 > 2
38 34 > 3
39 35 > 4
40 36 > 5
41 37 > 6
42 38 > 7
43 39 > 8
44 40 > 9
45 41 > EOF
46 42 $ chmod +x a
47 43 $ touch b
48 44 $ hg add a b
49 45 $ hg commit -m "initial commit"
50 46 $ cd ..
51 47
52 48 Testing merging mode change
53 49 ===========================
54 50
55 51 Adding the flag
56 52 ---------------
57 53
58 54 setup
59 55
60 56 Change on one side, executable bit on the other
61 57
62 58 $ hg clone base-repo simple-merge-repo
63 59 updating to branch default
64 60 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
65 61 $ cd simple-merge-repo
66 62 $ chmod +x a
67 63 $ hg ci -m "make a executable, no change"
68 64 $ [ -x a ] || echo "executable bit not recorded"
69 65 $ hg up ".^"
70 66 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
71 67 $ cat << EOF > a
72 68 > 1
73 69 > 2
74 70 > 3
75 71 > 4
76 72 > 5
77 73 > 6
78 74 > 7
79 75 > x
80 76 > 9
81 77 > EOF
82 78 $ hg commit -m "edit end of file"
83 79 created new head
84 80
85 81 merge them (from the update side)
86 82
87 83 $ hg merge 'desc("make a executable, no change")'
88 84 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
89 85 (branch merge, don't forget to commit)
90 86 $ hg st
91 87 M a
92 88 $ [ -x a ] || echo "executable bit lost"
93 89
94 90 merge them (from the chmod side)
95 91
96 92 $ hg up -C 'desc("make a executable, no change")'
97 93 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
98 94 $ hg merge 'desc("edit end of file")'
99 95 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
100 96 (branch merge, don't forget to commit)
101 97 $ hg st
102 98 M a
103 99 $ [ -x a ] || echo "executable bit lost"
104 100
105 101
106 102 $ cd ..
107 103
108 104
109 105 Removing the flag
110 106 -----------------
111 107
112 108 Change on one side, executable bit on the other
113 109
114 110 $ hg clone base-exec simple-merge-repo-removal
115 111 updating to branch default
116 112 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
117 113 $ cd simple-merge-repo-removal
118 114 $ chmod -x a
119 115 $ hg ci -m "make a non-executable, no change"
120 116 $ [ -x a ] && echo "executable bit not removed"
121 117 [1]
122 118 $ hg up ".^"
123 119 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
124 120 $ cat << EOF > a
125 121 > 1
126 122 > 2
127 123 > 3
128 124 > 4
129 125 > 5
130 126 > 6
131 127 > 7
132 128 > x
133 129 > 9
134 130 > EOF
135 131 $ hg commit -m "edit end of file"
136 132 created new head
137 133
138 134 merge them (from the update side)
139 135
140 136 $ hg merge 'desc("make a non-executable, no change")'
141 137 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
142 138 (branch merge, don't forget to commit)
143 139 $ hg st
144 140 M a
145 141 $ [ -x a ] && echo "executable bit not removed"
146 142 [1]
147 143
148 144 merge them (from the chmod side)
149 145
150 146 $ hg up -C 'desc("make a non-executable, no change")'
151 147 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
152 148 $ hg merge 'desc("edit end of file")'
153 149 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
154 150 (branch merge, don't forget to commit)
155 151 $ hg st
156 152 M a
157 153 $ [ -x a ] && echo "executable bit not removed"
158 154 [1]
159 155
160 156
161 157 $ cd ..
162 158
163 159 Testing merging mode change with rename
164 160 =======================================
165 161
166 162 Adding the flag
167 163 ---------------
168 164
169 165 $ hg clone base-repo rename-merge-repo
170 166 updating to branch default
171 167 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
172 168 $ cd rename-merge-repo
173 169
174 170 make "a" executable on one side
175 171
176 172 $ chmod +x a
177 173 $ hg status
178 174 M a
179 175 $ hg ci -m "make a executable"
180 176 $ [ -x a ] || echo "executable bit not recorded"
181 177 $ hg up ".^"
182 178 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
183 179
184 180 make "a" renamed on the other side
185 181
186 182 $ hg mv a z
187 183 $ hg st --copies
188 184 A z
189 185 a
190 186 R a
191 187 $ hg ci -m "rename a to z"
192 188 created new head
193 189
194 190 merge them (from the rename side)
195 191
196 192 $ hg merge 'desc("make a executable")'
197 193 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
198 194 (branch merge, don't forget to commit)
199 195 $ hg st --copies
200 196 M z
201 197 a
202 198 $ [ -x z ] || echo "executable bit lost"
203 199
204 200 merge them (from the chmod side)
205 201
206 202 $ hg up -C 'desc("make a executable")'
207 203 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
208 204 $ hg merge 'desc("rename a to z")'
209 205 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
210 206 (branch merge, don't forget to commit)
211 207 $ hg st --copies
212 208 M z
213 209 a
214 210 R a
215 211 $ [ -x z ] || echo "executable bit lost"
216 212
217 213
218 214 $ cd ..
219 215
220 216 Removing the flag
221 217 -----------------
222 218
223 219 $ hg clone base-exec rename-merge-repo-removal
224 220 updating to branch default
225 221 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
226 222 $ cd rename-merge-repo-removal
227 223
228 224 make "a" non-executable on one side
229 225
230 226 $ chmod -x a
231 227 $ hg status
232 228 M a
233 229 $ hg ci -m "make a non-executable"
234 230 $ [ -x a ] && echo "executable bit not removed"
235 231 [1]
236 232 $ hg up ".^"
237 233 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
238 234
239 235 make "a" renamed on the other side
240 236
241 237 $ hg mv a z
242 238 $ hg st --copies
243 239 A z
244 240 a
245 241 R a
246 242 $ hg ci -m "rename a to z"
247 243 created new head
248 244
249 245 merge them (from the rename side)
250 246
251 247 $ hg merge 'desc("make a non-executable")'
252 248 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
253 249 (branch merge, don't forget to commit)
254 250 $ hg st --copies
255 251 M z
256 252 a
257 253 $ [ -x z ] && echo "executable bit not removed"
258 254 [1]
259 255
260 256 merge them (from the chmod side)
261 257
262 258 $ hg up -C 'desc("make a non-executable")'
263 259 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
264 260 $ hg merge 'desc("rename a to z")'
265 261 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
266 262 (branch merge, don't forget to commit)
267 263 $ hg st --copies
268 264 M z
269 265 a
270 266 R a
271 267 $ [ -x z ] && echo "executable bit not removed"
272 268 [1]
273 269
274 270
275 271 $ cd ..
276 272
277 273
278 274 Testing merging mode change with rename + modification on both side
279 275 ===================================================================
280 276
281 277
282 278 Adding the flag
283 279 ---------------
284 280
285 281 $ hg clone base-repo rename+mod-merge-repo
286 282 updating to branch default
287 283 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
288 284 $ cd rename+mod-merge-repo
289 285
290 286 make "a" executable on one side
291 287
292 288 $ chmod +x a
293 289 $ cat << EOF > a
294 290 > 1
295 291 > x
296 292 > 3
297 293 > 4
298 294 > 5
299 295 > 6
300 296 > 7
301 297 > 8
302 298 > 9
303 299 > EOF
304 300 $ hg status
305 301 M a
306 302 $ hg ci -m "make a executable, and change start"
307 303 $ [ -x a ] || echo "executable bit not recorded"
308 304 $ hg up ".^"
309 305 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
310 306
311 307 make "a" renamed on the other side
312 308
313 309 $ hg mv a z
314 310 $ hg st --copies
315 311 A z
316 312 a
317 313 R a
318 314 $ cat << EOF > z
319 315 > 1
320 316 > 2
321 317 > 3
322 318 > 4
323 319 > 5
324 320 > 6
325 321 > 7
326 322 > x
327 323 > 9
328 324 > EOF
329 325 $ hg ci -m "rename a to z, and change end"
330 326 created new head
331 327
332 328 merge them (from the rename side)
333 329
334 330 $ hg merge 'desc("make a executable")'
335 331 merging z and a to z
336 332 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
337 333 (branch merge, don't forget to commit)
338 334 $ hg st --copies
339 335 M z
340 336 a
341 337 $ [ -x z ] || echo "executable bit lost"
342 338
343 339 merge them (from the chmod side)
344 340
345 341 $ hg up -C 'desc("make a executable")'
346 342 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
347 343 $ hg merge 'desc("rename a to z")'
348 344 merging a and z to z
349 345 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
350 346 (branch merge, don't forget to commit)
351 347 $ hg st --copies
352 348 M z
353 349 a
354 350 R a
355 351 $ [ -x z ] || echo "executable bit lost"
356 352
357 353 $ cd ..
358 354
359 355 Removing the flag
360 356 -----------------
361 357
362 358 $ hg clone base-exec rename+mod-merge-repo-removal
363 359 updating to branch default
364 360 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
365 361 $ cd rename+mod-merge-repo-removal
366 362
367 363 make "a" non-executable on one side
368 364
369 365 $ chmod -x a
370 366 $ cat << EOF > a
371 367 > 1
372 368 > x
373 369 > 3
374 370 > 4
375 371 > 5
376 372 > 6
377 373 > 7
378 374 > 8
379 375 > 9
380 376 > EOF
381 377 $ hg status
382 378 M a
383 379 $ hg ci -m "make a non-executable, and change start"
384 380 $ [ -x z ] && echo "executable bit not removed"
385 381 [1]
386 382 $ hg up ".^"
387 383 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
388 384
389 385 make "a" renamed on the other side
390 386
391 387 $ hg mv a z
392 388 $ hg st --copies
393 389 A z
394 390 a
395 391 R a
396 392 $ cat << EOF > z
397 393 > 1
398 394 > 2
399 395 > 3
400 396 > 4
401 397 > 5
402 398 > 6
403 399 > 7
404 400 > x
405 401 > 9
406 402 > EOF
407 403 $ hg ci -m "rename a to z, and change end"
408 404 created new head
409 405
410 406 merge them (from the rename side)
411 407
412 408 $ hg merge 'desc("make a non-executable")'
413 409 merging z and a to z
414 410 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
415 411 (branch merge, don't forget to commit)
416 412 $ hg st --copies
417 413 M z
418 414 a
419 415 $ [ -x z ] && echo "executable bit not removed"
420 416 [1]
421 417
422 418 merge them (from the chmod side)
423 419
424 420 $ hg up -C 'desc("make a non-executable")'
425 421 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
426 422 $ hg merge 'desc("rename a to z")'
427 423 merging a and z to z
428 424 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
429 425 (branch merge, don't forget to commit)
430 426 $ hg st --copies
431 427 M z
432 428 a
433 429 R a
434 430 $ [ -x z ] && echo "executable bit not removed"
435 431 [1]
436 432
437 433 $ cd ..
@@ -1,476 +1,472 b''
1 1 #require symlink execbit
2 2
3 TODO: fix rhg bugs that make this test fail when status is enabled
4 $ unset RHG_STATUS
5
6
7 3 $ tellmeabout() {
8 4 > if [ -h $1 ]; then
9 5 > echo $1 is a symlink:
10 6 > $TESTDIR/readlink.py $1
11 7 > elif [ -x $1 ]; then
12 8 > echo $1 is an executable file with content:
13 9 > cat $1
14 10 > else
15 11 > echo $1 is a plain file with content:
16 12 > cat $1
17 13 > fi
18 14 > }
19 15
20 16 $ hg init test1
21 17 $ cd test1
22 18
23 19 $ echo a > a
24 20 $ hg ci -Aqmadd
25 21 $ chmod +x a
26 22 $ hg ci -mexecutable
27 23
28 24 $ hg up -q 0
29 25 $ rm a
30 26 $ ln -s symlink a
31 27 $ hg ci -msymlink
32 28 created new head
33 29
34 30 Symlink is local parent, executable is other:
35 31
36 32 $ hg merge --debug
37 33 resolving manifests
38 34 branchmerge: True, force: False, partial: False
39 35 ancestor: c334dc3be0da, local: 521a1e40188f+, remote: 3574f3e69b1c
40 36 preserving a for resolve of a
41 37 a: versions differ -> m (premerge)
42 38 tool internal:merge (for pattern a) can't handle symlinks
43 39 couldn't find merge tool hgmerge
44 40 no tool found to merge a
45 41 picked tool ':prompt' for a (binary False symlink True changedelete False)
46 42 file 'a' needs to be resolved.
47 43 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
48 44 What do you want to do? u
49 45 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
50 46 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
51 47 [1]
52 48
53 49 $ tellmeabout a
54 50 a is a symlink:
55 51 a -> symlink
56 52 $ hg resolve a --tool internal:other
57 53 (no more unresolved files)
58 54 $ tellmeabout a
59 55 a is an executable file with content:
60 56 a
61 57 $ hg st
62 58 M a
63 59 ? a.orig
64 60
65 61 Symlink is other parent, executable is local:
66 62
67 63 $ hg update -C 1
68 64 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
69 65
70 66 $ hg merge --debug --tool :union
71 67 resolving manifests
72 68 branchmerge: True, force: False, partial: False
73 69 ancestor: c334dc3be0da, local: 3574f3e69b1c+, remote: 521a1e40188f
74 70 preserving a for resolve of a
75 71 a: versions differ -> m (premerge)
76 72 picked tool ':union' for a (binary False symlink True changedelete False)
77 73 merging a
78 74 my a@3574f3e69b1c+ other a@521a1e40188f ancestor a@c334dc3be0da
79 75 warning: internal :union cannot merge symlinks for a
80 76 warning: conflicts while merging a! (edit, then use 'hg resolve --mark')
81 77 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
82 78 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
83 79 [1]
84 80
85 81 $ tellmeabout a
86 82 a is an executable file with content:
87 83 a
88 84
89 85 $ hg update -C 1
90 86 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
91 87
92 88 $ hg merge --debug --tool :merge3
93 89 resolving manifests
94 90 branchmerge: True, force: False, partial: False
95 91 ancestor: c334dc3be0da, local: 3574f3e69b1c+, remote: 521a1e40188f
96 92 preserving a for resolve of a
97 93 a: versions differ -> m (premerge)
98 94 picked tool ':merge3' for a (binary False symlink True changedelete False)
99 95 merging a
100 96 my a@3574f3e69b1c+ other a@521a1e40188f ancestor a@c334dc3be0da
101 97 warning: internal :merge3 cannot merge symlinks for a
102 98 warning: conflicts while merging a! (edit, then use 'hg resolve --mark')
103 99 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
104 100 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
105 101 [1]
106 102
107 103 $ tellmeabout a
108 104 a is an executable file with content:
109 105 a
110 106
111 107 $ hg update -C 1
112 108 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
113 109
114 110 $ hg merge --debug --tool :merge-local
115 111 resolving manifests
116 112 branchmerge: True, force: False, partial: False
117 113 ancestor: c334dc3be0da, local: 3574f3e69b1c+, remote: 521a1e40188f
118 114 preserving a for resolve of a
119 115 a: versions differ -> m (premerge)
120 116 picked tool ':merge-local' for a (binary False symlink True changedelete False)
121 117 merging a
122 118 my a@3574f3e69b1c+ other a@521a1e40188f ancestor a@c334dc3be0da
123 119 warning: internal :merge-local cannot merge symlinks for a
124 120 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
125 121 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
126 122 [1]
127 123
128 124 $ tellmeabout a
129 125 a is an executable file with content:
130 126 a
131 127
132 128 $ hg update -C 1
133 129 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
134 130
135 131 $ hg merge --debug --tool :merge-other
136 132 resolving manifests
137 133 branchmerge: True, force: False, partial: False
138 134 ancestor: c334dc3be0da, local: 3574f3e69b1c+, remote: 521a1e40188f
139 135 preserving a for resolve of a
140 136 a: versions differ -> m (premerge)
141 137 picked tool ':merge-other' for a (binary False symlink True changedelete False)
142 138 merging a
143 139 my a@3574f3e69b1c+ other a@521a1e40188f ancestor a@c334dc3be0da
144 140 warning: internal :merge-other cannot merge symlinks for a
145 141 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
146 142 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
147 143 [1]
148 144
149 145 $ tellmeabout a
150 146 a is an executable file with content:
151 147 a
152 148
153 149 Update to link without local change should get us a symlink (issue3316):
154 150
155 151 $ hg up -C 0
156 152 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
157 153 $ hg up
158 154 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
159 155 updated to "521a1e40188f: symlink"
160 156 1 other heads for branch "default"
161 157 $ hg st
162 158 ? a.orig
163 159
164 160 Update to link with local change should cause a merge prompt (issue3200):
165 161
166 162 $ hg up -Cq 0
167 163 $ echo data > a
168 164 $ HGMERGE= hg up -y --debug --config ui.merge=
169 165 resolving manifests
170 166 branchmerge: False, force: False, partial: False
171 167 ancestor: c334dc3be0da, local: c334dc3be0da+, remote: 521a1e40188f
172 168 preserving a for resolve of a
173 169 a: versions differ -> m (premerge)
174 170 (couldn't find merge tool hgmerge|tool hgmerge can't handle symlinks) (re)
175 171 no tool found to merge a
176 172 picked tool ':prompt' for a (binary False symlink True changedelete False)
177 173 file 'a' needs to be resolved.
178 174 You can keep (l)ocal [working copy], take (o)ther [destination], or leave (u)nresolved.
179 175 What do you want to do? u
180 176 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
181 177 use 'hg resolve' to retry unresolved file merges
182 178 updated to "521a1e40188f: symlink"
183 179 1 other heads for branch "default"
184 180 [1]
185 181 $ hg diff --git
186 182 diff --git a/a b/a
187 183 old mode 120000
188 184 new mode 100644
189 185 --- a/a
190 186 +++ b/a
191 187 @@ -1,1 +1,1 @@
192 188 -symlink
193 189 \ No newline at end of file
194 190 +data
195 191
196 192
197 193 Test only 'l' change - happens rarely, except when recovering from situations
198 194 where that was what happened.
199 195
200 196 $ hg init test2
201 197 $ cd test2
202 198 $ printf base > f
203 199 $ hg ci -Aqm0
204 200 $ echo file > f
205 201 $ echo content >> f
206 202 $ hg ci -qm1
207 203 $ hg up -qr0
208 204 $ rm f
209 205 $ ln -s base f
210 206 $ hg ci -qm2
211 207 $ hg merge
212 208 tool internal:merge (for pattern f) can't handle symlinks
213 209 no tool found to merge f
214 210 file 'f' needs to be resolved.
215 211 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
216 212 What do you want to do? u
217 213 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
218 214 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
219 215 [1]
220 216 $ tellmeabout f
221 217 f is a symlink:
222 218 f -> base
223 219
224 220 $ hg up -Cqr1
225 221 $ hg merge
226 222 tool internal:merge (for pattern f) can't handle symlinks
227 223 no tool found to merge f
228 224 file 'f' needs to be resolved.
229 225 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
230 226 What do you want to do? u
231 227 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
232 228 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
233 229 [1]
234 230 $ tellmeabout f
235 231 f is a plain file with content:
236 232 file
237 233 content
238 234
239 235 $ cd ..
240 236
241 237 Test removed 'x' flag merged with change to symlink
242 238
243 239 $ hg init test3
244 240 $ cd test3
245 241 $ echo f > f
246 242 $ chmod +x f
247 243 $ hg ci -Aqm0
248 244 $ chmod -x f
249 245 $ hg ci -qm1
250 246 $ hg up -qr0
251 247 $ rm f
252 248 $ ln -s dangling f
253 249 $ hg ci -qm2
254 250 $ hg merge
255 251 tool internal:merge (for pattern f) can't handle symlinks
256 252 no tool found to merge f
257 253 file 'f' needs to be resolved.
258 254 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
259 255 What do you want to do? u
260 256 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
261 257 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
262 258 [1]
263 259 $ tellmeabout f
264 260 f is a symlink:
265 261 f -> dangling
266 262
267 263 $ hg up -Cqr1
268 264 $ hg merge
269 265 tool internal:merge (for pattern f) can't handle symlinks
270 266 no tool found to merge f
271 267 file 'f' needs to be resolved.
272 268 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
273 269 What do you want to do? u
274 270 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
275 271 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
276 272 [1]
277 273 $ tellmeabout f
278 274 f is a plain file with content:
279 275 f
280 276
281 277 Test removed 'x' flag merged with content change - both ways
282 278
283 279 $ hg up -Cqr0
284 280 $ echo change > f
285 281 $ hg ci -qm3
286 282 $ hg merge -r1
287 283 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
288 284 (branch merge, don't forget to commit)
289 285 $ tellmeabout f
290 286 f is a plain file with content:
291 287 change
292 288
293 289 $ hg up -qCr1
294 290 $ hg merge -r3
295 291 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
296 292 (branch merge, don't forget to commit)
297 293 $ tellmeabout f
298 294 f is a plain file with content:
299 295 change
300 296
301 297 $ cd ..
302 298
303 299 Test merge with no common ancestor:
304 300 a: just different
305 301 b: x vs -, different (cannot calculate x, cannot ask merge tool)
306 302 c: x vs -, same (cannot calculate x, merge tool is no good)
307 303 d: x vs l, different
308 304 e: x vs l, same
309 305 f: - vs l, different
310 306 g: - vs l, same
311 307 h: l vs l, different
312 308 (where same means the filelog entry is shared and there thus is an ancestor!)
313 309
314 310 $ hg init test4
315 311 $ cd test4
316 312 $ echo 0 > 0
317 313 $ hg ci -Aqm0
318 314
319 315 $ echo 1 > a
320 316 $ echo 1 > b
321 317 $ chmod +x b
322 318 $ echo 1 > bx
323 319 $ chmod +x bx
324 320 $ echo x > c
325 321 $ chmod +x c
326 322 $ echo 1 > d
327 323 $ chmod +x d
328 324 $ printf x > e
329 325 $ chmod +x e
330 326 $ echo 1 > f
331 327 $ printf x > g
332 328 $ ln -s 1 h
333 329 $ hg ci -qAm1
334 330
335 331 $ hg up -qr0
336 332 $ echo 2 > a
337 333 $ echo 2 > b
338 334 $ echo 2 > bx
339 335 $ chmod +x bx
340 336 $ echo x > c
341 337 $ ln -s 2 d
342 338 $ ln -s x e
343 339 $ ln -s 2 f
344 340 $ ln -s x g
345 341 $ ln -s 2 h
346 342 $ hg ci -Aqm2
347 343
348 344 $ hg merge
349 345 merging a
350 346 warning: cannot merge flags for b without common ancestor - keeping local flags
351 347 merging b
352 348 merging bx
353 349 warning: cannot merge flags for c without common ancestor - keeping local flags
354 350 tool internal:merge (for pattern d) can't handle symlinks
355 351 no tool found to merge d
356 352 file 'd' needs to be resolved.
357 353 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
358 354 What do you want to do? u
359 355 tool internal:merge (for pattern f) can't handle symlinks
360 356 no tool found to merge f
361 357 file 'f' needs to be resolved.
362 358 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
363 359 What do you want to do? u
364 360 tool internal:merge (for pattern h) can't handle symlinks
365 361 no tool found to merge h
366 362 file 'h' needs to be resolved.
367 363 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
368 364 What do you want to do? u
369 365 warning: conflicts while merging a! (edit, then use 'hg resolve --mark')
370 366 warning: conflicts while merging b! (edit, then use 'hg resolve --mark')
371 367 warning: conflicts while merging bx! (edit, then use 'hg resolve --mark')
372 368 3 files updated, 0 files merged, 0 files removed, 6 files unresolved
373 369 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
374 370 [1]
375 371 $ hg resolve -l
376 372 U a
377 373 U b
378 374 U bx
379 375 U d
380 376 U f
381 377 U h
382 378 $ tellmeabout a
383 379 a is a plain file with content:
384 380 <<<<<<< working copy: 0c617753b41b - test: 2
385 381 2
386 382 =======
387 383 1
388 384 >>>>>>> merge rev: 2e60aa20b912 - test: 1
389 385 $ tellmeabout b
390 386 b is a plain file with content:
391 387 <<<<<<< working copy: 0c617753b41b - test: 2
392 388 2
393 389 =======
394 390 1
395 391 >>>>>>> merge rev: 2e60aa20b912 - test: 1
396 392 $ tellmeabout c
397 393 c is a plain file with content:
398 394 x
399 395 $ tellmeabout d
400 396 d is a symlink:
401 397 d -> 2
402 398 $ tellmeabout e
403 399 e is a symlink:
404 400 e -> x
405 401 $ tellmeabout f
406 402 f is a symlink:
407 403 f -> 2
408 404 $ tellmeabout g
409 405 g is a symlink:
410 406 g -> x
411 407 $ tellmeabout h
412 408 h is a symlink:
413 409 h -> 2
414 410
415 411 $ hg up -Cqr1
416 412 $ hg merge
417 413 merging a
418 414 warning: cannot merge flags for b without common ancestor - keeping local flags
419 415 merging b
420 416 merging bx
421 417 warning: cannot merge flags for c without common ancestor - keeping local flags
422 418 tool internal:merge (for pattern d) can't handle symlinks
423 419 no tool found to merge d
424 420 file 'd' needs to be resolved.
425 421 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
426 422 What do you want to do? u
427 423 tool internal:merge (for pattern f) can't handle symlinks
428 424 no tool found to merge f
429 425 file 'f' needs to be resolved.
430 426 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
431 427 What do you want to do? u
432 428 tool internal:merge (for pattern h) can't handle symlinks
433 429 no tool found to merge h
434 430 file 'h' needs to be resolved.
435 431 You can keep (l)ocal [working copy], take (o)ther [merge rev], or leave (u)nresolved.
436 432 What do you want to do? u
437 433 warning: conflicts while merging a! (edit, then use 'hg resolve --mark')
438 434 warning: conflicts while merging b! (edit, then use 'hg resolve --mark')
439 435 warning: conflicts while merging bx! (edit, then use 'hg resolve --mark')
440 436 3 files updated, 0 files merged, 0 files removed, 6 files unresolved
441 437 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
442 438 [1]
443 439 $ tellmeabout a
444 440 a is a plain file with content:
445 441 <<<<<<< working copy: 2e60aa20b912 - test: 1
446 442 1
447 443 =======
448 444 2
449 445 >>>>>>> merge rev: 0c617753b41b - test: 2
450 446 $ tellmeabout b
451 447 b is an executable file with content:
452 448 <<<<<<< working copy: 2e60aa20b912 - test: 1
453 449 1
454 450 =======
455 451 2
456 452 >>>>>>> merge rev: 0c617753b41b - test: 2
457 453 $ tellmeabout c
458 454 c is an executable file with content:
459 455 x
460 456 $ tellmeabout d
461 457 d is an executable file with content:
462 458 1
463 459 $ tellmeabout e
464 460 e is an executable file with content:
465 461 x (no-eol)
466 462 $ tellmeabout f
467 463 f is a plain file with content:
468 464 1
469 465 $ tellmeabout g
470 466 g is a plain file with content:
471 467 x (no-eol)
472 468 $ tellmeabout h
473 469 h is a symlink:
474 470 h -> 1
475 471
476 472 $ cd ..
@@ -1,290 +1,286 b''
1 1 #require symlink
2 2
3 3 #testcases dirstate-v1 dirstate-v2
4 4
5 5 #if dirstate-v2
6 6 $ cat >> $HGRCPATH << EOF
7 7 > [format]
8 8 > exp-rc-dirstate-v2=1
9 9 > [storage]
10 10 > dirstate-v2.slow-path=allow
11 11 > EOF
12 12 #endif
13 13
14 TODO: fix rhg bugs that make this test fail when status is enabled
15 $ unset RHG_STATUS
16
17
18 14 == tests added in 0.7 ==
19 15
20 16 $ hg init test-symlinks-0.7; cd test-symlinks-0.7;
21 17 $ touch foo; ln -s foo bar; ln -s nonexistent baz
22 18
23 19 import with add and addremove -- symlink walking should _not_ screwup.
24 20
25 21 $ hg add
26 22 adding bar
27 23 adding baz
28 24 adding foo
29 25 $ hg forget bar baz foo
30 26 $ hg addremove
31 27 adding bar
32 28 adding baz
33 29 adding foo
34 30
35 31 commit -- the symlink should _not_ appear added to dir state
36 32
37 33 $ hg commit -m 'initial'
38 34
39 35 $ touch bomb
40 36
41 37 again, symlink should _not_ show up on dir state
42 38
43 39 $ hg addremove
44 40 adding bomb
45 41
46 42 Assert screamed here before, should go by without consequence
47 43
48 44 $ hg commit -m 'is there a bug?'
49 45 $ cd ..
50 46
51 47
52 48 == fifo & ignore ==
53 49
54 50 $ hg init test; cd test;
55 51
56 52 $ mkdir dir
57 53 $ touch a.c dir/a.o dir/b.o
58 54
59 55 test what happens if we want to trick hg
60 56
61 57 $ hg commit -A -m 0
62 58 adding a.c
63 59 adding dir/a.o
64 60 adding dir/b.o
65 61 $ echo "relglob:*.o" > .hgignore
66 62 $ rm a.c
67 63 $ rm dir/a.o
68 64 $ rm dir/b.o
69 65 $ mkdir dir/a.o
70 66 $ ln -s nonexistent dir/b.o
71 67 $ mkfifo a.c
72 68
73 69 it should show a.c, dir/a.o and dir/b.o deleted
74 70
75 71 $ hg status
76 72 M dir/b.o
77 73 ! a.c
78 74 ! dir/a.o
79 75 ? .hgignore
80 76 $ hg status a.c
81 77 a.c: unsupported file type (type is fifo)
82 78 ! a.c
83 79 $ cd ..
84 80
85 81
86 82 == symlinks from outside the tree ==
87 83
88 84 test absolute path through symlink outside repo
89 85
90 86 $ p=`pwd`
91 87 $ hg init x
92 88 $ ln -s x y
93 89 $ cd x
94 90 $ touch f
95 91 $ hg add f
96 92 $ hg status "$p"/y/f
97 93 A f
98 94
99 95 try symlink outside repo to file inside
100 96
101 97 $ ln -s x/f ../z
102 98
103 99 this should fail
104 100
105 101 $ hg status ../z && { echo hg mistakenly exited with status 0; exit 1; } || :
106 102 abort: ../z not under root '$TESTTMP/x'
107 103 $ cd ..
108 104
109 105
110 106 == cloning symlinks ==
111 107 $ hg init clone; cd clone;
112 108
113 109 try cloning symlink in a subdir
114 110 1. commit a symlink
115 111
116 112 $ mkdir -p a/b/c
117 113 $ cd a/b/c
118 114 $ ln -s /path/to/symlink/source demo
119 115 $ cd ../../..
120 116 $ hg stat
121 117 ? a/b/c/demo
122 118 $ hg commit -A -m 'add symlink in a/b/c subdir'
123 119 adding a/b/c/demo
124 120
125 121 2. clone it
126 122
127 123 $ cd ..
128 124 $ hg clone clone clonedest
129 125 updating to branch default
130 126 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
131 127
132 128
133 129 == symlink and git diffs ==
134 130
135 131 git symlink diff
136 132
137 133 $ cd clonedest
138 134 $ hg diff --git -r null:tip
139 135 diff --git a/a/b/c/demo b/a/b/c/demo
140 136 new file mode 120000
141 137 --- /dev/null
142 138 +++ b/a/b/c/demo
143 139 @@ -0,0 +1,1 @@
144 140 +/path/to/symlink/source
145 141 \ No newline at end of file
146 142 $ hg export --git tip > ../sl.diff
147 143
148 144 import git symlink diff
149 145
150 146 $ hg rm a/b/c/demo
151 147 $ hg commit -m'remove link'
152 148 $ hg import ../sl.diff
153 149 applying ../sl.diff
154 150 $ hg diff --git -r 1:tip
155 151 diff --git a/a/b/c/demo b/a/b/c/demo
156 152 new file mode 120000
157 153 --- /dev/null
158 154 +++ b/a/b/c/demo
159 155 @@ -0,0 +1,1 @@
160 156 +/path/to/symlink/source
161 157 \ No newline at end of file
162 158
163 159 == symlinks and addremove ==
164 160
165 161 directory moved and symlinked
166 162
167 163 $ mkdir foo
168 164 $ touch foo/a
169 165 $ hg ci -Ama
170 166 adding foo/a
171 167 $ mv foo bar
172 168 $ ln -s bar foo
173 169 $ hg status
174 170 ! foo/a
175 171 ? bar/a
176 172 ? foo
177 173
178 174 now addremove should remove old files
179 175
180 176 $ hg addremove
181 177 adding bar/a
182 178 adding foo
183 179 removing foo/a
184 180
185 181 commit and update back
186 182
187 183 $ hg ci -mb
188 184 $ hg up '.^'
189 185 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
190 186 $ hg up tip
191 187 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
192 188
193 189 $ cd ..
194 190
195 191 == root of repository is symlinked ==
196 192
197 193 $ hg init root
198 194 $ ln -s root link
199 195 $ cd root
200 196 $ echo foo > foo
201 197 $ hg status
202 198 ? foo
203 199 $ hg status ../link
204 200 ? foo
205 201 $ hg add foo
206 202 $ hg cp foo "$TESTTMP/link/bar"
207 203 foo has not been committed yet, so no copy data will be stored for bar.
208 204 $ cd ..
209 205
210 206
211 207 $ hg init b
212 208 $ cd b
213 209 $ ln -s nothing dangling
214 210 $ hg commit -m 'commit symlink without adding' dangling
215 211 abort: dangling: file not tracked!
216 212 [10]
217 213 $ hg add dangling
218 214 $ hg commit -m 'add symlink'
219 215
220 216 $ hg tip -v
221 217 changeset: 0:cabd88b706fc
222 218 tag: tip
223 219 user: test
224 220 date: Thu Jan 01 00:00:00 1970 +0000
225 221 files: dangling
226 222 description:
227 223 add symlink
228 224
229 225
230 226 $ hg manifest --debug
231 227 2564acbe54bbbedfbf608479340b359f04597f80 644 @ dangling
232 228 $ readlink.py dangling
233 229 dangling -> nothing
234 230
235 231 $ rm dangling
236 232 $ ln -s void dangling
237 233 $ hg commit -m 'change symlink'
238 234 $ readlink.py dangling
239 235 dangling -> void
240 236
241 237
242 238 modifying link
243 239
244 240 $ rm dangling
245 241 $ ln -s empty dangling
246 242 $ readlink.py dangling
247 243 dangling -> empty
248 244
249 245
250 246 reverting to rev 0:
251 247
252 248 $ hg revert -r 0 -a
253 249 reverting dangling
254 250 $ readlink.py dangling
255 251 dangling -> nothing
256 252
257 253
258 254 backups:
259 255
260 256 $ readlink.py *.orig
261 257 dangling.orig -> empty
262 258 $ rm *.orig
263 259 $ hg up -C
264 260 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
265 261
266 262 copies
267 263
268 264 $ hg cp -v dangling dangling2
269 265 copying dangling to dangling2
270 266 $ hg st -Cmard
271 267 A dangling2
272 268 dangling
273 269 $ readlink.py dangling dangling2
274 270 dangling -> void
275 271 dangling2 -> void
276 272
277 273
278 274 Issue995: hg copy -A incorrectly handles symbolic links
279 275
280 276 $ hg up -C
281 277 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
282 278 $ mkdir dir
283 279 $ ln -s dir dirlink
284 280 $ hg ci -qAm 'add dirlink'
285 281 $ mkdir newdir
286 282 $ mv dir newdir/dir
287 283 $ mv dirlink newdir/dirlink
288 284 $ hg mv -A dirlink newdir/dirlink
289 285
290 286 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now