##// END OF EJS Templates
dirstate: store mtimes with nanosecond precision in memory...
Simon Sapin -
r49079:269ff897 default
parent child Browse files
Show More
@@ -0,0 +1,53 b''
1 # Copyright Mercurial Contributors
2 #
3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2 or any later version.
5
6 from __future__ import absolute_import
7
8 import stat
9
10
11 rangemask = 0x7FFFFFFF
12
13
14 class timestamp(tuple):
15 """
16 A Unix timestamp with nanoseconds precision,
17 modulo 2**31 seconds.
18
19 A 2-tuple containing:
20
21 `truncated_seconds`: seconds since the Unix epoch,
22 truncated to its lower 31 bits
23
24 `subsecond_nanoseconds`: number of nanoseconds since `truncated_seconds`.
25 """
26
27 def __new__(cls, value):
28 truncated_seconds, subsec_nanos = value
29 value = (truncated_seconds & rangemask, subsec_nanos)
30 return super(timestamp, cls).__new__(cls, value)
31
32
33 def zero():
34 """
35 Returns the `timestamp` at the Unix epoch.
36 """
37 return tuple.__new__(timestamp, (0, 0))
38
39
40 def mtime_of(stat_result):
41 """
42 Takes an `os.stat_result`-like object and returns a `timestamp` object
43 for its modification time.
44 """
45 # https://docs.python.org/2/library/os.html#os.stat_float_times
46 # "For compatibility with older Python versions,
47 # accessing stat_result as a tuple always returns integers."
48 secs = stat_result[stat.ST_MTIME]
49
50 # For now
51 subsec_nanos = 0
52
53 return timestamp((secs, subsec_nanos))
@@ -57,7 +57,8 b' static PyObject *dirstate_item_new(PyTyp'
57 57 int has_meaningful_mtime;
58 58 int mode;
59 59 int size;
60 int mtime;
60 int mtime_s;
61 int mtime_ns;
61 62 PyObject *parentfiledata;
62 63 PyObject *fallback_exec;
63 64 PyObject *fallback_symlink;
@@ -111,15 +112,10 b' static PyObject *dirstate_item_new(PyTyp'
111 112 }
112 113
113 114 if (parentfiledata != Py_None) {
114 if (!PyTuple_CheckExact(parentfiledata)) {
115 PyErr_SetString(
116 PyExc_TypeError,
117 "parentfiledata should be a Tuple or None");
115 if (!PyArg_ParseTuple(parentfiledata, "ii(ii)", &mode, &size,
116 &mtime_s, &mtime_ns)) {
118 117 return NULL;
119 118 }
120 mode = (int)PyLong_AsLong(PyTuple_GetItem(parentfiledata, 0));
121 size = (int)PyLong_AsLong(PyTuple_GetItem(parentfiledata, 1));
122 mtime = (int)PyLong_AsLong(PyTuple_GetItem(parentfiledata, 2));
123 119 } else {
124 120 has_meaningful_data = 0;
125 121 has_meaningful_mtime = 0;
@@ -134,9 +130,11 b' static PyObject *dirstate_item_new(PyTyp'
134 130 }
135 131 if (has_meaningful_mtime) {
136 132 t->flags |= dirstate_flag_has_file_mtime;
137 t->mtime = mtime;
133 t->mtime_s = mtime_s;
134 t->mtime_ns = mtime_ns;
138 135 } else {
139 t->mtime = 0;
136 t->mtime_s = 0;
137 t->mtime_ns = 0;
140 138 }
141 139 return (PyObject *)t;
142 140 }
@@ -254,7 +252,7 b' static inline int dirstate_item_c_v1_mti'
254 252 (self->flags & dirstate_flag_p2_info)) {
255 253 return ambiguous_time;
256 254 } else {
257 return self->mtime;
255 return self->mtime_s;
258 256 }
259 257 }
260 258
@@ -272,7 +270,8 b' static PyObject *dirstate_item_v2_data(d'
272 270 } else {
273 271 flags &= ~dirstate_flag_mode_is_symlink;
274 272 }
275 return Py_BuildValue("iii", flags, self->size, self->mtime);
273 return Py_BuildValue("iiii", flags, self->size, self->mtime_s,
274 self->mtime_ns);
276 275 };
277 276
278 277 static PyObject *dirstate_item_v1_state(dirstateItemObject *self)
@@ -297,14 +296,30 b' static PyObject *dirstate_item_v1_mtime('
297 296 };
298 297
299 298 static PyObject *dirstate_item_need_delay(dirstateItemObject *self,
300 PyObject *value)
299 PyObject *now)
301 300 {
302 long now;
303 if (!pylong_to_long(value, &now)) {
301 int now_s;
302 int now_ns;
303 if (!PyArg_ParseTuple(now, "ii", &now_s, &now_ns)) {
304 304 return NULL;
305 305 }
306 if (dirstate_item_c_v1_state(self) == 'n' &&
307 dirstate_item_c_v1_mtime(self) == now) {
306 if (dirstate_item_c_v1_state(self) == 'n' && self->mtime_s == now_s) {
307 Py_RETURN_TRUE;
308 } else {
309 Py_RETURN_FALSE;
310 }
311 };
312
313 static PyObject *dirstate_item_mtime_likely_equal_to(dirstateItemObject *self,
314 PyObject *other)
315 {
316 int other_s;
317 int other_ns;
318 if (!PyArg_ParseTuple(other, "ii", &other_s, &other_ns)) {
319 return NULL;
320 }
321 if ((self->flags & dirstate_flag_has_file_mtime) &&
322 self->mtime_s == other_s && self->mtime_ns == other_ns) {
308 323 Py_RETURN_TRUE;
309 324 } else {
310 325 Py_RETURN_FALSE;
@@ -324,7 +339,8 b' dirstate_item_from_v1_data(char state, i'
324 339 t->flags = 0;
325 340 t->mode = 0;
326 341 t->size = 0;
327 t->mtime = 0;
342 t->mtime_s = 0;
343 t->mtime_ns = 0;
328 344
329 345 if (state == 'm') {
330 346 t->flags = (dirstate_flag_wc_tracked |
@@ -360,7 +376,7 b' dirstate_item_from_v1_data(char state, i'
360 376 dirstate_flag_has_file_mtime);
361 377 t->mode = mode;
362 378 t->size = size;
363 t->mtime = mtime;
379 t->mtime_s = mtime;
364 380 }
365 381 } else {
366 382 PyErr_Format(PyExc_RuntimeError,
@@ -395,7 +411,8 b' static PyObject *dirstate_item_from_v2_m'
395 411 if (!t) {
396 412 return NULL;
397 413 }
398 if (!PyArg_ParseTuple(args, "iii", &t->flags, &t->size, &t->mtime)) {
414 if (!PyArg_ParseTuple(args, "iiii", &t->flags, &t->size, &t->mtime_s,
415 &t->mtime_ns)) {
399 416 return NULL;
400 417 }
401 418 if (t->flags & dirstate_flag_expected_state_is_modified) {
@@ -431,8 +448,9 b' static PyObject *dirstate_item_set_possi'
431 448 static PyObject *dirstate_item_set_clean(dirstateItemObject *self,
432 449 PyObject *args)
433 450 {
434 int size, mode, mtime;
435 if (!PyArg_ParseTuple(args, "iii", &mode, &size, &mtime)) {
451 int size, mode, mtime_s, mtime_ns;
452 if (!PyArg_ParseTuple(args, "ii(ii)", &mode, &size, &mtime_s,
453 &mtime_ns)) {
436 454 return NULL;
437 455 }
438 456 self->flags = dirstate_flag_wc_tracked | dirstate_flag_p1_tracked |
@@ -440,7 +458,8 b' static PyObject *dirstate_item_set_clean'
440 458 dirstate_flag_has_file_mtime;
441 459 self->mode = mode;
442 460 self->size = size;
443 self->mtime = mtime;
461 self->mtime_s = mtime_s;
462 self->mtime_ns = mtime_ns;
444 463 Py_RETURN_NONE;
445 464 }
446 465
@@ -455,8 +474,9 b' static PyObject *dirstate_item_set_untra'
455 474 {
456 475 self->flags &= ~dirstate_flag_wc_tracked;
457 476 self->mode = 0;
458 self->mtime = 0;
459 477 self->size = 0;
478 self->mtime_s = 0;
479 self->mtime_ns = 0;
460 480 Py_RETURN_NONE;
461 481 }
462 482
@@ -467,8 +487,9 b' static PyObject *dirstate_item_drop_merg'
467 487 dirstate_flag_has_meaningful_data |
468 488 dirstate_flag_has_file_mtime);
469 489 self->mode = 0;
470 self->mtime = 0;
471 490 self->size = 0;
491 self->mtime_s = 0;
492 self->mtime_ns = 0;
472 493 }
473 494 Py_RETURN_NONE;
474 495 }
@@ -485,6 +506,8 b' static PyMethodDef dirstate_item_methods'
485 506 "return a \"mtime\" suitable for v1 serialization"},
486 507 {"need_delay", (PyCFunction)dirstate_item_need_delay, METH_O,
487 508 "True if the stored mtime would be ambiguous with the current time"},
509 {"mtime_likely_equal_to", (PyCFunction)dirstate_item_mtime_likely_equal_to,
510 METH_O, "True if the stored mtime is likely equal to the given mtime"},
488 511 {"from_v1_data", (PyCFunction)dirstate_item_from_v1_meth,
489 512 METH_VARARGS | METH_CLASS, "build a new DirstateItem object from V1 data"},
490 513 {"from_v2_data", (PyCFunction)dirstate_item_from_v2_meth,
@@ -855,11 +878,12 b' static PyObject *pack_dirstate(PyObject '
855 878 Py_ssize_t nbytes, pos, l;
856 879 PyObject *k, *v = NULL, *pn;
857 880 char *p, *s;
858 int now;
881 int now_s;
882 int now_ns;
859 883
860 if (!PyArg_ParseTuple(args, "O!O!O!i:pack_dirstate", &PyDict_Type, &map,
861 &PyDict_Type, &copymap, &PyTuple_Type, &pl,
862 &now)) {
884 if (!PyArg_ParseTuple(args, "O!O!O!(ii):pack_dirstate", &PyDict_Type,
885 &map, &PyDict_Type, &copymap, &PyTuple_Type, &pl,
886 &now_s, &now_ns)) {
863 887 return NULL;
864 888 }
865 889
@@ -928,7 +952,7 b' static PyObject *pack_dirstate(PyObject '
928 952 mode = dirstate_item_c_v1_mode(tuple);
929 953 size = dirstate_item_c_v1_size(tuple);
930 954 mtime = dirstate_item_c_v1_mtime(tuple);
931 if (state == 'n' && mtime == now) {
955 if (state == 'n' && tuple->mtime_s == now_s) {
932 956 /* See pure/parsers.py:pack_dirstate for why we do
933 957 * this. */
934 958 mtime = -1;
@@ -27,7 +27,8 b' typedef struct {'
27 27 int flags;
28 28 int mode;
29 29 int size;
30 int mtime;
30 int mtime_s;
31 int mtime_ns;
31 32 } dirstateItemObject;
32 33 /* clang-format on */
33 34
@@ -31,6 +31,10 b' from . import ('
31 31 util,
32 32 )
33 33
34 from .dirstateutils import (
35 timestamp,
36 )
37
34 38 from .interfaces import (
35 39 dirstate as intdirstate,
36 40 util as interfaceutil,
@@ -66,7 +70,7 b' def _getfsnow(vfs):'
66 70 '''Get "now" timestamp on filesystem'''
67 71 tmpfd, tmpname = vfs.mkstemp()
68 72 try:
69 return os.fstat(tmpfd)[stat.ST_MTIME]
73 return timestamp.mtime_of(os.fstat(tmpfd))
70 74 finally:
71 75 os.close(tmpfd)
72 76 vfs.unlink(tmpname)
@@ -122,7 +126,7 b' class dirstate(object):'
122 126 # UNC path pointing to root share (issue4557)
123 127 self._rootdir = pathutil.normasprefix(root)
124 128 self._dirty = False
125 self._lastnormaltime = 0
129 self._lastnormaltime = timestamp.zero()
126 130 self._ui = ui
127 131 self._filecache = {}
128 132 self._parentwriters = 0
@@ -440,7 +444,7 b' class dirstate(object):'
440 444 for a in ("_map", "_branch", "_ignore"):
441 445 if a in self.__dict__:
442 446 delattr(self, a)
443 self._lastnormaltime = 0
447 self._lastnormaltime = timestamp.zero()
444 448 self._dirty = False
445 449 self._parentwriters = 0
446 450 self._origpl = None
@@ -639,7 +643,7 b' class dirstate(object):'
639 643 s = os.lstat(self._join(filename))
640 644 mode = s.st_mode
641 645 size = s.st_size
642 mtime = s[stat.ST_MTIME]
646 mtime = timestamp.mtime_of(s)
643 647 return (mode, size, mtime)
644 648
645 649 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
@@ -720,7 +724,7 b' class dirstate(object):'
720 724
721 725 def clear(self):
722 726 self._map.clear()
723 self._lastnormaltime = 0
727 self._lastnormaltime = timestamp.zero()
724 728 self._dirty = True
725 729
726 730 def rebuild(self, parent, allfiles, changedfiles=None):
@@ -823,7 +827,7 b' class dirstate(object):'
823 827 if now is None:
824 828 # use the modification time of the newly created temporary file as the
825 829 # filesystem's notion of 'now'
826 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
830 now = timestamp.mtime_of(util.fstat(st))
827 831
828 832 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
829 833 # timestamp of each entries in dirstate, because of 'now > mtime'
@@ -840,11 +844,12 b' class dirstate(object):'
840 844 start = int(clock) - (int(clock) % delaywrite)
841 845 end = start + delaywrite
842 846 time.sleep(end - clock)
843 now = end # trust our estimate that the end is near now
847 # trust our estimate that the end is near now
848 now = timestamp.timestamp((end, 0))
844 849 break
845 850
846 851 self._map.write(tr, st, now)
847 self._lastnormaltime = 0
852 self._lastnormaltime = timestamp.zero()
848 853 self._dirty = False
849 854
850 855 def _dirignore(self, f):
@@ -1377,17 +1382,9 b' class dirstate(object):'
1377 1382 uadd(fn)
1378 1383 continue
1379 1384
1380 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1381 # written like that for performance reasons. dmap[fn] is not a
1382 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1383 # opcode has fast paths when the value to be unpacked is a tuple or
1384 # a list, but falls back to creating a full-fledged iterator in
1385 # general. That is much slower than simply accessing and storing the
1386 # tuple members one by one.
1387 1385 t = dget(fn)
1388 1386 mode = t.mode
1389 1387 size = t.size
1390 time = t.mtime
1391 1388
1392 1389 if not st and t.tracked:
1393 1390 dadd(fn)
@@ -1412,12 +1409,9 b' class dirstate(object):'
1412 1409 ladd(fn)
1413 1410 else:
1414 1411 madd(fn)
1415 elif (
1416 time != st[stat.ST_MTIME]
1417 and time != st[stat.ST_MTIME] & _rangemask
1418 ):
1412 elif not t.mtime_likely_equal_to(timestamp.mtime_of(st)):
1419 1413 ladd(fn)
1420 elif st[stat.ST_MTIME] == lastnormaltime:
1414 elif timestamp.mtime_of(st) == lastnormaltime:
1421 1415 # fn may have just been marked as normal and it may have
1422 1416 # changed in the same second without changing its size.
1423 1417 # This can happen if we quickly do multiple commits.
@@ -127,7 +127,6 b' class _dirstatemapcommon(object):'
127 127 def set_clean(self, filename, mode, size, mtime):
128 128 """mark a file as back to a clean state"""
129 129 entry = self[filename]
130 mtime = mtime & rangemask
131 130 size = size & rangemask
132 131 entry.set_clean(mode, size, mtime)
133 132 self._refresh_entry(filename, entry)
@@ -107,7 +107,10 b' def parse_nodes(map, copy_map, data, sta'
107 107 # Parse child nodes of this node recursively
108 108 parse_nodes(map, copy_map, data, children_start, children_count)
109 109
110 item = parsers.DirstateItem.from_v2_data(flags, size, mtime_s)
110 # Don’t yet use sub-second precision if it exists in the file,
111 # since other parts of the code still set it to zero.
112 mtime_ns = 0
113 item = parsers.DirstateItem.from_v2_data(flags, size, mtime_s, mtime_ns)
111 114 if not item.any_tracked:
112 115 continue
113 116 path = slice_with_len(data, path_start, path_len)
@@ -147,8 +150,7 b' class Node(object):'
147 150 copy_source_start = 0
148 151 copy_source_len = 0
149 152 if entry is not None:
150 flags, size, mtime_s = entry.v2_data()
151 mtime_ns = 0
153 flags, size, mtime_s, mtime_ns = entry.v2_data()
152 154 else:
153 155 # There are no mtime-cached directories in the Python implementation
154 156 flags = 0
@@ -249,7 +251,6 b' def pack_dirstate(map, copy_map, now):'
249 251 written to the docket. Again, see more details on the on-disk format in
250 252 `mercurial/helptext/internals/dirstate-v2`.
251 253 """
252 now = int(now)
253 254 data = bytearray()
254 255 root_nodes_start = 0
255 256 root_nodes_len = 0
@@ -9,13 +9,13 b' from __future__ import absolute_import'
9 9
10 10 import collections
11 11 import errno
12 import stat
13 12 import struct
14 13
15 14 from .i18n import _
16 15 from .node import nullrev
17 16 from .thirdparty import attr
18 17 from .utils import stringutil
18 from .dirstateutils import timestamp
19 19 from . import (
20 20 copies,
21 21 encoding,
@@ -1406,8 +1406,9 b' def batchget(repo, mctx, wctx, wantfiled'
1406 1406 if wantfiledata:
1407 1407 s = wfctx.lstat()
1408 1408 mode = s.st_mode
1409 mtime = s[stat.ST_MTIME]
1410 filedata[f] = (mode, size, mtime) # for dirstate.normal
1409 mtime = timestamp.mtime_of(s)
1410 # for dirstate.update_file's parentfiledata argument:
1411 filedata[f] = (mode, size, mtime)
1411 1412 if i == 100:
1412 1413 yield False, (i, f)
1413 1414 i = 0
@@ -99,7 +99,8 b' class DirstateItem(object):'
99 99 _p2_info = attr.ib()
100 100 _mode = attr.ib()
101 101 _size = attr.ib()
102 _mtime = attr.ib()
102 _mtime_s = attr.ib()
103 _mtime_ns = attr.ib()
103 104 _fallback_exec = attr.ib()
104 105 _fallback_symlink = attr.ib()
105 106
@@ -123,7 +124,8 b' class DirstateItem(object):'
123 124
124 125 self._mode = None
125 126 self._size = None
126 self._mtime = None
127 self._mtime_s = None
128 self._mtime_ns = None
127 129 if parentfiledata is None:
128 130 has_meaningful_mtime = False
129 131 has_meaningful_data = False
@@ -131,10 +133,10 b' class DirstateItem(object):'
131 133 self._mode = parentfiledata[0]
132 134 self._size = parentfiledata[1]
133 135 if has_meaningful_mtime:
134 self._mtime = parentfiledata[2]
136 self._mtime_s, self._mtime_ns = parentfiledata[2]
135 137
136 138 @classmethod
137 def from_v2_data(cls, flags, size, mtime):
139 def from_v2_data(cls, flags, size, mtime_s, mtime_ns):
138 140 """Build a new DirstateItem object from V2 data"""
139 141 has_mode_size = bool(flags & DIRSTATE_V2_HAS_MODE_AND_SIZE)
140 142 has_meaningful_mtime = bool(flags & DIRSTATE_V2_HAS_FILE_MTIME)
@@ -170,7 +172,7 b' class DirstateItem(object):'
170 172 p2_info=bool(flags & DIRSTATE_V2_P2_INFO),
171 173 has_meaningful_data=has_mode_size,
172 174 has_meaningful_mtime=has_meaningful_mtime,
173 parentfiledata=(mode, size, mtime),
175 parentfiledata=(mode, size, (mtime_s, mtime_ns)),
174 176 fallback_exec=fallback_exec,
175 177 fallback_symlink=fallback_symlink,
176 178 )
@@ -207,13 +209,13 b' class DirstateItem(object):'
207 209 wc_tracked=True,
208 210 p1_tracked=True,
209 211 has_meaningful_mtime=False,
210 parentfiledata=(mode, size, 42),
212 parentfiledata=(mode, size, (42, 0)),
211 213 )
212 214 else:
213 215 return cls(
214 216 wc_tracked=True,
215 217 p1_tracked=True,
216 parentfiledata=(mode, size, mtime),
218 parentfiledata=(mode, size, (mtime, 0)),
217 219 )
218 220 else:
219 221 raise RuntimeError(b'unknown state: %s' % state)
@@ -224,7 +226,8 b' class DirstateItem(object):'
224 226 This means the next status call will have to actually check its content
225 227 to make sure it is correct.
226 228 """
227 self._mtime = None
229 self._mtime_s = None
230 self._mtime_ns = None
228 231
229 232 def set_clean(self, mode, size, mtime):
230 233 """mark a file as "clean" cancelling potential "possibly dirty call"
@@ -238,7 +241,7 b' class DirstateItem(object):'
238 241 self._p1_tracked = True
239 242 self._mode = mode
240 243 self._size = size
241 self._mtime = mtime
244 self._mtime_s, self._mtime_ns = mtime
242 245
243 246 def set_tracked(self):
244 247 """mark a file as tracked in the working copy
@@ -250,7 +253,8 b' class DirstateItem(object):'
250 253 # the files as needing lookup
251 254 #
252 255 # Consider dropping this in the future in favor of something less broad.
253 self._mtime = None
256 self._mtime_s = None
257 self._mtime_ns = None
254 258
255 259 def set_untracked(self):
256 260 """mark a file as untracked in the working copy
@@ -260,7 +264,8 b' class DirstateItem(object):'
260 264 self._wc_tracked = False
261 265 self._mode = None
262 266 self._size = None
263 self._mtime = None
267 self._mtime_s = None
268 self._mtime_ns = None
264 269
265 270 def drop_merge_data(self):
266 271 """remove all "merge-only" from a DirstateItem
@@ -271,7 +276,8 b' class DirstateItem(object):'
271 276 self._p2_info = False
272 277 self._mode = None
273 278 self._size = None
274 self._mtime = None
279 self._mtime_s = None
280 self._mtime_ns = None
275 281
276 282 @property
277 283 def mode(self):
@@ -285,6 +291,14 b' class DirstateItem(object):'
285 291 def mtime(self):
286 292 return self.v1_mtime()
287 293
294 def mtime_likely_equal_to(self, other_mtime):
295 self_sec = self._mtime_s
296 if self_sec is None:
297 return False
298 self_ns = self._mtime_ns
299 other_sec, other_ns = other_mtime
300 return self_sec == other_sec and self_ns == other_ns
301
288 302 @property
289 303 def state(self):
290 304 """
@@ -440,7 +454,7 b' class DirstateItem(object):'
440 454 flags |= DIRSTATE_V2_MODE_EXEC_PERM
441 455 if stat.S_ISLNK(self.mode):
442 456 flags |= DIRSTATE_V2_MODE_IS_SYMLINK
443 if self._mtime is not None:
457 if self._mtime_s is not None:
444 458 flags |= DIRSTATE_V2_HAS_FILE_MTIME
445 459
446 460 if self._fallback_exec is not None:
@@ -456,7 +470,7 b' class DirstateItem(object):'
456 470 # Note: we do not need to do anything regarding
457 471 # DIRSTATE_V2_ALL_UNKNOWN_RECORDED and DIRSTATE_V2_ALL_IGNORED_RECORDED
458 472 # since we never set _DIRSTATE_V2_HAS_DIRCTORY_MTIME
459 return (flags, self._size or 0, self._mtime or 0)
473 return (flags, self._size or 0, self._mtime_s or 0, self._mtime_ns or 0)
460 474
461 475 def v1_state(self):
462 476 """return a "state" suitable for v1 serialization"""
@@ -504,18 +518,18 b' class DirstateItem(object):'
504 518 raise RuntimeError('untracked item')
505 519 elif self.removed:
506 520 return 0
507 elif self._mtime is None:
521 elif self._mtime_s is None:
508 522 return AMBIGUOUS_TIME
509 523 elif self._p2_info:
510 524 return AMBIGUOUS_TIME
511 525 elif not self._p1_tracked:
512 526 return AMBIGUOUS_TIME
513 527 else:
514 return self._mtime
528 return self._mtime_s
515 529
516 530 def need_delay(self, now):
517 531 """True if the stored mtime would be ambiguous with the current time"""
518 return self.v1_state() == b'n' and self.v1_mtime() == now
532 return self.v1_state() == b'n' and self._mtime_s == now[0]
519 533
520 534
521 535 def gettype(q):
@@ -883,7 +897,6 b' def parse_dirstate(dmap, copymap, st):'
883 897
884 898
885 899 def pack_dirstate(dmap, copymap, pl, now):
886 now = int(now)
887 900 cs = stringio()
888 901 write = cs.write
889 902 write(b"".join(pl))
@@ -14,14 +14,15 b' pub enum EntryState {'
14 14 Merged,
15 15 }
16 16
17 /// The C implementation uses all signed types. This will be an issue
18 /// either when 4GB+ source files are commonplace or in 2038, whichever
19 /// comes first.
20 #[derive(Debug, PartialEq, Copy, Clone)]
17 /// `size` and `mtime.seconds` are truncated to 31 bits.
18 ///
19 /// TODO: double-check status algorithm correctness for files
20 /// larger than 2 GiB or modified after 2038.
21 #[derive(Debug, Copy, Clone)]
21 22 pub struct DirstateEntry {
22 23 pub(crate) flags: Flags,
23 24 mode_size: Option<(u32, u32)>,
24 mtime: Option<u32>,
25 mtime: Option<TruncatedTimestamp>,
25 26 }
26 27
27 28 bitflags! {
@@ -37,7 +38,7 b' bitflags! {'
37 38 }
38 39
39 40 /// A Unix timestamp with nanoseconds precision
40 #[derive(Copy, Clone)]
41 #[derive(Debug, Copy, Clone)]
41 42 pub struct TruncatedTimestamp {
42 43 truncated_seconds: u32,
43 44 /// Always in the `0 .. 1_000_000_000` range.
@@ -90,6 +91,11 b' impl TruncatedTimestamp {'
90 91 }
91 92 }
92 93
94 pub fn to_integer_second(mut self) -> Self {
95 self.nanoseconds = 0;
96 self
97 }
98
93 99 /// The lower 31 bits of the number of seconds since the epoch.
94 100 pub fn truncated_seconds(&self) -> u32 {
95 101 self.truncated_seconds
@@ -182,7 +188,7 b' impl DirstateEntry {'
182 188 p1_tracked: bool,
183 189 p2_info: bool,
184 190 mode_size: Option<(u32, u32)>,
185 mtime: Option<u32>,
191 mtime: Option<TruncatedTimestamp>,
186 192 fallback_exec: Option<bool>,
187 193 fallback_symlink: Option<bool>,
188 194 ) -> Self {
@@ -191,9 +197,6 b' impl DirstateEntry {'
191 197 assert!(mode & !RANGE_MASK_31BIT == 0);
192 198 assert!(size & !RANGE_MASK_31BIT == 0);
193 199 }
194 if let Some(mtime) = mtime {
195 assert!(mtime & !RANGE_MASK_31BIT == 0);
196 }
197 200 let mut flags = Flags::empty();
198 201 flags.set(Flags::WDIR_TRACKED, wdir_tracked);
199 202 flags.set(Flags::P1_TRACKED, p1_tracked);
@@ -252,6 +255,9 b' impl DirstateEntry {'
252 255 let mode = u32::try_from(mode).unwrap();
253 256 let size = u32::try_from(size).unwrap();
254 257 let mtime = u32::try_from(mtime).unwrap();
258 let mtime =
259 TruncatedTimestamp::from_already_truncated(mtime, 0)
260 .unwrap();
255 261 Self {
256 262 flags: Flags::WDIR_TRACKED | Flags::P1_TRACKED,
257 263 mode_size: Some((mode, size)),
@@ -344,7 +350,7 b' impl DirstateEntry {'
344 350 bool,
345 351 bool,
346 352 Option<(u32, u32)>,
347 Option<u32>,
353 Option<TruncatedTimestamp>,
348 354 Option<bool>,
349 355 Option<bool>,
350 356 ) {
@@ -429,7 +435,7 b' impl DirstateEntry {'
429 435 } else if !self.flags.contains(Flags::P1_TRACKED) {
430 436 MTIME_UNSET
431 437 } else if let Some(mtime) = self.mtime {
432 i32::try_from(mtime).unwrap()
438 i32::try_from(mtime.truncated_seconds()).unwrap()
433 439 } else {
434 440 MTIME_UNSET
435 441 }
@@ -501,6 +507,10 b' impl DirstateEntry {'
501 507 }
502 508 }
503 509
510 pub fn truncated_mtime(&self) -> Option<TruncatedTimestamp> {
511 self.mtime
512 }
513
504 514 pub fn drop_merge_data(&mut self) {
505 515 if self.flags.contains(Flags::P2_INFO) {
506 516 self.flags.remove(Flags::P2_INFO);
@@ -513,9 +523,13 b' impl DirstateEntry {'
513 523 self.mtime = None
514 524 }
515 525
516 pub fn set_clean(&mut self, mode: u32, size: u32, mtime: u32) {
526 pub fn set_clean(
527 &mut self,
528 mode: u32,
529 size: u32,
530 mtime: TruncatedTimestamp,
531 ) {
517 532 let size = size & RANGE_MASK_31BIT;
518 let mtime = mtime & RANGE_MASK_31BIT;
519 533 self.flags.insert(Flags::WDIR_TRACKED | Flags::P1_TRACKED);
520 534 self.mode_size = Some((mode, size));
521 535 self.mtime = Some(mtime);
@@ -577,8 +591,13 b' impl DirstateEntry {'
577 591 }
578 592
579 593 /// True if the stored mtime would be ambiguous with the current time
580 pub fn need_delay(&self, now: i32) -> bool {
581 self.state() == EntryState::Normal && self.mtime() == now
594 pub fn need_delay(&self, now: TruncatedTimestamp) -> bool {
595 if let Some(mtime) = self.mtime {
596 self.state() == EntryState::Normal
597 && mtime.truncated_seconds() == now.truncated_seconds()
598 } else {
599 false
600 }
582 601 }
583 602 }
584 603
@@ -135,6 +135,3 b' pub fn pack_entry('
135 135 packed.extend(source.as_bytes());
136 136 }
137 137 }
138
139 /// Seconds since the Unix epoch
140 pub struct Timestamp(pub i64);
@@ -12,6 +12,7 b''
12 12 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
13 13
14 14 use crate::{
15 dirstate::TruncatedTimestamp,
15 16 utils::hg_path::{HgPath, HgPathError},
16 17 PatternError,
17 18 };
@@ -64,7 +65,7 b' pub struct StatusOptions {'
64 65 /// Remember the most recent modification timeslot for status, to make
65 66 /// sure we won't miss future size-preserving file content modifications
66 67 /// that happen within the same timeslot.
67 pub last_normal_time: i64,
68 pub last_normal_time: TruncatedTimestamp,
68 69 /// Whether we are on a filesystem with UNIX-like exec flags
69 70 pub check_exec: bool,
70 71 pub list_clean: bool,
@@ -1,7 +1,6 b''
1 1 use bytes_cast::BytesCast;
2 2 use micro_timer::timed;
3 3 use std::borrow::Cow;
4 use std::convert::TryInto;
5 4 use std::path::PathBuf;
6 5
7 6 use super::on_disk;
@@ -11,7 +10,6 b' use super::path_with_basename::WithBasen'
11 10 use crate::dirstate::parsers::pack_entry;
12 11 use crate::dirstate::parsers::packed_entry_size;
13 12 use crate::dirstate::parsers::parse_dirstate_entries;
14 use crate::dirstate::parsers::Timestamp;
15 13 use crate::dirstate::CopyMapIter;
16 14 use crate::dirstate::StateMapIter;
17 15 use crate::dirstate::TruncatedTimestamp;
@@ -932,10 +930,9 b' impl OwningDirstateMap {'
932 930 pub fn pack_v1(
933 931 &mut self,
934 932 parents: DirstateParents,
935 now: Timestamp,
933 now: TruncatedTimestamp,
936 934 ) -> Result<Vec<u8>, DirstateError> {
937 935 let map = self.get_map_mut();
938 let now: i32 = now.0.try_into().expect("time overflow");
939 936 let mut ambiguous_mtimes = Vec::new();
940 937 // Optizimation (to be measured?): pre-compute size to avoid `Vec`
941 938 // reallocations
@@ -981,12 +978,10 b' impl OwningDirstateMap {'
981 978 #[timed]
982 979 pub fn pack_v2(
983 980 &mut self,
984 now: Timestamp,
981 now: TruncatedTimestamp,
985 982 can_append: bool,
986 983 ) -> Result<(Vec<u8>, Vec<u8>, bool), DirstateError> {
987 984 let map = self.get_map_mut();
988 // TODO: how do we want to handle this in 2038?
989 let now: i32 = now.0.try_into().expect("time overflow");
990 985 let mut paths = Vec::new();
991 986 for node in map.iter_nodes() {
992 987 let node = node?;
@@ -317,7 +317,7 b' impl Node {'
317 317 &self,
318 318 ) -> Result<dirstate_map::NodeData, DirstateV2ParseError> {
319 319 if self.has_entry() {
320 Ok(dirstate_map::NodeData::Entry(self.assume_entry()))
320 Ok(dirstate_map::NodeData::Entry(self.assume_entry()?))
321 321 } else if let Some(mtime) = self.cached_directory_mtime()? {
322 322 Ok(dirstate_map::NodeData::CachedDirectory { mtime })
323 323 } else {
@@ -357,7 +357,7 b' impl Node {'
357 357 file_type | permisions
358 358 }
359 359
360 fn assume_entry(&self) -> DirstateEntry {
360 fn assume_entry(&self) -> Result<DirstateEntry, DirstateV2ParseError> {
361 361 // TODO: convert through raw bits instead?
362 362 let wdir_tracked = self.flags().contains(Flags::WDIR_TRACKED);
363 363 let p1_tracked = self.flags().contains(Flags::P1_TRACKED);
@@ -372,11 +372,19 b' impl Node {'
372 372 let mtime = if self.flags().contains(Flags::HAS_FILE_MTIME)
373 373 && !self.flags().contains(Flags::EXPECTED_STATE_IS_MODIFIED)
374 374 {
375 Some(self.mtime.truncated_seconds.into())
375 // TODO: replace this by `self.mtime.try_into()?` to use
376 // sub-second precision from the file.
377 // We don’t do this yet because other parts of the code
378 // always set it to zero.
379 let mtime = TruncatedTimestamp::from_already_truncated(
380 self.mtime.truncated_seconds.get(),
381 0,
382 )?;
383 Some(mtime)
376 384 } else {
377 385 None
378 386 };
379 DirstateEntry::from_v2_data(
387 Ok(DirstateEntry::from_v2_data(
380 388 wdir_tracked,
381 389 p1_tracked,
382 390 p2_info,
@@ -384,14 +392,14 b' impl Node {'
384 392 mtime,
385 393 None,
386 394 None,
387 )
395 ))
388 396 }
389 397
390 398 pub(super) fn entry(
391 399 &self,
392 400 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
393 401 if self.has_entry() {
394 Ok(Some(self.assume_entry()))
402 Ok(Some(self.assume_entry()?))
395 403 } else {
396 404 Ok(None)
397 405 }
@@ -450,10 +458,7 b' impl Node {'
450 458 };
451 459 let mtime = if let Some(m) = mtime_opt {
452 460 flags.insert(Flags::HAS_FILE_MTIME);
453 PackedTruncatedTimestamp {
454 truncated_seconds: m.into(),
455 nanoseconds: 0.into(),
456 }
461 m.into()
457 462 } else {
458 463 PackedTruncatedTimestamp::null()
459 464 };
@@ -501,9 +501,6 b" impl<'a, 'tree, 'on_disk> StatusCommon<'"
501 501 fn truncate_u64(value: u64) -> i32 {
502 502 (value & 0x7FFF_FFFF) as i32
503 503 }
504 fn truncate_i64(value: i64) -> i32 {
505 (value & 0x7FFF_FFFF) as i32
506 }
507 504
508 505 let entry = dirstate_node
509 506 .entry()?
@@ -531,10 +528,19 b" impl<'a, 'tree, 'on_disk> StatusCommon<'"
531 528 .modified
532 529 .push(hg_path.detach_from_tree())
533 530 } else {
534 let mtime = mtime_seconds(fs_metadata);
535 if truncate_i64(mtime) != entry.mtime()
536 || mtime == self.options.last_normal_time
537 {
531 let mtime_looks_clean;
532 if let Some(dirstate_mtime) = entry.truncated_mtime() {
533 let fs_mtime = TruncatedTimestamp::for_mtime_of(fs_metadata)
534 .expect("OS/libc does not support mtime?")
535 // For now don’t use sub-second precision for file mtimes
536 .to_integer_second();
537 mtime_looks_clean = fs_mtime.likely_equal(dirstate_mtime)
538 && !fs_mtime.likely_equal(self.options.last_normal_time)
539 } else {
540 // No mtime in the dirstate entry
541 mtime_looks_clean = false
542 };
543 if !mtime_looks_clean {
538 544 self.outcome
539 545 .lock()
540 546 .unwrap()
@@ -690,15 +696,6 b" impl<'a, 'tree, 'on_disk> StatusCommon<'"
690 696 }
691 697 }
692 698
693 #[cfg(unix)] // TODO
694 fn mtime_seconds(metadata: &std::fs::Metadata) -> i64 {
695 // Going through `Metadata::modified()` would be portable, but would take
696 // care to construct a `SystemTime` value with sub-second precision just
697 // for us to throw that away here.
698 use std::os::unix::fs::MetadataExt;
699 metadata.mtime()
700 }
701
702 699 struct DirEntry {
703 700 base_name: HgPathBuf,
704 701 full_path: PathBuf,
@@ -54,7 +54,7 b' pub fn init_module(py: Python, package: '
54 54 matcher: PyObject,
55 55 ignorefiles: PyList,
56 56 check_exec: bool,
57 last_normal_time: i64,
57 last_normal_time: (u32, u32),
58 58 list_clean: bool,
59 59 list_ignored: bool,
60 60 list_unknown: bool,
@@ -18,11 +18,10 b' use cpython::{'
18 18
19 19 use crate::{
20 20 dirstate::copymap::{CopyMap, CopyMapItemsIterator, CopyMapKeysIterator},
21 dirstate::item::DirstateItem,
21 dirstate::item::{timestamp, DirstateItem},
22 22 pybytes_deref::PyBytesDeref,
23 23 };
24 24 use hg::{
25 dirstate::parsers::Timestamp,
26 25 dirstate::StateMapIter,
27 26 dirstate_tree::dirstate_map::DirstateMap as TreeDirstateMap,
28 27 dirstate_tree::on_disk::DirstateV2ParseError,
@@ -195,9 +194,9 b' py_class!(pub class DirstateMap |py| {'
195 194 &self,
196 195 p1: PyObject,
197 196 p2: PyObject,
198 now: PyObject
197 now: (u32, u32)
199 198 ) -> PyResult<PyBytes> {
200 let now = Timestamp(now.extract(py)?);
199 let now = timestamp(py, now)?;
201 200
202 201 let mut inner = self.inner(py).borrow_mut();
203 202 let parents = DirstateParents {
@@ -219,10 +218,10 b' py_class!(pub class DirstateMap |py| {'
219 218 /// instead of written to a new data file (False).
220 219 def write_v2(
221 220 &self,
222 now: PyObject,
221 now: (u32, u32),
223 222 can_append: bool,
224 223 ) -> PyResult<PyObject> {
225 let now = Timestamp(now.extract(py)?);
224 let now = timestamp(py, now)?;
226 225
227 226 let mut inner = self.inner(py).borrow_mut();
228 227 let result = inner.pack_v2(now, can_append);
@@ -9,6 +9,7 b' use cpython::Python;'
9 9 use cpython::PythonObject;
10 10 use hg::dirstate::DirstateEntry;
11 11 use hg::dirstate::EntryState;
12 use hg::dirstate::TruncatedTimestamp;
12 13 use std::cell::Cell;
13 14 use std::convert::TryFrom;
14 15
@@ -22,7 +23,7 b' py_class!(pub class DirstateItem |py| {'
22 23 p2_info: bool = false,
23 24 has_meaningful_data: bool = true,
24 25 has_meaningful_mtime: bool = true,
25 parentfiledata: Option<(u32, u32, u32)> = None,
26 parentfiledata: Option<(u32, u32, (u32, u32))> = None,
26 27 fallback_exec: Option<bool> = None,
27 28 fallback_symlink: Option<bool> = None,
28 29
@@ -34,7 +35,7 b' py_class!(pub class DirstateItem |py| {'
34 35 mode_size_opt = Some((mode, size))
35 36 }
36 37 if has_meaningful_mtime {
37 mtime_opt = Some(mtime)
38 mtime_opt = Some(timestamp(py, mtime)?)
38 39 }
39 40 }
40 41 let entry = DirstateEntry::from_v2_data(
@@ -191,10 +192,19 b' py_class!(pub class DirstateItem |py| {'
191 192 Ok(mtime)
192 193 }
193 194
194 def need_delay(&self, now: i32) -> PyResult<bool> {
195 def need_delay(&self, now: (u32, u32)) -> PyResult<bool> {
196 let now = timestamp(py, now)?;
195 197 Ok(self.entry(py).get().need_delay(now))
196 198 }
197 199
200 def mtime_likely_equal_to(&self, other: (u32, u32)) -> PyResult<bool> {
201 if let Some(mtime) = self.entry(py).get().truncated_mtime() {
202 Ok(mtime.likely_equal(timestamp(py, other)?))
203 } else {
204 Ok(false)
205 }
206 }
207
198 208 @classmethod
199 209 def from_v1_data(
200 210 _cls,
@@ -220,8 +230,9 b' py_class!(pub class DirstateItem |py| {'
220 230 &self,
221 231 mode: u32,
222 232 size: u32,
223 mtime: u32,
233 mtime: (u32, u32),
224 234 ) -> PyResult<PyNone> {
235 let mtime = timestamp(py, mtime)?;
225 236 self.update(py, |entry| entry.set_clean(mode, size, mtime));
226 237 Ok(PyNone)
227 238 }
@@ -261,3 +272,15 b' impl DirstateItem {'
261 272 self.entry(py).set(entry)
262 273 }
263 274 }
275
276 pub(crate) fn timestamp(
277 py: Python<'_>,
278 (s, ns): (u32, u32),
279 ) -> PyResult<TruncatedTimestamp> {
280 TruncatedTimestamp::from_already_truncated(s, ns).map_err(|_| {
281 PyErr::new::<exc::ValueError, _>(
282 py,
283 "expected mtime truncated to 31 bits",
284 )
285 })
286 }
@@ -9,6 +9,7 b''
9 9 //! `hg-core` crate. From Python, this will be seen as
10 10 //! `rustext.dirstate.status`.
11 11
12 use crate::dirstate::item::timestamp;
12 13 use crate::{dirstate::DirstateMap, exceptions::FallbackError};
13 14 use cpython::exc::OSError;
14 15 use cpython::{
@@ -102,12 +103,13 b' pub fn status_wrapper('
102 103 root_dir: PyObject,
103 104 ignore_files: PyList,
104 105 check_exec: bool,
105 last_normal_time: i64,
106 last_normal_time: (u32, u32),
106 107 list_clean: bool,
107 108 list_ignored: bool,
108 109 list_unknown: bool,
109 110 collect_traversed_dirs: bool,
110 111 ) -> PyResult<PyTuple> {
112 let last_normal_time = timestamp(py, last_normal_time)?;
111 113 let bytes = root_dir.extract::<PyBytes>(py)?;
112 114 let root_dir = get_path_from_bytes(bytes.data(py));
113 115
@@ -11,6 +11,7 b' use crate::utils::path_utils::relativize'
11 11 use clap::{Arg, SubCommand};
12 12 use hg;
13 13 use hg::config::Config;
14 use hg::dirstate::TruncatedTimestamp;
14 15 use hg::errors::HgError;
15 16 use hg::manifest::Manifest;
16 17 use hg::matchers::AlwaysMatcher;
@@ -180,7 +181,7 b' pub fn run(invocation: &crate::CliInvoca'
180 181 // hence be stored on dmap. Using a value that assumes we aren't
181 182 // below the time resolution granularity of the FS and the
182 183 // dirstate.
183 last_normal_time: 0,
184 last_normal_time: TruncatedTimestamp::new_truncate(0, 0),
184 185 // we're currently supporting file systems with exec flags only
185 186 // anyway
186 187 check_exec: true,
@@ -15,6 +15,7 b' from mercurial import ('
15 15 policy,
16 16 registrar,
17 17 )
18 from mercurial.dirstateutils import timestamp
18 19 from mercurial.utils import dateutil
19 20
20 21 try:
@@ -40,9 +41,8 b" has_rust_dirstate = policy.importrust('d"
40 41 def pack_dirstate(fakenow, orig, dmap, copymap, pl, now):
41 42 # execute what original parsers.pack_dirstate should do actually
42 43 # for consistency
43 actualnow = int(now)
44 44 for f, e in dmap.items():
45 if e.need_delay(actualnow):
45 if e.need_delay(now):
46 46 e.set_possibly_dirty()
47 47
48 48 return orig(dmap, copymap, pl, fakenow)
@@ -62,6 +62,7 b' def fakewrite(ui, func):'
62 62 # parsing 'fakenow' in YYYYmmddHHMM format makes comparison between
63 63 # 'fakenow' value and 'touch -t YYYYmmddHHMM' argument easy
64 64 fakenow = dateutil.parsedate(fakenow, [b'%Y%m%d%H%M'])[0]
65 fakenow = timestamp.timestamp((fakenow, 0))
65 66
66 67 if has_rust_dirstate:
67 68 # The Rust implementation does not use public parse/pack dirstate
General Comments 0
You need to be logged in to leave comments. Login now