changeset 51849:95cdc01f313d

sparse: reliably avoid writing to store without a lock With the code as written before this patch we can still end up writing to store in `debugsparse`. Obviously we'll write to it if by accident a store requirement is modified, but more importantly we write to it if another concurrent transaction modifies the requirements file on disk. We can't rule this out since we're not holding the store lock, so it's better to explicitly pass a permission to write instead of inferring it based on file contents.
author Arseniy Alekseyev <aalekseyev@janestreet.com>
date Fri, 16 Aug 2024 11:12:19 +0100
parents 0d7ccb163b4f
children 0b2c978f595f
files mercurial/scmutil.py mercurial/sparse.py
diffstat 2 files changed, 10 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/scmutil.py	Thu Aug 15 13:52:14 2024 +0100
+++ b/mercurial/scmutil.py	Fri Aug 16 11:12:19 2024 +0100
@@ -1668,7 +1668,7 @@
     return requirementsmod.TREEMANIFEST_REQUIREMENT in repo.requirements
 
 
-def writereporequirements(repo, requirements=None) -> None:
+def writereporequirements(repo, requirements=None, maywritestore=True) -> None:
     """writes requirements for the repo
 
     Requirements are written to .hg/requires and .hg/store/requires based
@@ -1681,10 +1681,11 @@
     if wcreq is not None:
         writerequires(repo.vfs, wcreq)
     if storereq is not None:
-        writerequires(repo.svfs, storereq)
+        writerequires(repo.svfs, storereq, maywrite=maywritestore)
     elif repo.ui.configbool(b'format', b'usestore'):
         # only remove store requires if we are using store
-        repo.svfs.tryunlink(b'requires')
+        if maywritestore:
+            repo.svfs.tryunlink(b'requires')
 
 
 def readrequires(vfs, allowmissing):
@@ -1701,9 +1702,11 @@
     return set(read(b'requires').splitlines())
 
 
-def writerequires(opener, requirements) -> None:
+def writerequires(opener, requirements, maywrite=True) -> None:
     on_disk = readrequires(opener, True)
     if not (on_disk == set(requirements)):
+        if not maywrite:
+            raise error.Abort(_(b"store requirements are not as expected"))
         with opener(b'requires', b'w', atomictemp=True) as fp:
             for r in sorted(requirements):
                 fp.write(b"%s\n" % r)
--- a/mercurial/sparse.py	Thu Aug 15 13:52:14 2024 +0100
+++ b/mercurial/sparse.py	Fri Aug 16 11:12:19 2024 +0100
@@ -632,10 +632,10 @@
 
         if requirements.SPARSE_REQUIREMENT in oldrequires and removing:
             repo.requirements.discard(requirements.SPARSE_REQUIREMENT)
-            scmutil.writereporequirements(repo)
+            scmutil.writereporequirements(repo, maywritestore=False)
         elif requirements.SPARSE_REQUIREMENT not in oldrequires:
             repo.requirements.add(requirements.SPARSE_REQUIREMENT)
-            scmutil.writereporequirements(repo)
+            scmutil.writereporequirements(repo, maywritestore=False)
 
         try:
             writeconfig(repo, includes, excludes, profiles)
@@ -644,7 +644,7 @@
             if repo.requirements != oldrequires:
                 repo.requirements.clear()
                 repo.requirements |= oldrequires
-                scmutil.writereporequirements(repo)
+                scmutil.writereporequirements(repo, maywritestore=False)
             writeconfig(repo, oldincludes, oldexcludes, oldprofiles)
             raise