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.
--- 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