comparison mercurial/dirstate.py @ 49958:96e526fe5fb0

dirstate: invalidate changes when parent-change fails When an error occurs during changing parents, we should invalidate all dirstate modifications and reload the dirstate. This is currently done by a `unlock` callback on the `wlock`. To fix this anomaly, we start dealing with the error directly in the context manager and its potential nesting. The "hard" part is to make sure that, when the parent-change context are nested, we and higher level nesting do not continue to use the invalidated dirstate. We introduce dedicated code to enforce that.
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Tue, 13 Dec 2022 11:39:44 +0100
parents d09a57ce6fc4
children c166b212bdee
comparison
equal deleted inserted replaced
49957:ff4df0954742 49958:96e526fe5fb0
69 def requires_parents_change(func): 69 def requires_parents_change(func):
70 def wrap(self, *args, **kwargs): 70 def wrap(self, *args, **kwargs):
71 if not self.pendingparentchange(): 71 if not self.pendingparentchange():
72 msg = 'calling `%s` outside of a parentchange context' 72 msg = 'calling `%s` outside of a parentchange context'
73 msg %= func.__name__ 73 msg %= func.__name__
74 raise error.ProgrammingError(msg)
75 if self._invalidated_context:
76 msg = 'calling `%s` after the dirstate was invalidated'
74 raise error.ProgrammingError(msg) 77 raise error.ProgrammingError(msg)
75 return func(self, *args, **kwargs) 78 return func(self, *args, **kwargs)
76 79
77 return wrap 80 return wrap
78 81
122 self._dirty = False 125 self._dirty = False
123 # True if the set of tracked file may be different 126 # True if the set of tracked file may be different
124 self._dirty_tracked_set = False 127 self._dirty_tracked_set = False
125 self._ui = ui 128 self._ui = ui
126 self._filecache = {} 129 self._filecache = {}
130 # nesting level of `parentchange` context
127 self._parentwriters = 0 131 self._parentwriters = 0
132 # True if the current dirstate changing operations have been
133 # invalidated (used to make sure all nested contexts have been exited)
134 self._invalidated_context = False
128 self._filename = b'dirstate' 135 self._filename = b'dirstate'
129 self._filename_th = b'dirstate-tracked-hint' 136 self._filename_th = b'dirstate-tracked-hint'
130 self._pendingfilename = b'%s.pending' % self._filename 137 self._pendingfilename = b'%s.pending' % self._filename
131 self._plchangecallbacks = {} 138 self._plchangecallbacks = {}
132 self._origpl = None 139 self._origpl = None
149 156
150 If an exception occurs in the scope of the context manager, 157 If an exception occurs in the scope of the context manager,
151 the incoherent dirstate won't be written when wlock is 158 the incoherent dirstate won't be written when wlock is
152 released. 159 released.
153 """ 160 """
161 if self._invalidated_context:
162 msg = "trying to use an invalidated dirstate before it has reset"
163 raise error.ProgrammingError(msg)
154 self._parentwriters += 1 164 self._parentwriters += 1
155 yield 165 try:
156 # Typically we want the "undo" step of a context manager in a 166 yield
157 # finally block so it happens even when an exception 167 except Exception:
158 # occurs. In this case, however, we only want to decrement 168 self.invalidate()
159 # parentwriters if the code in the with statement exits 169 raise
160 # normally, so we don't have a try/finally here on purpose. 170 finally:
161 self._parentwriters -= 1 171 if self._parentwriters > 0:
172 if self._invalidated_context:
173 # make sure we invalidate anything an upper context might
174 # have changed.
175 self.invalidate()
176 self._parentwriters -= 1
177 # The invalidation is complete once we exit the final context
178 # manager
179 if self._parentwriters <= 0:
180 assert self._parentwriters == 0
181 self._invalidated_context = False
162 182
163 def pendingparentchange(self): 183 def pendingparentchange(self):
164 """Returns true if the dirstate is in the middle of a set of changes 184 """Returns true if the dirstate is in the middle of a set of changes
165 that modify the dirstate parent. 185 that modify the dirstate parent.
166 """ 186 """
417 for a in ("_map", "_branch", "_ignore"): 437 for a in ("_map", "_branch", "_ignore"):
418 if a in self.__dict__: 438 if a in self.__dict__:
419 delattr(self, a) 439 delattr(self, a)
420 self._dirty = False 440 self._dirty = False
421 self._dirty_tracked_set = False 441 self._dirty_tracked_set = False
422 self._parentwriters = 0 442 self._invalidated_context = self._parentwriters > 0
423 self._origpl = None 443 self._origpl = None
424 444
425 def copy(self, source, dest): 445 def copy(self, source, dest):
426 """Mark dest as a copy of source. Unmark dest if source is None.""" 446 """Mark dest as a copy of source. Unmark dest if source is None."""
427 if source == dest: 447 if source == dest: