changeset 32651:c850f0ed54c1 stable 4.2.1

status: don't crash if a lookup file disappears This can happen if another process (even another hg process!) comes along and removes the file at that time. This partly resolves issue5584, but not completely -- a bogus dirstate update can still happen. However, the full fix is too involved for stable.
author Siddharth Agarwal <sid0@fb.com>
date Fri, 02 Jun 2017 22:27:52 -0700
parents 85b978031a75
children 9929af2b09b4 028e6b1fd119
files mercurial/context.py tests/test-dirstate-race.t
diffstat 2 files changed, 82 insertions(+), 9 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/context.py	Fri Jun 02 10:44:40 2017 +0200
+++ b/mercurial/context.py	Fri Jun 02 22:27:52 2017 -0700
@@ -1613,18 +1613,30 @@
     def _checklookup(self, files):
         # check for any possibly clean files
         if not files:
-            return [], []
+            return [], [], []
 
         modified = []
+        deleted = []
         fixup = []
         pctx = self._parents[0]
         # do a full compare of any files that might have changed
         for f in sorted(files):
-            if (f not in pctx or self.flags(f) != pctx.flags(f)
-                or pctx[f].cmp(self[f])):
-                modified.append(f)
-            else:
-                fixup.append(f)
+            try:
+                # This will return True for a file that got replaced by a
+                # directory in the interim, but fixing that is pretty hard.
+                if (f not in pctx or self.flags(f) != pctx.flags(f)
+                    or pctx[f].cmp(self[f])):
+                    modified.append(f)
+                else:
+                    fixup.append(f)
+            except (IOError, OSError):
+                # A file become inaccessible in between? Mark it as deleted,
+                # matching dirstate behavior (issue5584).
+                # The dirstate has more complex behavior around whether a
+                # missing file matches a directory, etc, but we don't need to
+                # bother with that: if f has made it to this point, we're sure
+                # it's in the dirstate.
+                deleted.append(f)
 
         # update dirstate for files that are actually clean
         if fixup:
@@ -1644,7 +1656,7 @@
                     self._repo.dirstate.write(self._repo.currenttransaction())
             except error.LockError:
                 pass
-        return modified, fixup
+        return modified, deleted, fixup
 
     def _dirstatestatus(self, match=None, ignored=False, clean=False,
                         unknown=False):
@@ -1659,8 +1671,9 @@
 
         # check for any possibly clean files
         if cmp:
-            modified2, fixup = self._checklookup(cmp)
+            modified2, deleted2, fixup = self._checklookup(cmp)
             s.modified.extend(modified2)
+            s.deleted.extend(deleted2)
 
             # update dirstate for files that are actually clean
             if fixup and listclean:
--- a/tests/test-dirstate-race.t	Fri Jun 02 10:44:40 2017 +0200
+++ b/tests/test-dirstate-race.t	Fri Jun 02 22:27:52 2017 -0700
@@ -1,4 +1,5 @@
-  $ hg init
+  $ hg init repo
+  $ cd repo
   $ echo a > a
   $ hg add a
   $ hg commit -m test
@@ -31,3 +32,62 @@
   M a
   M a
 
+  $ echo test > b
+  $ mkdir dir1
+  $ echo test > dir1/c
+  $ echo test > d
+
+  $ echo test > e
+#if execbit
+A directory will typically have the execute bit -- make sure it doesn't get
+confused with a file with the exec bit set
+  $ chmod +x e
+#endif
+
+  $ hg add b dir1 d e
+  adding dir1/c
+  $ hg commit -m test2
+
+  $ cat >> $TESTTMP/dirstaterace.py << EOF
+  > from mercurial import (
+  >     context,
+  >     extensions,
+  > )
+  > def extsetup():
+  >     extensions.wrapfunction(context.workingctx, '_checklookup', overridechecklookup)
+  > def overridechecklookup(orig, self, files):
+  >     # make an update that changes the dirstate from underneath
+  >     self._repo.ui.system(self._repo.ui.config('dirstaterace', 'command'), cwd=self._repo.root)
+  >     return orig(self, files)
+  > EOF
+
+  $ hg debugrebuilddirstate
+  $ hg debugdirstate
+  n   0         -1 unset               a
+  n   0         -1 unset               b
+  n   0         -1 unset               d
+  n   0         -1 unset               dir1/c
+  n   0         -1 unset               e
+
+XXX Note that this returns M for files that got replaced by directories. This is
+definitely a bug, but the fix for that is hard and the next status run is fine
+anyway.
+
+  $ hg status --config extensions.dirstaterace=$TESTTMP/dirstaterace.py \
+  >   --config dirstaterace.command='rm b && rm -r dir1 && rm d && mkdir d && rm e && mkdir e'
+  M d
+  M e
+  ! b
+  ! dir1/c
+  $ hg debugdirstate
+  n 644          2 * a (glob)
+  n   0         -1 unset               b
+  n   0         -1 unset               d
+  n   0         -1 unset               dir1/c
+  n   0         -1 unset               e
+
+  $ hg status
+  ! b
+  ! d
+  ! dir1/c
+  ! e