diff --git a/mercurial/cext/parsers.c b/mercurial/cext/parsers.c --- a/mercurial/cext/parsers.c +++ b/mercurial/cext/parsers.c @@ -57,7 +57,8 @@ int has_meaningful_mtime; int mode; int size; - int mtime; + int mtime_s; + int mtime_ns; PyObject *parentfiledata; static char *keywords_name[] = { "wc_tracked", @@ -97,15 +98,10 @@ } if (parentfiledata != Py_None) { - if (!PyTuple_CheckExact(parentfiledata)) { - PyErr_SetString( - PyExc_TypeError, - "parentfiledata should be a Tuple or None"); + if (!PyArg_ParseTuple(parentfiledata, "ii(ii)", &mode, &size, + &mtime_s, &mtime_ns)) { return NULL; } - mode = (int)PyLong_AsLong(PyTuple_GetItem(parentfiledata, 0)); - size = (int)PyLong_AsLong(PyTuple_GetItem(parentfiledata, 1)); - mtime = (int)PyLong_AsLong(PyTuple_GetItem(parentfiledata, 2)); } else { has_meaningful_data = 0; has_meaningful_mtime = 0; @@ -120,9 +116,11 @@ } if (has_meaningful_mtime) { t->flags |= dirstate_flag_has_file_mtime; - t->mtime = mtime; + t->mtime_s = mtime_s; + t->mtime_ns = mtime_ns; } else { - t->mtime = 0; + t->mtime_s = 0; + t->mtime_ns = 0; } return (PyObject *)t; } @@ -231,7 +229,7 @@ (self->flags & dirstate_flag_p2_info)) { return ambiguous_time; } else { - return self->mtime; + return self->mtime_s; } } @@ -249,7 +247,8 @@ } else { flags &= ~dirstate_flag_mode_is_symlink; } - return Py_BuildValue("Bii", flags, self->size, self->mtime); + return Py_BuildValue("Biii", flags, self->size, self->mtime_s, + self->mtime_ns); }; static PyObject *dirstate_item_v1_state(dirstateItemObject *self) @@ -274,14 +273,31 @@ }; static PyObject *dirstate_item_need_delay(dirstateItemObject *self, - PyObject *value) + PyObject *now) { - long now; - if (!pylong_to_long(value, &now)) { + int now_s; + int now_ns; + if (!PyArg_ParseTuple(now, "ii", &now_s, &now_ns)) { return NULL; } - if (dirstate_item_c_v1_state(self) == 'n' && - dirstate_item_c_v1_mtime(self) == now) { + if (dirstate_item_c_v1_state(self) == 'n' && self->mtime_s == now_s && + self->mtime_ns == now_ns) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +}; + +static PyObject *dirstate_item_mtime_likely_equal_to(dirstateItemObject *self, + PyObject *other) +{ + int other_s; + int other_ns; + if (!PyArg_ParseTuple(other, "ii", &other_s, &other_ns)) { + return NULL; + } + if ((self->flags & dirstate_flag_has_file_mtime) && + self->mtime_s == other_s && self->mtime_ns == other_ns) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; @@ -301,7 +317,8 @@ t->flags = 0; t->mode = 0; t->size = 0; - t->mtime = 0; + t->mtime_s = 0; + t->mtime_ns = 0; if (state == 'm') { t->flags = (dirstate_flag_wc_tracked | @@ -337,7 +354,7 @@ dirstate_flag_has_file_mtime); t->mode = mode; t->size = size; - t->mtime = mtime; + t->mtime_s = mtime; } } else { PyErr_Format(PyExc_RuntimeError, @@ -372,7 +389,8 @@ if (!t) { return NULL; } - if (!PyArg_ParseTuple(args, "bii", &t->flags, &t->size, &t->mtime)) { + if (!PyArg_ParseTuple(args, "biii", &t->flags, &t->size, &t->mtime_s, + &t->mtime_ns)) { return NULL; } t->mode = 0; @@ -403,8 +421,9 @@ static PyObject *dirstate_item_set_clean(dirstateItemObject *self, PyObject *args) { - int size, mode, mtime; - if (!PyArg_ParseTuple(args, "iii", &mode, &size, &mtime)) { + int size, mode, mtime_s, mtime_ns; + if (!PyArg_ParseTuple(args, "ii(ii)", &mode, &size, &mtime_s, + &mtime_ns)) { return NULL; } self->flags = dirstate_flag_wc_tracked | dirstate_flag_p1_tracked | @@ -412,7 +431,8 @@ dirstate_flag_has_file_mtime; self->mode = mode; self->size = size; - self->mtime = mtime; + self->mtime_s = mtime_s; + self->mtime_ns = mtime_ns; Py_RETURN_NONE; } @@ -427,8 +447,9 @@ { self->flags &= ~dirstate_flag_wc_tracked; self->mode = 0; - self->mtime = 0; self->size = 0; + self->mtime_s = 0; + self->mtime_ns = 0; Py_RETURN_NONE; } @@ -439,8 +460,9 @@ dirstate_flag_has_meaningful_data | dirstate_flag_has_file_mtime); self->mode = 0; - self->mtime = 0; self->size = 0; + self->mtime_s = 0; + self->mtime_ns = 0; } Py_RETURN_NONE; } @@ -457,6 +479,8 @@ "return a \"mtime\" suitable for v1 serialization"}, {"need_delay", (PyCFunction)dirstate_item_need_delay, METH_O, "True if the stored mtime would be ambiguous with the current time"}, + {"mtime_likely_equal_to", (PyCFunction)dirstate_item_mtime_likely_equal_to, + METH_O, "True if the stored mtime is likely equal to the given mtime"}, {"from_v1_data", (PyCFunction)dirstate_item_from_v1_meth, METH_VARARGS | METH_CLASS, "build a new DirstateItem object from V1 data"}, {"from_v2_data", (PyCFunction)dirstate_item_from_v2_meth, @@ -742,11 +766,12 @@ Py_ssize_t nbytes, pos, l; PyObject *k, *v = NULL, *pn; char *p, *s; - int now; + int now_s; + int now_ns; - if (!PyArg_ParseTuple(args, "O!O!O!i:pack_dirstate", &PyDict_Type, &map, - &PyDict_Type, ©map, &PyTuple_Type, &pl, - &now)) { + if (!PyArg_ParseTuple(args, "O!O!O!(ii):pack_dirstate", &PyDict_Type, + &map, &PyDict_Type, ©map, &PyTuple_Type, &pl, + &now_s, &now_ns)) { return NULL; } @@ -815,7 +840,8 @@ mode = dirstate_item_c_v1_mode(tuple); size = dirstate_item_c_v1_size(tuple); mtime = dirstate_item_c_v1_mtime(tuple); - if (state == 'n' && mtime == now) { + if (state == 'n' && tuple->mtime_s == now_s && + tuple->mtime_ns == now_ns) { /* See pure/parsers.py:pack_dirstate for why we do * this. */ mtime = -1; diff --git a/mercurial/cext/util.h b/mercurial/cext/util.h --- a/mercurial/cext/util.h +++ b/mercurial/cext/util.h @@ -27,7 +27,8 @@ unsigned char flags; int mode; int size; - int mtime; + int mtime_s; + int mtime_ns; } dirstateItemObject; /* clang-format on */ diff --git a/mercurial/dirstate.py b/mercurial/dirstate.py --- a/mercurial/dirstate.py +++ b/mercurial/dirstate.py @@ -31,6 +31,10 @@ util, ) +from .dirstateutils import ( + timestamp, +) + from .interfaces import ( dirstate as intdirstate, util as interfaceutil, @@ -66,7 +70,7 @@ '''Get "now" timestamp on filesystem''' tmpfd, tmpname = vfs.mkstemp() try: - return os.fstat(tmpfd)[stat.ST_MTIME] + return timestamp.mtime_of(os.fstat(tmpfd)) finally: os.close(tmpfd) vfs.unlink(tmpname) @@ -122,7 +126,7 @@ # UNC path pointing to root share (issue4557) self._rootdir = pathutil.normasprefix(root) self._dirty = False - self._lastnormaltime = 0 + self._lastnormaltime = timestamp.zero() self._ui = ui self._filecache = {} self._parentwriters = 0 @@ -421,7 +425,7 @@ for a in ("_map", "_branch", "_ignore"): if a in self.__dict__: delattr(self, a) - self._lastnormaltime = 0 + self._lastnormaltime = timestamp.zero() self._dirty = False self._parentwriters = 0 self._origpl = None @@ -620,7 +624,7 @@ s = os.lstat(self._join(filename)) mode = s.st_mode size = s.st_size - mtime = s[stat.ST_MTIME] + mtime = timestamp.mtime_of(s) return (mode, size, mtime) def _discoverpath(self, path, normed, ignoremissing, exists, storemap): @@ -804,7 +808,7 @@ if now is None: # use the modification time of the newly created temporary file as the # filesystem's notion of 'now' - now = util.fstat(st)[stat.ST_MTIME] & _rangemask + now = timestamp.mtime_of(util.fstat(st)) # enough 'delaywrite' prevents 'pack_dirstate' from dropping # timestamp of each entries in dirstate, because of 'now > mtime' @@ -821,11 +825,12 @@ start = int(clock) - (int(clock) % delaywrite) end = start + delaywrite time.sleep(end - clock) - now = end # trust our estimate that the end is near now + # trust our estimate that the end is near now + now = timestamp.timestamp((end, 0)) break self._map.write(tr, st, now) - self._lastnormaltime = 0 + self._lastnormaltime = timestamp.zero() self._dirty = False def _dirignore(self, f): @@ -1358,17 +1363,9 @@ uadd(fn) continue - # This is equivalent to 'state, mode, size, time = dmap[fn]' but not - # written like that for performance reasons. dmap[fn] is not a - # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE - # opcode has fast paths when the value to be unpacked is a tuple or - # a list, but falls back to creating a full-fledged iterator in - # general. That is much slower than simply accessing and storing the - # tuple members one by one. t = dget(fn) mode = t.mode size = t.size - time = t.mtime if not st and t.tracked: dadd(fn) @@ -1393,12 +1390,9 @@ ladd(fn) else: madd(fn) - elif ( - time != st[stat.ST_MTIME] - and time != st[stat.ST_MTIME] & _rangemask - ): + elif not t.mtime_likely_equal_to(timestamp.mtime_of(st)): ladd(fn) - elif st[stat.ST_MTIME] == lastnormaltime: + elif timestamp.mtime_of(st) == lastnormaltime: # fn may have just been marked as normal and it may have # changed in the same second without changing its size. # This can happen if we quickly do multiple commits. diff --git a/mercurial/dirstatemap.py b/mercurial/dirstatemap.py --- a/mercurial/dirstatemap.py +++ b/mercurial/dirstatemap.py @@ -127,7 +127,6 @@ def set_clean(self, filename, mode, size, mtime): """mark a file as back to a clean state""" entry = self[filename] - mtime = mtime & rangemask size = size & rangemask entry.set_clean(mode, size, mtime) self._refresh_entry(filename, entry) diff --git a/mercurial/dirstateutils/timestamp.py b/mercurial/dirstateutils/timestamp.py new file mode 100644 --- /dev/null +++ b/mercurial/dirstateutils/timestamp.py @@ -0,0 +1,53 @@ +# Copyright Mercurial Contributors +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from __future__ import absolute_import + +import stat + + +rangemask = 0x7FFFFFFF + + +class timestamp(tuple): + """ + A Unix timestamp with nanoseconds precision, + modulo 2**31 seconds. + + A 2-tuple containing: + + `truncated_seconds`: seconds since the Unix epoch, + truncated to its lower 31 bits + + `subsecond_nanoseconds`: number of nanoseconds since `truncated_seconds`. + """ + + def __new__(cls, value): + truncated_seconds, subsec_nanos = value + value = (truncated_seconds & rangemask, subsec_nanos) + return super(timestamp, cls).__new__(cls, value) + + +def zero(): + """ + Returns the `timestamp` at the Unix epoch. + """ + return tuple.__new__(timestamp, (0, 0)) + + +def mtime_of(stat_result): + """ + Takes an `os.stat_result`-like object and returns a `timestamp` object + for its modification time. + """ + # https://docs.python.org/2/library/os.html#os.stat_float_times + # "For compatibility with older Python versions, + # accessing stat_result as a tuple always returns integers." + secs = stat_result[stat.ST_MTIME] + + # For now + subsec_nanos = 0 + + return timestamp((secs, subsec_nanos)) diff --git a/mercurial/dirstateutils/v2.py b/mercurial/dirstateutils/v2.py --- a/mercurial/dirstateutils/v2.py +++ b/mercurial/dirstateutils/v2.py @@ -98,13 +98,13 @@ flags, size, mtime_s, - _mtime_ns, + mtime_ns, ) = NODE.unpack(node_bytes) # Parse child nodes of this node recursively parse_nodes(map, copy_map, data, children_start, children_count) - item = parsers.DirstateItem.from_v2_data(flags, size, mtime_s) + item = parsers.DirstateItem.from_v2_data(flags, size, mtime_s, mtime_ns) if not item.any_tracked: continue path = slice_with_len(data, path_start, path_len) @@ -144,8 +144,7 @@ copy_source_start = 0 copy_source_len = 0 if entry is not None: - flags, size, mtime_s = entry.v2_data() - mtime_ns = 0 + flags, size, mtime_s, mtime_ns = entry.v2_data() else: # There are no mtime-cached directories in the Python implementation flags = 0 @@ -246,7 +245,6 @@ written to the docket. Again, see more details on the on-disk format in `mercurial/helptext/internals/dirstate-v2`. """ - now = int(now) data = bytearray() root_nodes_start = 0 root_nodes_len = 0 diff --git a/mercurial/merge.py b/mercurial/merge.py --- a/mercurial/merge.py +++ b/mercurial/merge.py @@ -9,13 +9,13 @@ import collections import errno -import stat import struct from .i18n import _ from .node import nullrev from .thirdparty import attr from .utils import stringutil +from .dirstateutils import timestamp from . import ( copies, encoding, @@ -1406,8 +1406,9 @@ if wantfiledata: s = wfctx.lstat() mode = s.st_mode - mtime = s[stat.ST_MTIME] - filedata[f] = (mode, size, mtime) # for dirstate.normal + mtime = timestamp.mtime_of(s) + # for dirstate.update_file's parentfiledata argument: + filedata[f] = (mode, size, mtime) if i == 100: yield False, (i, f) i = 0 diff --git a/mercurial/pure/parsers.py b/mercurial/pure/parsers.py --- a/mercurial/pure/parsers.py +++ b/mercurial/pure/parsers.py @@ -92,7 +92,8 @@ _p2_info = attr.ib() _mode = attr.ib() _size = attr.ib() - _mtime = attr.ib() + _mtime_s = attr.ib() + _mtime_ns = attr.ib() def __init__( self, @@ -109,7 +110,8 @@ self._mode = None self._size = None - self._mtime = None + self._mtime_s = None + self._mtime_ns = None if parentfiledata is None: has_meaningful_mtime = False has_meaningful_data = False @@ -117,10 +119,10 @@ self._mode = parentfiledata[0] self._size = parentfiledata[1] if has_meaningful_mtime: - self._mtime = parentfiledata[2] + self._mtime_s, self._mtime_ns = parentfiledata[2] @classmethod - def from_v2_data(cls, flags, size, mtime): + def from_v2_data(cls, flags, size, mtime_s, mtime_ns): """Build a new DirstateItem object from V2 data""" has_mode_size = bool(flags & DIRSTATE_V2_HAS_MODE_AND_SIZE) mode = None @@ -140,7 +142,7 @@ p2_info=bool(flags & DIRSTATE_V2_P2_INFO), has_meaningful_data=has_mode_size, has_meaningful_mtime=bool(flags & DIRSTATE_V2_HAS_FILE_MTIME), - parentfiledata=(mode, size, mtime), + parentfiledata=(mode, size, (mtime_s, mtime_ns)), ) @classmethod @@ -175,13 +177,13 @@ wc_tracked=True, p1_tracked=True, has_meaningful_mtime=False, - parentfiledata=(mode, size, 42), + parentfiledata=(mode, size, (42, 0)), ) else: return cls( wc_tracked=True, p1_tracked=True, - parentfiledata=(mode, size, mtime), + parentfiledata=(mode, size, (mtime, 0)), ) else: raise RuntimeError(b'unknown state: %s' % state) @@ -192,7 +194,8 @@ This means the next status call will have to actually check its content to make sure it is correct. """ - self._mtime = None + self._mtime_s = None + self._mtime_ns = None def set_clean(self, mode, size, mtime): """mark a file as "clean" cancelling potential "possibly dirty call" @@ -206,7 +209,7 @@ self._p1_tracked = True self._mode = mode self._size = size - self._mtime = mtime + self._mtime_s, self._mtime_ns = mtime def set_tracked(self): """mark a file as tracked in the working copy @@ -218,7 +221,8 @@ # the files as needing lookup # # Consider dropping this in the future in favor of something less broad. - self._mtime = None + self._mtime_s = None + self._mtime_ns = None def set_untracked(self): """mark a file as untracked in the working copy @@ -228,7 +232,8 @@ self._wc_tracked = False self._mode = None self._size = None - self._mtime = None + self._mtime_s = None + self._mtime_ns = None def drop_merge_data(self): """remove all "merge-only" from a DirstateItem @@ -239,7 +244,8 @@ self._p2_info = False self._mode = None self._size = None - self._mtime = None + self._mtime_s = None + self._mtime_ns = None @property def mode(self): @@ -253,6 +259,14 @@ def mtime(self): return self.v1_mtime() + def mtime_likely_equal_to(self, other_mtime): + self_sec = self._mtime_s + if self_sec is None: + return False + self_ns = self._mtime_ns + other_sec, other_ns = other_mtime + return self_sec == other_sec and self_ns == other_ns + @property def state(self): """ @@ -329,9 +343,9 @@ flags |= DIRSTATE_V2_MODE_EXEC_PERM if stat.S_ISLNK(self.mode): flags |= DIRSTATE_V2_MODE_IS_SYMLINK - if self._mtime is not None: + if self._mtime_s is not None: flags |= DIRSTATE_V2_HAS_FILE_MTIME - return (flags, self._size or 0, self._mtime or 0) + return (flags, self._size or 0, self._mtime_s or 0, self._mtime_ns or 0) def v1_state(self): """return a "state" suitable for v1 serialization""" @@ -379,18 +393,18 @@ raise RuntimeError('untracked item') elif self.removed: return 0 - elif self._mtime is None: + elif self._mtime_s is None: return AMBIGUOUS_TIME elif self._p2_info: return AMBIGUOUS_TIME elif not self._p1_tracked: return AMBIGUOUS_TIME else: - return self._mtime + return self._mtime_s def need_delay(self, now): """True if the stored mtime would be ambiguous with the current time""" - return self.v1_state() == b'n' and self.v1_mtime() == now + return self.v1_state() == b'n' and self.mtime_likely_equal_to(now) def gettype(q): @@ -758,7 +772,6 @@ def pack_dirstate(dmap, copymap, pl, now): - now = int(now) cs = stringio() write = cs.write write(b"".join(pl)) 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 @@ -14,14 +14,15 @@ Merged, } -/// The C implementation uses all signed types. This will be an issue -/// either when 4GB+ source files are commonplace or in 2038, whichever -/// comes first. -#[derive(Debug, PartialEq, Copy, Clone)] +/// `size` and `mtime.seconds` are truncated to 31 bits. +/// +/// TODO: double-check status algorithm correctness for files +/// larger than 2 GiB or modified after 2038. +#[derive(Debug, Copy, Clone)] pub struct DirstateEntry { pub(crate) flags: Flags, mode_size: Option<(u32, u32)>, - mtime: Option, + mtime: Option, } bitflags! { @@ -33,7 +34,7 @@ } /// A Unix timestamp with nanoseconds precision -#[derive(Copy, Clone)] +#[derive(Debug, Copy, Clone)] pub struct TruncatedTimestamp { truncated_seconds: u32, /// Always in the `0 .. 1_000_000_000` range. @@ -178,16 +179,13 @@ p1_tracked: bool, p2_info: bool, mode_size: Option<(u32, u32)>, - mtime: Option, + mtime: Option, ) -> Self { if let Some((mode, size)) = mode_size { // TODO: return an error for out of range values? assert!(mode & !RANGE_MASK_31BIT == 0); assert!(size & !RANGE_MASK_31BIT == 0); } - if let Some(mtime) = mtime { - assert!(mtime & !RANGE_MASK_31BIT == 0); - } let mut flags = Flags::empty(); flags.set(Flags::WDIR_TRACKED, wdir_tracked); flags.set(Flags::P1_TRACKED, p1_tracked); @@ -234,6 +232,9 @@ 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(); Self { flags: Flags::WDIR_TRACKED | Flags::P1_TRACKED, mode_size: Some((mode, size)), @@ -321,7 +322,13 @@ /// Returns `(wdir_tracked, p1_tracked, p2_info, mode_size, mtime)` pub(crate) fn v2_data( &self, - ) -> (bool, bool, bool, Option<(u32, u32)>, Option) { + ) -> ( + bool, + bool, + bool, + Option<(u32, u32)>, + Option, + ) { if !self.any_tracked() { // TODO: return an Option instead? panic!("Accessing v1_state of an untracked DirstateEntry") @@ -395,7 +402,7 @@ } else if !self.flags.contains(Flags::P1_TRACKED) { MTIME_UNSET } else if let Some(mtime) = self.mtime { - i32::try_from(mtime).unwrap() + i32::try_from(mtime.truncated_seconds()).unwrap() } else { MTIME_UNSET } @@ -421,6 +428,10 @@ self.v1_mtime() } + pub fn truncated_mtime(&self) -> Option { + self.mtime + } + pub fn drop_merge_data(&mut self) { if self.flags.contains(Flags::P2_INFO) { self.flags.remove(Flags::P2_INFO); @@ -433,9 +444,13 @@ self.mtime = None } - pub fn set_clean(&mut self, mode: u32, size: u32, mtime: u32) { + pub fn set_clean( + &mut self, + mode: u32, + size: u32, + mtime: TruncatedTimestamp, + ) { let size = size & RANGE_MASK_31BIT; - let mtime = mtime & RANGE_MASK_31BIT; self.flags.insert(Flags::WDIR_TRACKED | Flags::P1_TRACKED); self.mode_size = Some((mode, size)); self.mtime = Some(mtime); @@ -496,11 +511,15 @@ (self.state().into(), self.mode(), self.size(), self.mtime()) } - pub fn mtime_is_ambiguous(&self, now: i32) -> bool { - self.state() == EntryState::Normal && self.mtime() == now + pub fn mtime_is_ambiguous(&self, now: TruncatedTimestamp) -> bool { + if let Some(mtime) = self.mtime { + self.state() == EntryState::Normal && mtime.very_likely_equal(now) + } else { + false + } } - pub fn clear_ambiguous_mtime(&mut self, now: i32) -> bool { + pub fn clear_ambiguous_mtime(&mut self, now: TruncatedTimestamp) -> bool { let ambiguous = self.mtime_is_ambiguous(now); if ambiguous { // The file was last modified "simultaneously" with the current diff --git a/rust/hg-core/src/dirstate/parsers.rs b/rust/hg-core/src/dirstate/parsers.rs --- a/rust/hg-core/src/dirstate/parsers.rs +++ b/rust/hg-core/src/dirstate/parsers.rs @@ -135,6 +135,3 @@ packed.extend(source.as_bytes()); } } - -/// Seconds since the Unix epoch -pub struct Timestamp(pub i64); diff --git a/rust/hg-core/src/dirstate/status.rs b/rust/hg-core/src/dirstate/status.rs --- a/rust/hg-core/src/dirstate/status.rs +++ b/rust/hg-core/src/dirstate/status.rs @@ -12,6 +12,7 @@ use crate::dirstate_tree::on_disk::DirstateV2ParseError; use crate::{ + dirstate::TruncatedTimestamp, utils::hg_path::{HgPath, HgPathError}, PatternError, }; @@ -64,7 +65,7 @@ /// Remember the most recent modification timeslot for status, to make /// sure we won't miss future size-preserving file content modifications /// that happen within the same timeslot. - pub last_normal_time: i64, + pub last_normal_time: TruncatedTimestamp, /// Whether we are on a filesystem with UNIX-like exec flags pub check_exec: bool, pub list_clean: bool, diff --git a/rust/hg-core/src/dirstate_tree/dirstate_map.rs b/rust/hg-core/src/dirstate_tree/dirstate_map.rs --- a/rust/hg-core/src/dirstate_tree/dirstate_map.rs +++ b/rust/hg-core/src/dirstate_tree/dirstate_map.rs @@ -1,7 +1,6 @@ use bytes_cast::BytesCast; use micro_timer::timed; use std::borrow::Cow; -use std::convert::TryInto; use std::path::PathBuf; use super::on_disk; @@ -11,7 +10,6 @@ use crate::dirstate::parsers::pack_entry; use crate::dirstate::parsers::packed_entry_size; use crate::dirstate::parsers::parse_dirstate_entries; -use crate::dirstate::parsers::Timestamp; use crate::dirstate::CopyMapIter; use crate::dirstate::StateMapIter; use crate::dirstate::TruncatedTimestamp; @@ -932,10 +930,9 @@ pub fn pack_v1( &mut self, parents: DirstateParents, - now: Timestamp, + now: TruncatedTimestamp, ) -> Result, DirstateError> { let map = self.get_map_mut(); - let now: i32 = now.0.try_into().expect("time overflow"); let mut ambiguous_mtimes = Vec::new(); // Optizimation (to be measured?): pre-compute size to avoid `Vec` // reallocations @@ -981,12 +978,10 @@ #[timed] pub fn pack_v2( &mut self, - now: Timestamp, + now: TruncatedTimestamp, can_append: bool, ) -> Result<(Vec, Vec, bool), DirstateError> { let map = self.get_map_mut(); - // TODO: how do we want to handle this in 2038? - let now: i32 = now.0.try_into().expect("time overflow"); let mut paths = Vec::new(); for node in map.iter_nodes() { let node = node?; 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 @@ -310,7 +310,7 @@ &self, ) -> Result { if self.has_entry() { - Ok(dirstate_map::NodeData::Entry(self.assume_entry())) + Ok(dirstate_map::NodeData::Entry(self.assume_entry()?)) } else if let Some(mtime) = self.cached_directory_mtime()? { Ok(dirstate_map::NodeData::CachedDirectory { mtime }) } else { @@ -346,7 +346,7 @@ file_type | permisions } - fn assume_entry(&self) -> DirstateEntry { + fn assume_entry(&self) -> Result { // TODO: convert through raw bits instead? let wdir_tracked = self.flags().contains(Flags::WDIR_TRACKED); let p1_tracked = self.flags().contains(Flags::P1_TRACKED); @@ -357,24 +357,24 @@ None }; let mtime = if self.flags().contains(Flags::HAS_FILE_MTIME) { - Some(self.mtime.truncated_seconds.into()) + Some(self.mtime.try_into()?) } else { None }; - DirstateEntry::from_v2_data( + Ok(DirstateEntry::from_v2_data( wdir_tracked, p1_tracked, p2_info, mode_size, mtime, - ) + )) } pub(super) fn entry( &self, ) -> Result, DirstateV2ParseError> { if self.has_entry() { - Ok(Some(self.assume_entry())) + Ok(Some(self.assume_entry()?)) } else { Ok(None) } @@ -426,10 +426,7 @@ }; let mtime = if let Some(m) = mtime_opt { flags.insert(Flags::HAS_FILE_MTIME); - PackedTruncatedTimestamp { - truncated_seconds: m.into(), - nanoseconds: 0.into(), - } + m.into() } else { PackedTruncatedTimestamp::null() }; diff --git a/rust/hg-core/src/dirstate_tree/status.rs b/rust/hg-core/src/dirstate_tree/status.rs --- a/rust/hg-core/src/dirstate_tree/status.rs +++ b/rust/hg-core/src/dirstate_tree/status.rs @@ -533,7 +533,8 @@ } else { let mtime = mtime_seconds(fs_metadata); if truncate_i64(mtime) != entry.mtime() - || mtime == self.options.last_normal_time + || mtime + == self.options.last_normal_time.truncated_seconds() as i64 { self.outcome .lock() diff --git a/rust/hg-cpython/src/dirstate.rs b/rust/hg-cpython/src/dirstate.rs --- a/rust/hg-cpython/src/dirstate.rs +++ b/rust/hg-cpython/src/dirstate.rs @@ -54,7 +54,7 @@ matcher: PyObject, ignorefiles: PyList, check_exec: bool, - last_normal_time: i64, + last_normal_time: (u32, u32), list_clean: bool, list_ignored: bool, list_unknown: bool, diff --git a/rust/hg-cpython/src/dirstate/dirstate_map.rs b/rust/hg-cpython/src/dirstate/dirstate_map.rs --- a/rust/hg-cpython/src/dirstate/dirstate_map.rs +++ b/rust/hg-cpython/src/dirstate/dirstate_map.rs @@ -18,11 +18,10 @@ use crate::{ dirstate::copymap::{CopyMap, CopyMapItemsIterator, CopyMapKeysIterator}, - dirstate::item::DirstateItem, + dirstate::item::{timestamp, DirstateItem}, pybytes_deref::PyBytesDeref, }; use hg::{ - dirstate::parsers::Timestamp, dirstate::StateMapIter, dirstate_tree::dirstate_map::DirstateMap as TreeDirstateMap, dirstate_tree::on_disk::DirstateV2ParseError, @@ -195,9 +194,9 @@ &self, p1: PyObject, p2: PyObject, - now: PyObject + now: (u32, u32) ) -> PyResult { - let now = Timestamp(now.extract(py)?); + let now = timestamp(py, now)?; let mut inner = self.inner(py).borrow_mut(); let parents = DirstateParents { @@ -219,10 +218,10 @@ /// instead of written to a new data file (False). def write_v2( &self, - now: PyObject, + now: (u32, u32), can_append: bool, ) -> PyResult { - let now = Timestamp(now.extract(py)?); + let now = timestamp(py, now)?; let mut inner = self.inner(py).borrow_mut(); let result = inner.pack_v2(now, can_append); 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 @@ -8,6 +8,7 @@ use cpython::PythonObject; use hg::dirstate::DirstateEntry; use hg::dirstate::EntryState; +use hg::dirstate::TruncatedTimestamp; use std::cell::Cell; use std::convert::TryFrom; @@ -21,7 +22,7 @@ p2_info: bool = false, has_meaningful_data: bool = true, has_meaningful_mtime: bool = true, - parentfiledata: Option<(u32, u32, u32)> = None, + parentfiledata: Option<(u32, u32, (u32, u32))> = None, ) -> PyResult { let mut mode_size_opt = None; @@ -31,7 +32,7 @@ mode_size_opt = Some((mode, size)) } if has_meaningful_mtime { - mtime_opt = Some(mtime) + mtime_opt = Some(timestamp(py, mtime)?) } } let entry = DirstateEntry::from_v2_data( @@ -118,10 +119,19 @@ Ok(mtime) } - def need_delay(&self, now: i32) -> PyResult { + def need_delay(&self, now: (u32, u32)) -> PyResult { + let now = timestamp(py, now)?; Ok(self.entry(py).get().mtime_is_ambiguous(now)) } + def mtime_likely_equal_to(&self, other: (u32, u32)) -> PyResult { + if let Some(mtime) = self.entry(py).get().truncated_mtime() { + Ok(mtime.very_likely_equal(timestamp(py, other)?)) + } else { + Ok(false) + } + } + @classmethod def from_v1_data( _cls, @@ -147,8 +157,9 @@ &self, mode: u32, size: u32, - mtime: u32, + mtime: (u32, u32), ) -> PyResult { + let mtime = timestamp(py, mtime)?; self.update(py, |entry| entry.set_clean(mode, size, mtime)); Ok(PyNone) } @@ -188,3 +199,15 @@ self.entry(py).set(entry) } } + +pub(crate) fn timestamp( + py: Python<'_>, + (s, ns): (u32, u32), +) -> PyResult { + TruncatedTimestamp::from_already_truncated(s, ns).map_err(|_| { + PyErr::new::( + py, + "expected mtime truncated to 31 bits", + ) + }) +} diff --git a/rust/hg-cpython/src/dirstate/status.rs b/rust/hg-cpython/src/dirstate/status.rs --- a/rust/hg-cpython/src/dirstate/status.rs +++ b/rust/hg-cpython/src/dirstate/status.rs @@ -9,6 +9,7 @@ //! `hg-core` crate. From Python, this will be seen as //! `rustext.dirstate.status`. +use crate::dirstate::item::timestamp; use crate::{dirstate::DirstateMap, exceptions::FallbackError}; use cpython::exc::OSError; use cpython::{ @@ -102,12 +103,13 @@ root_dir: PyObject, ignore_files: PyList, check_exec: bool, - last_normal_time: i64, + last_normal_time: (u32, u32), list_clean: bool, list_ignored: bool, list_unknown: bool, collect_traversed_dirs: bool, ) -> PyResult { + let last_normal_time = timestamp(py, last_normal_time)?; let bytes = root_dir.extract::(py)?; let root_dir = get_path_from_bytes(bytes.data(py)); diff --git a/rust/rhg/src/commands/status.rs b/rust/rhg/src/commands/status.rs --- a/rust/rhg/src/commands/status.rs +++ b/rust/rhg/src/commands/status.rs @@ -11,6 +11,7 @@ use clap::{Arg, SubCommand}; use hg; use hg::config::Config; +use hg::dirstate::TruncatedTimestamp; use hg::errors::HgError; use hg::manifest::Manifest; use hg::matchers::AlwaysMatcher; @@ -180,7 +181,7 @@ // hence be stored on dmap. Using a value that assumes we aren't // below the time resolution granularity of the FS and the // dirstate. - last_normal_time: 0, + last_normal_time: TruncatedTimestamp::new_truncate(0, 0), // we're currently supporting file systems with exec flags only // anyway check_exec: true, diff --git a/tests/fakedirstatewritetime.py b/tests/fakedirstatewritetime.py --- a/tests/fakedirstatewritetime.py +++ b/tests/fakedirstatewritetime.py @@ -15,6 +15,7 @@ policy, registrar, ) +from mercurial.dirstateutils import timestamp from mercurial.utils import dateutil try: @@ -40,9 +41,8 @@ def pack_dirstate(fakenow, orig, dmap, copymap, pl, now): # execute what original parsers.pack_dirstate should do actually # for consistency - actualnow = int(now) for f, e in dmap.items(): - if e.need_delay(actualnow): + if e.need_delay(now): e.set_possibly_dirty() return orig(dmap, copymap, pl, fakenow) @@ -62,6 +62,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)) if has_rust_dirstate: # The Rust implementation does not use public parse/pack dirstate