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