diff --git a/mercurial/cext/parsers.c b/mercurial/cext/parsers.c --- a/mercurial/cext/parsers.c +++ b/mercurial/cext/parsers.c @@ -61,6 +61,7 @@ int p2_info; int has_meaningful_data; int has_meaningful_mtime; + int mtime_second_ambiguous; int mode; int size; int mtime_s; @@ -79,6 +80,7 @@ p2_info = 0; has_meaningful_mtime = 1; has_meaningful_data = 1; + mtime_second_ambiguous = 0; parentfiledata = Py_None; fallback_exec = Py_None; fallback_symlink = Py_None; @@ -124,8 +126,8 @@ return NULL; } if (mtime != Py_None) { - if (!PyArg_ParseTuple(mtime, "ii", &mtime_s, - &mtime_ns)) { + if (!PyArg_ParseTuple(mtime, "iii", &mtime_s, &mtime_ns, + &mtime_second_ambiguous)) { return NULL; } } else { @@ -139,6 +141,9 @@ t->flags |= dirstate_flag_has_meaningful_data; t->mode = mode; t->size = size; + if (mtime_second_ambiguous) { + t->flags |= dirstate_flag_mtime_second_ambiguous; + } } else { t->mode = 0; t->size = 0; @@ -325,7 +330,9 @@ { int other_s; int other_ns; - if (!PyArg_ParseTuple(other, "ii", &other_s, &other_ns)) { + int other_second_ambiguous; + if (!PyArg_ParseTuple(other, "iii", &other_s, &other_ns, + &other_second_ambiguous)) { return NULL; } if ((self->flags & dirstate_flag_has_mtime) && @@ -468,15 +475,17 @@ static PyObject *dirstate_item_set_clean(dirstateItemObject *self, PyObject *args) { - int size, mode, mtime_s, mtime_ns; + int size, mode, mtime_s, mtime_ns, mtime_second_ambiguous; PyObject *mtime; mtime_s = 0; mtime_ns = 0; + mtime_second_ambiguous = 0; if (!PyArg_ParseTuple(args, "iiO", &mode, &size, &mtime)) { return NULL; } if (mtime != Py_None) { - if (!PyArg_ParseTuple(mtime, "ii", &mtime_s, &mtime_ns)) { + if (!PyArg_ParseTuple(mtime, "iii", &mtime_s, &mtime_ns, + &mtime_second_ambiguous)) { return NULL; } } else { @@ -485,6 +494,9 @@ self->flags = dirstate_flag_wc_tracked | dirstate_flag_p1_tracked | dirstate_flag_has_meaningful_data | dirstate_flag_has_mtime; + if (mtime_second_ambiguous) { + self->flags |= dirstate_flag_mtime_second_ambiguous; + } self->mode = mode; self->size = size; self->mtime_s = mtime_s; diff --git a/mercurial/dirstateutils/timestamp.py b/mercurial/dirstateutils/timestamp.py --- a/mercurial/dirstateutils/timestamp.py +++ b/mercurial/dirstateutils/timestamp.py @@ -31,8 +31,8 @@ """ def __new__(cls, value): - truncated_seconds, subsec_nanos = value - value = (truncated_seconds & rangemask, subsec_nanos) + truncated_seconds, subsec_nanos, second_ambiguous = value + value = (truncated_seconds & rangemask, subsec_nanos, second_ambiguous) return super(timestamp, cls).__new__(cls, value) def __eq__(self, other): @@ -89,7 +89,7 @@ secs = nanos // billion subsec_nanos = nanos % billion - return timestamp((secs, subsec_nanos)) + return timestamp((secs, subsec_nanos, False)) def reliable_mtime_of(stat_result, present_mtime): diff --git a/mercurial/pure/parsers.py b/mercurial/pure/parsers.py --- a/mercurial/pure/parsers.py +++ b/mercurial/pure/parsers.py @@ -104,6 +104,7 @@ _mtime_ns = attr.ib() _fallback_exec = attr.ib() _fallback_symlink = attr.ib() + _mtime_second_ambiguous = attr.ib() def __init__( self, @@ -127,6 +128,7 @@ self._size = None self._mtime_s = None self._mtime_ns = None + self._mtime_second_ambiguous = False if parentfiledata is None: has_meaningful_mtime = False has_meaningful_data = False @@ -136,7 +138,11 @@ self._mode = parentfiledata[0] self._size = parentfiledata[1] if has_meaningful_mtime: - self._mtime_s, self._mtime_ns = parentfiledata[2] + ( + self._mtime_s, + self._mtime_ns, + self._mtime_second_ambiguous, + ) = parentfiledata[2] @classmethod def from_v2_data(cls, flags, size, mtime_s, mtime_ns): @@ -179,7 +185,7 @@ p2_info=bool(flags & DIRSTATE_V2_P2_INFO), has_meaningful_data=has_mode_size, has_meaningful_mtime=has_meaningful_mtime, - parentfiledata=(mode, size, (mtime_s, mtime_ns)), + parentfiledata=(mode, size, (mtime_s, mtime_ns, False)), fallback_exec=fallback_exec, fallback_symlink=fallback_symlink, ) @@ -216,13 +222,13 @@ wc_tracked=True, p1_tracked=True, has_meaningful_mtime=False, - parentfiledata=(mode, size, (42, 0)), + parentfiledata=(mode, size, (42, 0, False)), ) else: return cls( wc_tracked=True, p1_tracked=True, - parentfiledata=(mode, size, (mtime, 0)), + parentfiledata=(mode, size, (mtime, 0, False)), ) else: raise RuntimeError(b'unknown state: %s' % state) @@ -248,7 +254,7 @@ self._p1_tracked = True self._mode = mode self._size = size - self._mtime_s, self._mtime_ns = mtime + self._mtime_s, self._mtime_ns, self._mtime_second_ambiguous = mtime def set_tracked(self): """mark a file as tracked in the working copy @@ -303,7 +309,7 @@ if self_sec is None: return False self_ns = self._mtime_ns - other_sec, other_ns = other_mtime + other_sec, other_ns, second_ambiguous = other_mtime return self_sec == other_sec and ( self_ns == other_ns or self_ns == 0 or other_ns == 0 ) diff --git a/rust/hg-core/src/dirstate/entry.rs b/rust/hg-core/src/dirstate/entry.rs --- a/rust/hg-core/src/dirstate/entry.rs +++ b/rust/hg-core/src/dirstate/entry.rs @@ -43,6 +43,7 @@ truncated_seconds: u32, /// Always in the `0 .. 1_000_000_000` range. nanoseconds: u32, + second_ambiguous: bool, } impl TruncatedTimestamp { @@ -50,11 +51,16 @@ /// and truncate the seconds components to its lower 31 bits. /// /// Panics if the nanoseconds components is not in the expected range. - pub fn new_truncate(seconds: i64, nanoseconds: u32) -> Self { + pub fn new_truncate( + seconds: i64, + nanoseconds: u32, + second_ambiguous: bool, + ) -> Self { assert!(nanoseconds < NSEC_PER_SEC); Self { truncated_seconds: seconds as u32 & RANGE_MASK_31BIT, nanoseconds, + second_ambiguous, } } @@ -63,6 +69,7 @@ pub fn from_already_truncated( truncated_seconds: u32, nanoseconds: u32, + second_ambiguous: bool, ) -> Result { if truncated_seconds & !RANGE_MASK_31BIT == 0 && nanoseconds < NSEC_PER_SEC @@ -70,6 +77,7 @@ Ok(Self { truncated_seconds, nanoseconds, + second_ambiguous, }) } else { Err(DirstateV2ParseError) @@ -83,7 +91,7 @@ let seconds = metadata.mtime(); // i64 -> u32 with value always in the `0 .. NSEC_PER_SEC` range let nanoseconds = metadata.mtime_nsec().try_into().unwrap(); - Ok(Self::new_truncate(seconds, nanoseconds)) + Ok(Self::new_truncate(seconds, nanoseconds, false)) } #[cfg(not(unix))] { @@ -168,7 +176,7 @@ } } }; - Self::new_truncate(seconds, nanoseconds) + Self::new_truncate(seconds, nanoseconds, false) } } @@ -258,9 +266,10 @@ let mode = u32::try_from(mode).unwrap(); let size = u32::try_from(size).unwrap(); let mtime = u32::try_from(mtime).unwrap(); - let mtime = - TruncatedTimestamp::from_already_truncated(mtime, 0) - .unwrap(); + let mtime = TruncatedTimestamp::from_already_truncated( + mtime, 0, false, + ) + .unwrap(); Self { flags: Flags::WDIR_TRACKED | Flags::P1_TRACKED, mode_size: Some((mode, size)), diff --git a/rust/hg-core/src/dirstate_tree/on_disk.rs b/rust/hg-core/src/dirstate_tree/on_disk.rs --- a/rust/hg-core/src/dirstate_tree/on_disk.rs +++ b/rust/hg-core/src/dirstate_tree/on_disk.rs @@ -773,6 +773,7 @@ Self::from_already_truncated( timestamp.truncated_seconds.get(), timestamp.nanoseconds.get(), + false, ) } } diff --git a/rust/hg-cpython/src/dirstate/item.rs b/rust/hg-cpython/src/dirstate/item.rs --- a/rust/hg-cpython/src/dirstate/item.rs +++ b/rust/hg-cpython/src/dirstate/item.rs @@ -23,7 +23,7 @@ p2_info: bool = false, has_meaningful_data: bool = true, has_meaningful_mtime: bool = true, - parentfiledata: Option<(u32, u32, Option<(u32, u32)>)> = None, + parentfiledata: Option<(u32, u32, Option<(u32, u32, bool)>)> = None, fallback_exec: Option = None, fallback_symlink: Option = None, @@ -194,7 +194,8 @@ Ok(mtime) } - def mtime_likely_equal_to(&self, other: (u32, u32)) -> PyResult { + def mtime_likely_equal_to(&self, other: (u32, u32, bool)) + -> PyResult { if let Some(mtime) = self.entry(py).get().truncated_mtime() { Ok(mtime.likely_equal(timestamp(py, other)?)) } else { @@ -227,7 +228,7 @@ &self, mode: u32, size: u32, - mtime: (u32, u32), + mtime: (u32, u32, bool), ) -> PyResult { let mtime = timestamp(py, mtime)?; self.update(py, |entry| entry.set_clean(mode, size, mtime)); @@ -272,12 +273,13 @@ pub(crate) fn timestamp( py: Python<'_>, - (s, ns): (u32, u32), + (s, ns, second_ambiguous): (u32, u32, bool), ) -> PyResult { - TruncatedTimestamp::from_already_truncated(s, ns).map_err(|_| { - PyErr::new::( - py, - "expected mtime truncated to 31 bits", - ) - }) + TruncatedTimestamp::from_already_truncated(s, ns, second_ambiguous) + .map_err(|_| { + PyErr::new::( + py, + "expected mtime truncated to 31 bits", + ) + }) } diff --git a/tests/fakedirstatewritetime.py b/tests/fakedirstatewritetime.py --- a/tests/fakedirstatewritetime.py +++ b/tests/fakedirstatewritetime.py @@ -55,7 +55,7 @@ # parsing 'fakenow' in YYYYmmddHHMM format makes comparison between # 'fakenow' value and 'touch -t YYYYmmddHHMM' argument easy fakenow = dateutil.parsedate(fakenow, [b'%Y%m%d%H%M'])[0] - fakenow = timestamp.timestamp((fakenow, 0)) + fakenow = timestamp.timestamp((fakenow, 0, False)) if has_rust_dirstate: # The Rust implementation does not use public parse/pack dirstate