changeset 49467:0705afae6253 stable

dirstate-v2: backup the data file during the transaction (issue6730) If we backup of the docket, without doing a backup of the data file, we highly risk restoring a docket pointing to a missing file in the future. So we now backup the data-file alongside the docket.
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Wed, 31 Aug 2022 06:37:42 +0200
parents 486b8a383100
children 2905b78fc52e
files mercurial/dirstate.py mercurial/localrepo.py tests/test-dirstate.t
diffstat 3 files changed, 59 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/dirstate.py	Wed Aug 31 05:48:32 2022 +0200
+++ b/mercurial/dirstate.py	Wed Aug 31 06:37:42 2022 +0200
@@ -31,6 +31,7 @@
 )
 
 from .dirstateutils import (
+    docket as docketmod,
     timestamp,
 )
 
@@ -1433,6 +1434,27 @@
         else:
             return self._filename
 
+    def data_backup_filename(self, backupname):
+        if not self._use_dirstate_v2:
+            return None
+        return backupname + b'.v2-data'
+
+    def _new_backup_data_filename(self, backupname):
+        """return a filename to backup a data-file or None"""
+        if not self._use_dirstate_v2:
+            return None
+        data_filename = self._map.docket.data_filename()
+        return data_filename, self.data_backup_filename(backupname)
+
+    def backup_data_file(self, backupname):
+        if not self._use_dirstate_v2:
+            return None
+        docket = docketmod.DirstateDocket.parse(
+            self._opener.read(backupname),
+            self._nodeconstants,
+        )
+        return self.data_backup_filename(backupname), docket.data_filename()
+
     def savebackup(self, tr, backupname):
         '''Save current dirstate into backup file'''
         filename = self._actualfilename(tr)
@@ -1472,6 +1494,19 @@
             self._opener.join(backupname),
             hardlink=True,
         )
+        data_pair = self._new_backup_data_filename(backupname)
+        if data_pair is not None:
+            data_filename, bck_data_filename = data_pair
+            util.copyfile(
+                self._opener.join(data_filename),
+                self._opener.join(bck_data_filename),
+                hardlink=True,
+            )
+            if tr is not None:
+                # ensure that pending file written above is unlinked at
+                # failure, even if tr.writepending isn't invoked until the
+                # end of this transaction
+                tr.registertmp(bck_data_filename, location=b'plain')
 
     def restorebackup(self, tr, backupname):
         '''Restore dirstate by backup file'''
@@ -1480,14 +1515,29 @@
         self.invalidate()
         filename = self._actualfilename(tr)
         o = self._opener
+        data_pair = self.backup_data_file(backupname)
         if util.samefile(o.join(backupname), o.join(filename)):
             o.unlink(backupname)
         else:
             o.rename(backupname, filename, checkambig=True)
 
+        if data_pair is not None:
+            data_backup, target = data_pair
+            if o.exists(target) and util.samefile(
+                o.join(data_backup), o.join(target)
+            ):
+                o.unlink(data_backup)
+            else:
+                o.rename(data_backup, target, checkambig=True)
+
     def clearbackup(self, tr, backupname):
         '''Clear backup file'''
-        self._opener.unlink(backupname)
+        o = self._opener
+        data_backup = self.backup_data_file(backupname)
+        o.unlink(backupname)
+
+        if data_backup is not None:
+            o.unlink(data_backup[0])
 
     def verify(self, m1, m2):
         """check the dirstate content again the parent manifest and yield errors"""
--- a/mercurial/localrepo.py	Wed Aug 31 05:48:32 2022 +0200
+++ b/mercurial/localrepo.py	Wed Aug 31 06:37:42 2022 +0200
@@ -2618,16 +2618,23 @@
         return tr
 
     def _journalfiles(self):
-        return (
+        first = (
             (self.svfs, b'journal'),
             (self.svfs, b'journal.narrowspec'),
             (self.vfs, b'journal.narrowspec.dirstate'),
             (self.vfs, b'journal.dirstate'),
+        )
+        middle = []
+        dirstate_data = self.dirstate.data_backup_filename(b'journal.dirstate')
+        if dirstate_data is not None:
+            middle.append((self.vfs, dirstate_data))
+        end = (
             (self.vfs, b'journal.branch'),
             (self.vfs, b'journal.desc'),
             (bookmarks.bookmarksvfs(self), b'journal.bookmarks'),
             (self.svfs, b'journal.phaseroots'),
         )
+        return first + tuple(middle) + end
 
     def undofiles(self):
         return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
--- a/tests/test-dirstate.t	Wed Aug 31 05:48:32 2022 +0200
+++ b/tests/test-dirstate.t	Wed Aug 31 06:37:42 2022 +0200
@@ -243,11 +243,5 @@
   repository tip rolled back to revision 1 (undo commit)
   working directory now based on revision 1
 
-#if dirstate-v1
   $ hg status
   A foo
-#else
-  $ hg status
-  abort: $ENOENT$: '*/.hg/dirstate.*' (glob) (known-bad-output !)
-  [255]
-#endif