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'
--- 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 ..
+