shelve: make shelvestate use simplekeyvaluefile
authorKostia Balytskyi <ikostia@fb.com>
Sat, 13 May 2017 14:52:29 -0700
changeset 32285 fe3105e6e051
parent 32284 16d424b97125
child 32286 539cbe0f8fa3
shelve: make shelvestate use simplekeyvaluefile Currently shelvestate uses line ordering to differentiate fields. This makes it hard for extensions to wrap shelve, since if two alternative versions of code add a new line, correct merging is going to be problematic. simplekeyvaluefile was introduced fot this purpose specifically. After this patch: - shelve will always write a simplekeyvaluefile - unshelve will check the first line of the file for a version, and if the version is 1, will read it in a position-based way, if the version is 2, will read it in a key-value way As discussed with Yuya previously, this will be able to handle old-style shelvedstate files, but old Mercurial versions will fail on the attempt to read shelvedstate file of version 2 with a self-explanatory message: 'abort: this version of shelve is incompatible with the version used in this repo'
hgext/shelve.py
tests/test-shelve.t
--- a/hgext/shelve.py	Sun May 14 14:15:07 2017 -0700
+++ b/hgext/shelve.py	Sat May 13 14:52:29 2017 -0700
@@ -167,7 +167,7 @@
     Handles saving and restoring a shelved state. Ensures that different
     versions of a shelved state are possible and handles them appropriately.
     """
-    _version = 1
+    _version = 2
     _filename = 'shelvedstate'
     _keep = 'keep'
     _nokeep = 'nokeep'
@@ -175,26 +175,9 @@
     _noactivebook = ':no-active-bookmark'
 
     @classmethod
-    def load(cls, repo):
-        # Order is important, because old shelvestate file uses it
-        # to detemine values of fields (i.g. version is on the first line,
-        # name is on the second and so forth). Please do not change.
-        keys = ['version', 'name', 'originalwctx', 'pendingctx', 'parents',
-                'nodestoremove', 'branchtorestore', 'keep', 'activebook']
-        d = {}
-        fp = repo.vfs(cls._filename)
+    def _verifyandtransform(cls, d):
+        """Some basic shelvestate syntactic verification and transformation"""
         try:
-            for key in keys:
-                d[key] = fp.readline().strip()
-        finally:
-            fp.close()
-
-        # some basic syntactic verification and transformation
-        try:
-            d['version'] = int(d['version'])
-            if d['version'] != cls._version:
-                raise error.Abort(_('this version of shelve is incompatible '
-                                    'with the version used in this repo'))
             d['originalwctx'] = nodemod.bin(d['originalwctx'])
             d['pendingctx'] = nodemod.bin(d['pendingctx'])
             d['parents'] = [nodemod.bin(h)
@@ -204,6 +187,50 @@
         except (ValueError, TypeError, KeyError) as err:
             raise error.CorruptedState(str(err))
 
+    @classmethod
+    def _getversion(cls, repo):
+        """Read version information from shelvestate file"""
+        fp = repo.vfs(cls._filename)
+        try:
+            version = int(fp.readline().strip())
+        except ValueError as err:
+            raise error.CorruptedState(str(err))
+        finally:
+            fp.close()
+        return version
+
+    @classmethod
+    def _readold(cls, repo):
+        """Read the old position-based version of a shelvestate file"""
+        # Order is important, because old shelvestate file uses it
+        # to detemine values of fields (i.g. name is on the second line,
+        # originalwctx is on the third and so forth). Please do not change.
+        keys = ['version', 'name', 'originalwctx', 'pendingctx', 'parents',
+                'nodestoremove', 'branchtorestore', 'keep', 'activebook']
+        # this is executed only seldomly, so it is not a big deal
+        # that we open this file twice
+        fp = repo.vfs(cls._filename)
+        d = {}
+        try:
+            for key in keys:
+                d[key] = fp.readline().strip()
+        finally:
+            fp.close()
+        return d
+
+    @classmethod
+    def load(cls, repo):
+        version = cls._getversion(repo)
+        if version < cls._version:
+            d = cls._readold(repo)
+        elif version == cls._version:
+            d = scmutil.simplekeyvaluefile(repo.vfs, cls._filename)\
+                       .read(firstlinenonkeyval=True)
+        else:
+            raise error.Abort(_('this version of shelve is incompatible '
+                                'with the version used in this repo'))
+
+        cls._verifyandtransform(d)
         try:
             obj = cls()
             obj.name = d['name']
@@ -224,19 +251,20 @@
     @classmethod
     def save(cls, repo, name, originalwctx, pendingctx, nodestoremove,
              branchtorestore, keep=False, activebook=''):
-        fp = repo.vfs(cls._filename, 'wb')
-        fp.write('%i\n' % cls._version)
-        fp.write('%s\n' % name)
-        fp.write('%s\n' % nodemod.hex(originalwctx.node()))
-        fp.write('%s\n' % nodemod.hex(pendingctx.node()))
-        fp.write('%s\n' %
-                 ' '.join([nodemod.hex(p) for p in repo.dirstate.parents()]))
-        fp.write('%s\n' %
-                 ' '.join([nodemod.hex(n) for n in nodestoremove]))
-        fp.write('%s\n' % branchtorestore)
-        fp.write('%s\n' % (cls._keep if keep else cls._nokeep))
-        fp.write('%s\n' % (activebook or cls._noactivebook))
-        fp.close()
+        info = {
+            "name": name,
+            "originalwctx": nodemod.hex(originalwctx.node()),
+            "pendingctx": nodemod.hex(pendingctx.node()),
+            "parents": ' '.join([nodemod.hex(p)
+                                 for p in repo.dirstate.parents()]),
+            "nodestoremove": ' '.join([nodemod.hex(n)
+                                      for n in nodestoremove]),
+            "branchtorestore": branchtorestore,
+            "keep": cls._keep if keep else cls._nokeep,
+            "activebook": activebook or cls._noactivebook
+        }
+        scmutil.simplekeyvaluefile(repo.vfs, cls._filename)\
+               .write(info, firstline=str(cls._version))
 
     @classmethod
     def clear(cls, repo):
--- a/tests/test-shelve.t	Sun May 14 14:15:07 2017 -0700
+++ b/tests/test-shelve.t	Sat May 13 14:52:29 2017 -0700
@@ -1591,9 +1591,8 @@
 Removing restore branch information from shelvedstate file(making it looks like
 in previous versions) and running unshelve --continue
 
-  $ head -n 6 < .hg/shelvedstate > .hg/shelvedstate_oldformat
-  $ rm .hg/shelvedstate
-  $ mv .hg/shelvedstate_oldformat .hg/shelvedstate
+  $ cp .hg/shelvedstate .hg/shelvedstate_old
+  $ cat .hg/shelvedstate_old | grep -v 'branchtorestore' > .hg/shelvedstate
 
   $ echo "aaabbbccc" > a
   $ rm a.orig
@@ -1737,3 +1736,48 @@
   [255]
   $ hg st
   ! a
+  $ cd ..
+
+New versions of Mercurial know how to read onld shelvedstate files
+  $ hg init oldshelvedstate
+  $ cd oldshelvedstate
+  $ echo root > root && hg ci -Am root
+  adding root
+  $ echo 1 > a
+  $ hg add a
+  $ hg shelve --name ashelve
+  shelved as ashelve
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ echo 2 > a
+  $ hg ci -Am a
+  adding a
+  $ hg unshelve
+  unshelving change 'ashelve'
+  rebasing shelved changes
+  rebasing 2:003d2d94241c "changes to: root" (tip)
+  merging a
+  warning: conflicts while merging a! (edit, then use 'hg resolve --mark')
+  unresolved conflicts (see 'hg resolve', then 'hg unshelve --continue')
+  [1]
+putting v1 shelvedstate file in place of a created v2
+  $ cat << EOF > .hg/shelvedstate
+  > 1
+  > ashelve
+  > 8b058dae057a5a78f393f4535d9e363dd5efac9d
+  > 8b058dae057a5a78f393f4535d9e363dd5efac9d
+  > 8b058dae057a5a78f393f4535d9e363dd5efac9d 003d2d94241cc7aff0c3a148e966d6a4a377f3a7
+  > 003d2d94241cc7aff0c3a148e966d6a4a377f3a7
+  > 
+  > nokeep
+  > :no-active-bookmark
+  > EOF
+  $ echo 1 > a
+  $ hg resolve --mark a
+  (no more unresolved files)
+  continue: hg unshelve --continue
+mercurial does not crash
+  $ hg unshelve --continue
+  rebasing 2:003d2d94241c "changes to: root" (tip)
+  unshelve of 'ashelve' complete
+  $ cd ..
+