mercurial/dirstatemap.py
changeset 47674 ff97e793ed36
parent 47672 0efaa1bbad2b
child 47675 48aec076b8fb
--- a/mercurial/dirstatemap.py	Thu Jul 15 17:24:09 2021 +0200
+++ b/mercurial/dirstatemap.py	Thu Jul 08 12:18:21 2021 +0200
@@ -18,6 +18,10 @@
     util,
 )
 
+from .dirstateutils import (
+    docket as docketmod,
+)
+
 parsers = policy.importmod('parsers')
 rustmod = policy.importrust('dirstate')
 
@@ -416,7 +420,7 @@
         self.__getitem__ = self._map.__getitem__
         self.get = self._map.get
 
-    def write(self, st, now):
+    def write(self, _tr, st, now):
         st.write(
             parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
         )
@@ -466,6 +470,7 @@
             self._nodelen = 20  # Also update Rust code when changing this!
             self._parents = None
             self._dirtyparents = False
+            self._docket = None
 
             # for consistent view between _pl() and _read() invocations
             self._pendingmode = None
@@ -565,6 +570,16 @@
             self._pendingmode = mode
             return fp
 
+        def _readdirstatefile(self, size=-1):
+            try:
+                with self._opendirstatefile() as fp:
+                    return fp.read(size)
+            except IOError as err:
+                if err.errno != errno.ENOENT:
+                    raise
+                # File doesn't exist, so the current state is empty
+                return b''
+
         def setparents(self, p1, p2):
             self._parents = (p1, p2)
             self._dirtyparents = True
@@ -572,39 +587,40 @@
         def parents(self):
             if not self._parents:
                 if self._use_dirstate_v2:
-                    offset = len(rustmod.V2_FORMAT_MARKER)
+                    self._parents = self.docket.parents
                 else:
-                    offset = 0
-                read_len = offset + self._nodelen * 2
-                try:
-                    fp = self._opendirstatefile()
-                    st = fp.read(read_len)
-                    fp.close()
-                except IOError as err:
-                    if err.errno != errno.ENOENT:
-                        raise
-                    # File doesn't exist, so the current state is empty
-                    st = b''
-
-                l = len(st)
-                if l == read_len:
-                    st = st[offset:]
-                    self._parents = (
-                        st[: self._nodelen],
-                        st[self._nodelen : 2 * self._nodelen],
-                    )
-                elif l == 0:
-                    self._parents = (
-                        self._nodeconstants.nullid,
-                        self._nodeconstants.nullid,
-                    )
-                else:
-                    raise error.Abort(
-                        _(b'working directory state appears damaged!')
-                    )
+                    read_len = self._nodelen * 2
+                    st = self._readdirstatefile(read_len)
+                    l = len(st)
+                    if l == read_len:
+                        self._parents = (
+                            st[: self._nodelen],
+                            st[self._nodelen : 2 * self._nodelen],
+                        )
+                    elif l == 0:
+                        self._parents = (
+                            self._nodeconstants.nullid,
+                            self._nodeconstants.nullid,
+                        )
+                    else:
+                        raise error.Abort(
+                            _(b'working directory state appears damaged!')
+                        )
 
             return self._parents
 
+        @property
+        def docket(self):
+            if not self._docket:
+                if not self._use_dirstate_v2:
+                    raise error.ProgrammingError(
+                        b'dirstate only has a docket in v2 format'
+                    )
+                self._docket = docketmod.DirstateDocket.parse(
+                    self._readdirstatefile(), self._nodeconstants
+                )
+            return self._docket
+
         @propertycache
         def _rustmap(self):
             """
@@ -615,20 +631,18 @@
                 self._opener.join(self._filename)
             )
 
-            try:
-                fp = self._opendirstatefile()
-                try:
-                    st = fp.read()
-                finally:
-                    fp.close()
-            except IOError as err:
-                if err.errno != errno.ENOENT:
-                    raise
-                st = b''
-
-            self._rustmap, parents = rustmod.DirstateMap.new(
-                self._use_dirstate_tree, self._use_dirstate_v2, st
-            )
+            if self._use_dirstate_v2:
+                if self.docket.uuid:
+                    # TODO: use mmap when possible
+                    data = self._opener.read(self.docket.data_filename())
+                else:
+                    data = b''
+                self._rustmap = rustmod.DirstateMap.new_v2(data)
+                parents = self.docket.parents
+            else:
+                self._rustmap, parents = rustmod.DirstateMap.new_v1(
+                    self._use_dirstate_tree, self._readdirstatefile()
+                )
 
             if parents and not self._dirtyparents:
                 self.setparents(*parents)
@@ -638,13 +652,29 @@
             self.get = self._rustmap.get
             return self._rustmap
 
-        def write(self, st, now):
-            parents = self.parents()
-            packed = self._rustmap.write(
-                self._use_dirstate_v2, parents[0], parents[1], now
-            )
-            st.write(packed)
-            st.close()
+        def write(self, tr, st, now):
+            if self._use_dirstate_v2:
+                packed = self._rustmap.write_v2(now)
+                old_docket = self.docket
+                new_docket = docketmod.DirstateDocket.with_new_uuid(
+                    self.parents(), len(packed)
+                )
+                self._opener.write(new_docket.data_filename(), packed)
+                # Write the new docket after the new data file has been
+                # written. Because `st` was opened with `atomictemp=True`,
+                # the actual `.hg/dirstate` file is only affected on close.
+                st.write(new_docket.serialize())
+                st.close()
+                # Remove the old data file after the new docket pointing to
+                # the new data file was written.
+                if old_docket.uuid:
+                    self._opener.unlink(old_docket.data_filename())
+                self._docket = new_docket
+            else:
+                p1, p2 = self.parents()
+                packed = self._rustmap.write_v1(p1, p2, now)
+                st.write(packed)
+                st.close()
             self._dirtyparents = False
 
         @propertycache