diff --git a/mercurial/dirstate.py b/mercurial/dirstate.py
index ff4df09547429d32a8e75476fc752b834b1b824e_bWVyY3VyaWFsL2RpcnN0YXRlLnB5..96e526fe5fb0c900cbd198728a8e5636e36092ab_bWVyY3VyaWFsL2RpcnN0YXRlLnB5 100644
--- a/mercurial/dirstate.py
+++ b/mercurial/dirstate.py
@@ -72,6 +72,9 @@
             msg = 'calling `%s` outside of a parentchange context'
             msg %= func.__name__
             raise error.ProgrammingError(msg)
+        if self._invalidated_context:
+            msg = 'calling `%s` after the dirstate was invalidated'
+            raise error.ProgrammingError(msg)
         return func(self, *args, **kwargs)
 
     return wrap
@@ -124,4 +127,5 @@
         self._dirty_tracked_set = False
         self._ui = ui
         self._filecache = {}
+        # nesting level of `parentchange` context
         self._parentwriters = 0
@@ -127,4 +131,7 @@
         self._parentwriters = 0
+        # True if the current dirstate changing operations have been
+        # invalidated (used to make sure all nested contexts have been exited)
+        self._invalidated_context = False
         self._filename = b'dirstate'
         self._filename_th = b'dirstate-tracked-hint'
         self._pendingfilename = b'%s.pending' % self._filename
@@ -151,4 +158,7 @@
         the incoherent dirstate won't be written when wlock is
         released.
         """
+        if self._invalidated_context:
+            msg = "trying to use an invalidated dirstate before it has reset"
+            raise error.ProgrammingError(msg)
         self._parentwriters += 1
@@ -154,11 +164,21 @@
         self._parentwriters += 1
-        yield
-        # Typically we want the "undo" step of a context manager in a
-        # finally block so it happens even when an exception
-        # occurs. In this case, however, we only want to decrement
-        # parentwriters if the code in the with statement exits
-        # normally, so we don't have a try/finally here on purpose.
-        self._parentwriters -= 1
+        try:
+            yield
+        except Exception:
+            self.invalidate()
+            raise
+        finally:
+            if self._parentwriters > 0:
+                if self._invalidated_context:
+                    # make sure we invalidate anything an upper context might
+                    # have changed.
+                    self.invalidate()
+                self._parentwriters -= 1
+                # The invalidation is complete once we exit the final context
+                # manager
+                if self._parentwriters <= 0:
+                    assert self._parentwriters == 0
+                    self._invalidated_context = False
 
     def pendingparentchange(self):
         """Returns true if the dirstate is in the middle of a set of changes
@@ -419,7 +439,7 @@
                 delattr(self, a)
         self._dirty = False
         self._dirty_tracked_set = False
-        self._parentwriters = 0
+        self._invalidated_context = self._parentwriters > 0
         self._origpl = None
 
     def copy(self, source, dest):