evolve: write our own custom evolvestate file
authorPierre-Yves David <pierre-yves.david@fb.com>
Thu, 04 Feb 2016 01:19:14 +0000
changeset 1597 7876ed4fceb7
parent 1596 6079dcbfb726
child 1598 2a08ef812b84
evolve: write our own custom evolvestate file Since for ever, we were using 'graftstate' to record the node currently being evolve and allow 'hg evolve --continue' we now move to our on 'evolvestate' file. This remove and issue with 'hg summary' listing interrupted evolve as graft. This also open the way for storing more data into that file and allow proper --abort and --continue of the whole evolve operation (and not just the last one). The whole thing is very hacky but at least there is some progress. Thanks goes to Shusen Liu for initiating this work.
README
hgext/evolve.py
--- a/README	Thu Feb 04 10:16:52 2016 +0000
+++ b/README	Thu Feb 04 01:19:14 2016 +0000
@@ -64,6 +64,8 @@
 - evolve: compatibility with Mercurial 3.7
 - evolve: support merge with a single obsolete parent.
 - evolve: prevent added file to be marked as unknown if evolve fails (issue4966)
+- evolve: stop relying on graftstate file for save evolve state
+          (for `hg evolve --continue`)
 
 5.2.2 --
 
--- a/hgext/evolve.py	Thu Feb 04 10:16:52 2016 +0000
+++ b/hgext/evolve.py	Thu Feb 04 01:19:14 2016 +0000
@@ -67,6 +67,7 @@
 import collections
 import socket
 import errno
+import struct
 sha1re = re.compile(r'\b[0-9a-f]{6,40}\b')
 
 import mercurial
@@ -116,6 +117,7 @@
 command = cmdutil.command(cmdtable)
 
 _pack = struct.pack
+_unpack = struct.unpack
 
 if gboptsmap is not None:
     memfilectx = context.memfilectx
@@ -1651,8 +1653,19 @@
             raise error.Abort('cannot specify both "--any" and "--continue"')
         if allopt:
             raise error.Abort('cannot specify both "--all" and "--continue"')
-        graftcmd = commands.table['graft'][0]
-        return graftcmd(ui, repo, old_obsolete=True, **{'continue': True})
+        state = _evolvestateread(repo)
+        if state is None:
+            raise error.Abort('no evolve to continue')
+        orig = repo[state['current']]
+        # XXX This is a terrible terrible hack, please get rid of it.
+        repo.opener.write('graftstate', orig.hex() + '\n')
+        try:
+            graftcmd = commands.table['graft'][0]
+            ret = graftcmd(ui, repo, old_obsolete=True, **{'continue': True})
+            _evolvestatedelete(repo)
+            return ret
+        finally:
+            util.unlinkpath(repo.join('graftstate'), ignoremissing=True)
     cmdutil.bailifchanged(repo)
 
 
@@ -1796,7 +1809,7 @@
         try:
             relocate(repo, orig, target, pctx, keepbranch)
         except MergeFailure:
-            repo.opener.write('graftstate', orig.hex() + '\n')
+            _evolvestatewrite(repo, {'current': orig.node()})
             repo.ui.write_err(_('evolve failed!\n'))
             repo.ui.write_err(
                 _('fix conflict and run "hg evolve --continue"'
@@ -3736,6 +3749,84 @@
     if oldbookmarks or destbookmarks:
         repo._bookmarks.recordchange(tr)
 
+evolvestateversion = 0
+
+@eh.uisetup
+def setupevolveunfinished(ui):
+    data = ('evolvestate', True, False, _('evolve in progress'),
+           _("use 'hg evolve --continue' or 'hg update' to abort"))
+    cmdutil.unfinishedstates.append(data)
+
+@eh.wrapfunction(hg, 'clean')
+def clean(orig, repo, *args, **kwargs):
+    ret = orig(repo, *args, **kwargs)
+    util.unlinkpath(repo.join('evolvestate'), ignoremissing=True)
+    return ret
+
+def _evolvestatewrite(repo, state):
+    # [version]
+    # [type][length][content]
+    #
+    # `version` is a 4 bytes integer (handled at higher level)
+    # `type` is a single character, `length` is a 4 byte integer, and
+    # `content` is an arbitrary byte sequence of length `length`.
+    f = repo.vfs('evolvestate', 'w')
+    try:
+        f.write(_pack('>I', evolvestateversion))
+        current = state['current']
+        key = 'C' # as in 'current'
+        format = '>sI%is' % len(current)
+        f.write(_pack(format, key, len(current), current))
+    finally:
+        f.close()
+
+def _evolvestateread(repo):
+    try:
+        f = repo.vfs('evolvestate')
+    except IOError, err:
+        if err.errno != errno.ENOENT:
+            raise
+        return None
+    try:
+        versionblob = f.read(4)
+        if len(versionblob) < 4:
+            repo.ui.debug('ignoring corrupted evolvestte (file contains %i bits)'
+                          % len(versionblob))
+            return None
+        version = _unpack('>I', versionblob)[0]
+        if version != evolvestateversion:
+            raise error.Abort(_('unknown evolvestate version %i')
+                                % version, hint=_('upgrade your evolve'))
+        records = []
+        data = f.read()
+        off = 0
+        end = len(data)
+        while off < end:
+            rtype = data[off]
+            off += 1
+            length = _unpack('>I', data[off:(off + 4)])[0]
+            off += 4
+            record = data[off:(off + length)]
+            off += length
+            if rtype == 't':
+                rtype, record = record[0], record[1:]
+            records.append((rtype, record))
+        state = {}
+        for rtype, rdata in records:
+            if rtype == 'C':
+                state['current'] = rdata
+            elif rtype.lower():
+                repo.ui.debug('ignore evolve state record type %s' % rtype)
+            else:
+                raise error.Abort(_('unknown evolvestate field type %r')
+                                  % rtype, hint=_('upgrade your evolve'))
+        return state
+    finally:
+        f.close()
+
+def _evolvestatedelete(repo):
+    util.unlinkpath(repo.join('evolvestate'), ignoremissing=True)
+
 def _evolvemerge(repo, orig, dest, pctx, keepbranch):
     """Used by the evolve function to merge dest on top of pctx.
     return the same tuple as merge.graft"""