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