[PATCH RFC] update: detect and print a warning when a previous update crashed

Greg Ward greg-hg at gerg.ca
Fri Nov 23 18:24:30 CST 2012


# HG changeset patch
# User Greg Ward <greg at gerg.ca>
# Date 1353716294 18000
# Node ID 1fdde1253bd76c429712a1720031a5f8300c5085
# Parent  4ae21a7568f353dcb781df28d2acf458b06afcad
update: detect and print a warning when a previous update crashed

Write a file .hg/updating when update starts; existence of this file
either means an update is running now, or a previous update terminated
abnormally. Use that knowledge in localrepository.status() to print a
warning that the working dir is inconsistent. This affects "hg
status", "hg diff", and no doubt many other commands.

In fact, it affects "hg update" too, which raises an interesting
wrinkle: if the user is correcting the inconsistency by running "hg
update --clean", we don't want to bother them with the "inconsistent
working dir" warning. So there's a new in-memory flag, repo.updating,
to suppress that.

This merely reports the problem; we don't yet have a good way to
recover from it. "hg update --clean" is fine if you had no uncommitted
changes before the interrupted update, but we can't guarantee that's
the case.

diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
--- a/mercurial/localrepo.py
+++ b/mercurial/localrepo.py
@@ -125,6 +125,7 @@
         self.auditor = scmutil.pathauditor(self.root, self._checknested)
         self.vfs = scmutil.vfs(self.path)
         self.opener = self.vfs
+        self.updating = False
         self.baseui = baseui
         self.ui = baseui.copy()
         # A list of callback to shape the phase if no data were found.
@@ -1141,6 +1142,42 @@
         self._wlockref = weakref.ref(l)
         return l
 
+    def soonupdate(self):
+        '''Record that this process will soon start updating the working
+        directory (under the same wlock that it holds already). This is
+        necessary if a previous update crashed; we don't want status()
+        calls from pre-update checks to issue bogus "inconsistent working
+        dir" warnings if the user is now running "hg update" to correct the
+        situation.'''
+        self.updating = True
+
+    def startupdate(self, oldnode, newnode):
+        '''Record that we have started updating this repo's working dir
+        from oldnode to newnode. If the update crashes, we'll leave behind
+        a file so that future processes can know something went wrong.
+        Returns a writeable file object that caller may write to and must
+        close. Caller must call doneupdate() when the update finishes
+        successfully.'''
+        # Don't use atomictempfile here: we want the file to appear in its
+        # real name immediately, because the user is going to trip over the
+        # power cord as soon as update starts writing files.
+        updatingfile = open(self.join('updating'), 'w')
+        updatingfile.write(hex(oldnode) + '\n' + hex(newnode) + '\n')
+        updatingfile.flush()
+        return updatingfile
+
+    def doneupdate(self, updatingfile):
+        '''Record the successful completion of an update.'''
+        os.remove(updatingfile.name)
+        self.updating = False
+
+    def updatecrashed(self):
+        '''Return true if the working directory is in an inconsistent state
+        because updating it in a previous process terminated abnormally --
+        e.g. power loss, kill -9, SIGINT (Ctrl-C), abort, unhandled
+        exception, ...'''
+        return not self.updating and os.path.exists(self.join('updating'))
+
     def _filecommit(self, fctx, manifest1, manifest2, linkrev, tr, changelist):
         """
         commit an individual file as part of a larger transaction
@@ -1563,6 +1600,10 @@
             match.bad = bad
 
         if working: # we need to scan the working dir
+            if self.updatecrashed():
+                self.ui.warn(
+                    _('working directory in inconsistent state: '
+                      'was update interrupted?\n'))
             subrepos = []
             if '.hgsub' in self.dirstate:
                 subrepos = ctx2.substate.keys()
diff --git a/mercurial/merge.py b/mercurial/merge.py
--- a/mercurial/merge.py
+++ b/mercurial/merge.py
@@ -554,6 +554,8 @@
 
     onode = node
     wlock = repo.wlock()
+    repo.soonupdate()
+    updatingfile = None
     try:
         wc = repo[None]
         if node is None:
@@ -629,6 +631,7 @@
         if not partial:
             repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
 
+        updatingfile = repo.startupdate(p1.node(), p2.node())
         stats = applyupdates(repo, action, wc, p2, pa, overwrite)
 
         if not partial:
@@ -636,8 +639,15 @@
             recordupdates(repo, action, branchmerge)
             if not branchmerge:
                 repo.dirstate.setbranch(p2.branch())
+
+        # this must *not* be in the finally block; we only want to remove
+        # the 'updating' file if the update completed successfully (ie. the
+        # working dir is back in a consistent state)
+        repo.doneupdate(updatingfile)
     finally:
         wlock.release()
+        if updatingfile:
+            updatingfile.close()
 
     if not partial:
         repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])


More information about the Mercurial-devel mailing list