diff mercurial/dirstate.py @ 48698:568f63b5a30f

dirstate: introduce a "tracked-key" feature A new format variant is introduced. When used, a `tracked-key` file will be generated. That file will be update when the set of tracked file might have changed. This will be useful for external automation (e.g. build tool) to be notified when the set of relevant files changes. One of the motivation for this changes is to mitigate effect dirstate-v2 has on such automation. Since the dirstate file is updated much more frequently on dirstate-v2, monitoring update to that file is no longer a viable strategy. See the associated documentation for details about the feature To prevent older client to update the repository without updating that file, a new requirements is introduced. The `postfinalizegenerators` business is a bit weird, so I'll likely clean that up soon. Differential Revision: https://phab.mercurial-scm.org/D12124
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Mon, 31 Jan 2022 08:44:48 +0100
parents 111f5a0cbcaa
children 21ac6aedd5e5
line wrap: on
line diff
--- a/mercurial/dirstate.py	Mon Jan 31 14:26:35 2022 +0100
+++ b/mercurial/dirstate.py	Mon Jan 31 08:44:48 2022 +0100
@@ -12,6 +12,7 @@
 import errno
 import os
 import stat
+import uuid
 
 from .i18n import _
 from .pycompat import delattr
@@ -23,6 +24,7 @@
     encoding,
     error,
     match as matchmod,
+    node,
     pathutil,
     policy,
     pycompat,
@@ -99,6 +101,7 @@
         sparsematchfn,
         nodeconstants,
         use_dirstate_v2,
+        use_tracked_key=False,
     ):
         """Create a new dirstate object.
 
@@ -107,6 +110,7 @@
         the dirstate.
         """
         self._use_dirstate_v2 = use_dirstate_v2
+        self._use_tracked_key = use_tracked_key
         self._nodeconstants = nodeconstants
         self._opener = opener
         self._validate = validate
@@ -115,11 +119,15 @@
         # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
         # UNC path pointing to root share (issue4557)
         self._rootdir = pathutil.normasprefix(root)
+        # True is any internal state may be different
         self._dirty = False
+        # True if the set of tracked file may be different
+        self._dirty_tracked_set = False
         self._ui = ui
         self._filecache = {}
         self._parentwriters = 0
         self._filename = b'dirstate'
+        self._filename_tk = b'dirstate-tracked-key'
         self._pendingfilename = b'%s.pending' % self._filename
         self._plchangecallbacks = {}
         self._origpl = None
@@ -409,6 +417,7 @@
             if a in self.__dict__:
                 delattr(self, a)
         self._dirty = False
+        self._dirty_tracked_set = False
         self._parentwriters = 0
         self._origpl = None
 
@@ -446,6 +455,8 @@
         pre_tracked = self._map.set_tracked(filename)
         if reset_copy:
             self._map.copymap.pop(filename, None)
+        if pre_tracked:
+            self._dirty_tracked_set = True
         return pre_tracked
 
     @requires_no_parents_change
@@ -460,6 +471,7 @@
         ret = self._map.set_untracked(filename)
         if ret:
             self._dirty = True
+            self._dirty_tracked_set = True
         return ret
 
     @requires_no_parents_change
@@ -544,6 +556,13 @@
         # this. The test agrees
 
         self._dirty = True
+        old_entry = self._map.get(filename)
+        if old_entry is None:
+            prev_tracked = False
+        else:
+            prev_tracked = old_entry.tracked
+        if prev_tracked != wc_tracked:
+            self._dirty_tracked_set = True
 
         self._map.reset_state(
             filename,
@@ -702,20 +721,44 @@
         if not self._dirty:
             return
 
-        filename = self._filename
+        write_key = self._use_tracked_key and self._dirty_tracked_set
         if tr:
             # delay writing in-memory changes out
+            if write_key:
+                tr.addfilegenerator(
+                    b'dirstate-0-key-pre',
+                    (self._filename_tk,),
+                    lambda f: self._write_tracked_key(tr, f),
+                    location=b'plain',
+                )
             tr.addfilegenerator(
                 b'dirstate-1-main',
                 (self._filename,),
                 lambda f: self._writedirstate(tr, f),
                 location=b'plain',
             )
+            if write_key:
+                tr.addfilegenerator(
+                    b'dirstate-2-key-post',
+                    (self._filename_tk,),
+                    lambda f: self._write_tracked_key(tr, f),
+                    location=b'plain',
+                )
             return
 
         file = lambda f: self._opener(f, b"w", atomictemp=True, checkambig=True)
+        if write_key:
+            # we change the key-file before changing the dirstate to make sure
+            # reading invalidate there cache before we start writing
+            with file(self._filename_tk) as f:
+                self._write_tracked_key(tr, f)
         with file(self._filename) as f:
             self._writedirstate(tr, f)
+        if write_key:
+            # we update the key-file after writing to make sure reader have a
+            # key that match the newly written content
+            with file(self._filename_tk) as f:
+                self._write_tracked_key(tr, f)
 
     def addparentchangecallback(self, category, callback):
         """add a callback to be called when the wd parents are changed
@@ -736,9 +779,13 @@
             ):
                 callback(self, self._origpl, self._pl)
             self._origpl = None
-
         self._map.write(tr, st)
         self._dirty = False
+        self._dirty_tracked_set = False
+
+    def _write_tracked_key(self, tr, f):
+        key = node.hex(uuid.uuid4().bytes)
+        f.write(b"1\n%s\n" % key)  # 1 is the format version
 
     def _dirignore(self, f):
         if self._ignore(f):