D6553: shelve: move shelve extension to core

navaneeth.suresh (Navaneeth Suresh) phabricator at mercurial-scm.org
Thu Jun 20 18:03:04 UTC 2019


navaneeth.suresh created this revision.
Herald added subscribers: mercurial-devel, mjpieters.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  Until now,`shelve` was bootstrapped as an extension. This patch adds
  `shelve` on core.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D6553

AFFECTED FILES
  hgext/shelve.py
  mercurial/cmdutil.py
  mercurial/commands.py
  mercurial/configitems.py
  mercurial/shelve.py
  tests/test-shelve.t
  tests/test-shelve2.t

CHANGE DETAILS

diff --git a/tests/test-shelve2.t b/tests/test-shelve2.t
--- a/tests/test-shelve2.t
+++ b/tests/test-shelve2.t
@@ -3,7 +3,6 @@
   $ cat <<EOF >> $HGRCPATH
   > [extensions]
   > mq =
-  > shelve =
   > [defaults]
   > diff = --nodates --git
   > qnew = --date '0 0'
diff --git a/tests/test-shelve.t b/tests/test-shelve.t
--- a/tests/test-shelve.t
+++ b/tests/test-shelve.t
@@ -3,7 +3,6 @@
   $ cat <<EOF >> $HGRCPATH
   > [extensions]
   > mq =
-  > shelve =
   > [defaults]
   > diff = --nodates --git
   > qnew = --date '0 0'
@@ -65,8 +64,6 @@
       To delete specific shelved changes, use "--delete". To delete all shelved
       changes, use "--cleanup".
   
-  (use 'hg help -e shelve' to show help for the shelve extension)
-  
   options ([+] can be repeated):
   
    -A --addremove           mark new/missing files as added/removed before
diff --git a/mercurial/shelve.py b/mercurial/shelve.py
new file mode 100644
--- /dev/null
+++ b/mercurial/shelve.py
@@ -0,0 +1,971 @@
+# shelve.py - save/restore working directory state
+#
+# Copyright 2013 Facebook, Inc.
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+"""save and restore changes to the working directory
+
+The "hg shelve" command saves changes made to the working directory
+and reverts those changes, resetting the working directory to a clean
+state.
+
+Later on, the "hg unshelve" command restores the changes saved by "hg
+shelve". Changes can be restored even after updating to a different
+parent, in which case Mercurial's merge machinery will resolve any
+conflicts if necessary.
+
+You can have more than one shelved change outstanding at a time; each
+shelved change has a distinct name. For details, see the help for "hg
+shelve".
+"""
+from __future__ import absolute_import
+
+import collections
+import errno
+import itertools
+import stat
+
+from .i18n import _
+from . import (
+    bookmarks,
+    bundle2,
+    bundlerepo,
+    changegroup,
+    cmdutil,
+    discovery,
+    error,
+    exchange,
+    hg,
+    lock as lockmod,
+    mdiff,
+    merge,
+    node as nodemod,
+    patch,
+    phases,
+    pycompat,
+    registrar,
+    repair,
+    scmutil,
+    templatefilters,
+    util,
+    vfs as vfsmod,
+)
+
+from hgext import (
+    rebase,
+)
+from .utils import (
+    dateutil,
+    stringutil,
+)
+
+backupdir = 'shelve-backup'
+shelvedir = 'shelved'
+shelvefileextensions = ['hg', 'patch', 'shelve']
+# universal extension is present in all types of shelves
+patchextension = 'patch'
+
+# we never need the user, so we use a
+# generic user for all shelve operations
+shelveuser = 'shelve at localhost'
+
+class shelvedfile(object):
+    """Helper for the file storing a single shelve
+
+    Handles common functions on shelve files (.hg/.patch) using
+    the vfs layer"""
+    def __init__(self, repo, name, filetype=None):
+        self.repo = repo
+        self.name = name
+        self.vfs = vfsmod.vfs(repo.vfs.join(shelvedir))
+        self.backupvfs = vfsmod.vfs(repo.vfs.join(backupdir))
+        self.ui = self.repo.ui
+        if filetype:
+            self.fname = name + '.' + filetype
+        else:
+            self.fname = name
+
+    def exists(self):
+        return self.vfs.exists(self.fname)
+
+    def filename(self):
+        return self.vfs.join(self.fname)
+
+    def backupfilename(self):
+        def gennames(base):
+            yield base
+            base, ext = base.rsplit('.', 1)
+            for i in itertools.count(1):
+                yield '%s-%d.%s' % (base, i, ext)
+
+        name = self.backupvfs.join(self.fname)
+        for n in gennames(name):
+            if not self.backupvfs.exists(n):
+                return n
+
+    def movetobackup(self):
+        if not self.backupvfs.isdir():
+            self.backupvfs.makedir()
+        util.rename(self.filename(), self.backupfilename())
+
+    def stat(self):
+        return self.vfs.stat(self.fname)
+
+    def opener(self, mode='rb'):
+        try:
+            return self.vfs(self.fname, mode)
+        except IOError as err:
+            if err.errno != errno.ENOENT:
+                raise
+            raise error.Abort(_("shelved change '%s' not found") % self.name)
+
+    def applybundle(self, tr):
+        fp = self.opener()
+        try:
+            targetphase = phases.internal
+            if not phases.supportinternal(self.repo):
+                targetphase = phases.secret
+            gen = exchange.readbundle(self.repo.ui, fp, self.fname, self.vfs)
+            pretip = self.repo['tip']
+            bundle2.applybundle(self.repo, gen, tr,
+                                source='unshelve',
+                                url='bundle:' + self.vfs.join(self.fname),
+                                targetphase=targetphase)
+            shelvectx = self.repo['tip']
+            if pretip == shelvectx:
+                shelverev = tr.changes['revduplicates'][-1]
+                shelvectx = self.repo[shelverev]
+            return shelvectx
+        finally:
+            fp.close()
+
+    def bundlerepo(self):
+        path = self.vfs.join(self.fname)
+        return bundlerepo.instance(self.repo.baseui,
+                                   'bundle://%s+%s' % (self.repo.root, path))
+
+    def writebundle(self, bases, node):
+        cgversion = changegroup.safeversion(self.repo)
+        if cgversion == '01':
+            btype = 'HG10BZ'
+            compression = None
+        else:
+            btype = 'HG20'
+            compression = 'BZ'
+
+        repo = self.repo.unfiltered()
+
+        outgoing = discovery.outgoing(repo, missingroots=bases,
+                                      missingheads=[node])
+        cg = changegroup.makechangegroup(repo, outgoing, cgversion, 'shelve')
+
+        bundle2.writebundle(self.ui, cg, self.fname, btype, self.vfs,
+                                compression=compression)
+
+    def writeinfo(self, info):
+        scmutil.simplekeyvaluefile(self.vfs, self.fname).write(info)
+
+    def readinfo(self):
+        return scmutil.simplekeyvaluefile(self.vfs, self.fname).read()
+
+class shelvedstate(object):
+    """Handle persistence during unshelving operations.
+
+    Handles saving and restoring a shelved state. Ensures that different
+    versions of a shelved state are possible and handles them appropriately.
+    """
+    _version = 2
+    _filename = 'shelvedstate'
+    _keep = 'keep'
+    _nokeep = 'nokeep'
+    # colon is essential to differentiate from a real bookmark name
+    _noactivebook = ':no-active-bookmark'
+
+    @classmethod
+    def _verifyandtransform(cls, d):
+        """Some basic shelvestate syntactic verification and transformation"""
+        try:
+            d['originalwctx'] = nodemod.bin(d['originalwctx'])
+            d['pendingctx'] = nodemod.bin(d['pendingctx'])
+            d['parents'] = [nodemod.bin(h)
+                            for h in d['parents'].split(' ')]
+            d['nodestoremove'] = [nodemod.bin(h)
+                                  for h in d['nodestoremove'].split(' ')]
+        except (ValueError, TypeError, KeyError) as err:
+            raise error.CorruptedState(pycompat.bytestr(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(pycompat.bytestr(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']
+            obj.wctx = repo[d['originalwctx']]
+            obj.pendingctx = repo[d['pendingctx']]
+            obj.parents = d['parents']
+            obj.nodestoremove = d['nodestoremove']
+            obj.branchtorestore = d.get('branchtorestore', '')
+            obj.keep = d.get('keep') == cls._keep
+            obj.activebookmark = ''
+            if d.get('activebook', '') != cls._noactivebook:
+                obj.activebookmark = d.get('activebook', '')
+        except (error.RepoLookupError, KeyError) as err:
+            raise error.CorruptedState(pycompat.bytestr(err))
+
+        return obj
+
+    @classmethod
+    def save(cls, repo, name, originalwctx, pendingctx, nodestoremove,
+             branchtorestore, keep=False, activebook=''):
+        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=("%d" % cls._version))
+
+    @classmethod
+    def clear(cls, repo):
+        repo.vfs.unlinkpath(cls._filename, ignoremissing=True)
+
+def cleanupoldbackups(repo):
+    vfs = vfsmod.vfs(repo.vfs.join(backupdir))
+    maxbackups = repo.ui.configint('shelve', 'maxbackups')
+    hgfiles = [f for f in vfs.listdir()
+               if f.endswith('.' + patchextension)]
+    hgfiles = sorted([(vfs.stat(f)[stat.ST_MTIME], f) for f in hgfiles])
+    if maxbackups > 0 and maxbackups < len(hgfiles):
+        bordermtime = hgfiles[-maxbackups][0]
+    else:
+        bordermtime = None
+    for mtime, f in hgfiles[:len(hgfiles) - maxbackups]:
+        if mtime == bordermtime:
+            # keep it, because timestamp can't decide exact order of backups
+            continue
+        base = f[:-(1 + len(patchextension))]
+        for ext in shelvefileextensions:
+            vfs.tryunlink(base + '.' + ext)
+
+def _backupactivebookmark(repo):
+    activebookmark = repo._activebookmark
+    if activebookmark:
+        bookmarks.deactivate(repo)
+    return activebookmark
+
+def _restoreactivebookmark(repo, mark):
+    if mark:
+        bookmarks.activate(repo, mark)
+
+def _aborttransaction(repo, tr):
+    '''Abort current transaction for shelve/unshelve, but keep dirstate
+    '''
+    dirstatebackupname = 'dirstate.shelve'
+    repo.dirstate.savebackup(tr, dirstatebackupname)
+    tr.abort()
+    repo.dirstate.restorebackup(None, dirstatebackupname)
+
+def getshelvename(repo, parent, opts):
+    """Decide on the name this shelve is going to have"""
+    def gennames():
+        yield label
+        for i in itertools.count(1):
+            yield '%s-%02d' % (label, i)
+    name = opts.get('name')
+    label = repo._activebookmark or parent.branch() or 'default'
+    # slashes aren't allowed in filenames, therefore we rename it
+    label = label.replace('/', '_')
+    label = label.replace('\\', '_')
+    # filenames must not start with '.' as it should not be hidden
+    if label.startswith('.'):
+        label = label.replace('.', '_', 1)
+
+    if name:
+        if shelvedfile(repo, name, patchextension).exists():
+            e = _("a shelved change named '%s' already exists") % name
+            raise error.Abort(e)
+
+        # ensure we are not creating a subdirectory or a hidden file
+        if '/' in name or '\\' in name:
+            raise error.Abort(_('shelved change names can not contain slashes'))
+        if name.startswith('.'):
+            raise error.Abort(_("shelved change names can not start with '.'"))
+
+    else:
+        for n in gennames():
+            if not shelvedfile(repo, n, patchextension).exists():
+                name = n
+                break
+
+    return name
+
+def mutableancestors(ctx):
+    """return all mutable ancestors for ctx (included)
+
+    Much faster than the revset ancestors(ctx) & draft()"""
+    seen = {nodemod.nullrev}
+    visit = collections.deque()
+    visit.append(ctx)
+    while visit:
+        ctx = visit.popleft()
+        yield ctx.node()
+        for parent in ctx.parents():
+            rev = parent.rev()
+            if rev not in seen:
+                seen.add(rev)
+                if parent.mutable():
+                    visit.append(parent)
+
+def getcommitfunc(extra, interactive, editor=False):
+    def commitfunc(ui, repo, message, match, opts):
+        hasmq = util.safehasattr(repo, 'mq')
+        if hasmq:
+            saved, repo.mq.checkapplied = repo.mq.checkapplied, False
+
+        targetphase = phases.internal
+        if not phases.supportinternal(repo):
+            targetphase = phases.secret
+        overrides = {('phases', 'new-commit'): targetphase}
+        try:
+            editor_ = False
+            if editor:
+                editor_ = cmdutil.getcommiteditor(editform='shelve.shelve',
+                                                  **pycompat.strkwargs(opts))
+            with repo.ui.configoverride(overrides):
+                return repo.commit(message, shelveuser, opts.get('date'),
+                                   match, editor=editor_, extra=extra)
+        finally:
+            if hasmq:
+                repo.mq.checkapplied = saved
+
+    def interactivecommitfunc(ui, repo, *pats, **opts):
+        opts = pycompat.byteskwargs(opts)
+        match = scmutil.match(repo['.'], pats, {})
+        message = opts['message']
+        return commitfunc(ui, repo, message, match, opts)
+
+    return interactivecommitfunc if interactive else commitfunc
+
+def _nothingtoshelvemessaging(ui, repo, pats, opts):
+    stat = repo.status(match=scmutil.match(repo[None], pats, opts))
+    if stat.deleted:
+        ui.status(_("nothing changed (%d missing files, see "
+                    "'hg status')\n") % len(stat.deleted))
+    else:
+        ui.status(_("nothing changed\n"))
+
+def _shelvecreatedcommit(repo, node, name, match):
+    info = {'node': nodemod.hex(node)}
+    shelvedfile(repo, name, 'shelve').writeinfo(info)
+    bases = list(mutableancestors(repo[node]))
+    shelvedfile(repo, name, 'hg').writebundle(bases, node)
+    with shelvedfile(repo, name, patchextension).opener('wb') as fp:
+        cmdutil.exportfile(repo, [node], fp, opts=mdiff.diffopts(git=True),
+                           match=match)
+
+def _includeunknownfiles(repo, pats, opts, extra):
+    s = repo.status(match=scmutil.match(repo[None], pats, opts),
+                    unknown=True)
+    if s.unknown:
+        extra['shelve_unknown'] = '\0'.join(s.unknown)
+        repo[None].add(s.unknown)
+
+def _finishshelve(repo, tr):
+    if phases.supportinternal(repo):
+        tr.close()
+    else:
+        _aborttransaction(repo, tr)
+
+def createcmd(ui, repo, pats, opts):
+    """subcommand that creates a new shelve"""
+    with repo.wlock():
+        cmdutil.checkunfinished(repo)
+        return _docreatecmd(ui, repo, pats, opts)
+
+def _docreatecmd(ui, repo, pats, opts):
+    wctx = repo[None]
+    parents = wctx.parents()
+    if len(parents) > 1:
+        raise error.Abort(_('cannot shelve while merging'))
+    parent = parents[0]
+    origbranch = wctx.branch()
+
+    if parent.node() != nodemod.nullid:
+        desc = "changes to: %s" % parent.description().split('\n', 1)[0]
+    else:
+        desc = '(changes in empty repository)'
+
+    if not opts.get('message'):
+        opts['message'] = desc
+
+    lock = tr = activebookmark = None
+    try:
+        lock = repo.lock()
+
+        # use an uncommitted transaction to generate the bundle to avoid
+        # pull races. ensure we don't print the abort message to stderr.
+        tr = repo.transaction('shelve', report=lambda x: None)
+
+        interactive = opts.get('interactive', False)
+        includeunknown = (opts.get('unknown', False) and
+                          not opts.get('addremove', False))
+
+        name = getshelvename(repo, parent, opts)
+        activebookmark = _backupactivebookmark(repo)
+        extra = {'internal': 'shelve'}
+        if includeunknown:
+            _includeunknownfiles(repo, pats, opts, extra)
+
+        if _iswctxonnewbranch(repo) and not _isbareshelve(pats, opts):
+            # In non-bare shelve we don't store newly created branch
+            # at bundled commit
+            repo.dirstate.setbranch(repo['.'].branch())
+
+        commitfunc = getcommitfunc(extra, interactive, editor=True)
+        if not interactive:
+            node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
+        else:
+            node = cmdutil.dorecord(ui, repo, commitfunc, None,
+                                    False, cmdutil.recordfilter, *pats,
+                                    **pycompat.strkwargs(opts))
+        if not node:
+            _nothingtoshelvemessaging(ui, repo, pats, opts)
+            return 1
+
+        # Create a matcher so that prefetch doesn't attempt to fetch
+        # the entire repository pointlessly, and as an optimisation
+        # for movedirstate, if needed.
+        match = scmutil.matchfiles(repo, repo[node].files())
+        _shelvecreatedcommit(repo, node, name, match)
+
+        if ui.formatted():
+            desc = stringutil.ellipsis(desc, ui.termwidth())
+        ui.status(_('shelved as %s\n') % name)
+        if opts['keep']:
+            with repo.dirstate.parentchange():
+                scmutil.movedirstate(repo, parent, match)
+        else:
+            hg.update(repo, parent.node())
+        if origbranch != repo['.'].branch() and not _isbareshelve(pats, opts):
+            repo.dirstate.setbranch(origbranch)
+
+        _finishshelve(repo, tr)
+    finally:
+        _restoreactivebookmark(repo, activebookmark)
+        lockmod.release(tr, lock)
+
+def _isbareshelve(pats, opts):
+    return (not pats
+            and not opts.get('interactive', False)
+            and not opts.get('include', False)
+            and not opts.get('exclude', False))
+
+def _iswctxonnewbranch(repo):
+    return repo[None].branch() != repo['.'].branch()
+
+def cleanupcmd(ui, repo):
+    """subcommand that deletes all shelves"""
+
+    with repo.wlock():
+        for (name, _type) in repo.vfs.readdir(shelvedir):
+            suffix = name.rsplit('.', 1)[-1]
+            if suffix in shelvefileextensions:
+                shelvedfile(repo, name).movetobackup()
+            cleanupoldbackups(repo)
+
+def deletecmd(ui, repo, pats):
+    """subcommand that deletes a specific shelve"""
+    if not pats:
+        raise error.Abort(_('no shelved changes specified!'))
+    with repo.wlock():
+        try:
+            for name in pats:
+                for suffix in shelvefileextensions:
+                    shfile = shelvedfile(repo, name, suffix)
+                    # patch file is necessary, as it should
+                    # be present for any kind of shelve,
+                    # but the .hg file is optional as in future we
+                    # will add obsolete shelve with does not create a
+                    # bundle
+                    if shfile.exists() or suffix == patchextension:
+                        shfile.movetobackup()
+            cleanupoldbackups(repo)
+        except OSError as err:
+            if err.errno != errno.ENOENT:
+                raise
+            raise error.Abort(_("shelved change '%s' not found") % name)
+
+def listshelves(repo):
+    """return all shelves in repo as list of (time, filename)"""
+    try:
+        names = repo.vfs.readdir(shelvedir)
+    except OSError as err:
+        if err.errno != errno.ENOENT:
+            raise
+        return []
+    info = []
+    for (name, _type) in names:
+        pfx, sfx = name.rsplit('.', 1)
+        if not pfx or sfx != patchextension:
+            continue
+        st = shelvedfile(repo, name).stat()
+        info.append((st[stat.ST_MTIME], shelvedfile(repo, pfx).filename()))
+    return sorted(info, reverse=True)
+
+def listcmd(ui, repo, pats, opts):
+    """subcommand that displays the list of shelves"""
+    pats = set(pats)
+    width = 80
+    if not ui.plain():
+        width = ui.termwidth()
+    namelabel = 'shelve.newest'
+    ui.pager('shelve')
+    for mtime, name in listshelves(repo):
+        sname = util.split(name)[1]
+        if pats and sname not in pats:
+            continue
+        ui.write(sname, label=namelabel)
+        namelabel = 'shelve.name'
+        if ui.quiet:
+            ui.write('\n')
+            continue
+        ui.write(' ' * (16 - len(sname)))
+        used = 16
+        date = dateutil.makedate(mtime)
+        age = '(%s)' % templatefilters.age(date, abbrev=True)
+        ui.write(age, label='shelve.age')
+        ui.write(' ' * (12 - len(age)))
+        used += 12
+        with open(name + '.' + patchextension, 'rb') as fp:
+            while True:
+                line = fp.readline()
+                if not line:
+                    break
+                if not line.startswith('#'):
+                    desc = line.rstrip()
+                    if ui.formatted():
+                        desc = stringutil.ellipsis(desc, width - used)
+                    ui.write(desc)
+                    break
+            ui.write('\n')
+            if not (opts['patch'] or opts['stat']):
+                continue
+            difflines = fp.readlines()
+            if opts['patch']:
+                for chunk, label in patch.difflabel(iter, difflines):
+                    ui.write(chunk, label=label)
+            if opts['stat']:
+                for chunk, label in patch.diffstatui(difflines, width=width):
+                    ui.write(chunk, label=label)
+
+def patchcmds(ui, repo, pats, opts):
+    """subcommand that displays shelves"""
+    if len(pats) == 0:
+        shelves = listshelves(repo)
+        if not shelves:
+            raise error.Abort(_("there are no shelves to show"))
+        mtime, name = shelves[0]
+        sname = util.split(name)[1]
+        pats = [sname]
+
+    for shelfname in pats:
+        if not shelvedfile(repo, shelfname, patchextension).exists():
+            raise error.Abort(_("cannot find shelf %s") % shelfname)
+
+    listcmd(ui, repo, pats, opts)
+
+def checkparents(repo, state):
+    """check parent while resuming an unshelve"""
+    if state.parents != repo.dirstate.parents():
+        raise error.Abort(_('working directory parents do not match unshelve '
+                           'state'))
+
+def unshelveabort(ui, repo, state, opts):
+    """subcommand that abort an in-progress unshelve"""
+    with repo.lock():
+        try:
+            checkparents(repo, state)
+
+            merge.update(repo, state.pendingctx, branchmerge=False, force=True)
+            if (state.activebookmark
+                    and state.activebookmark in repo._bookmarks):
+                bookmarks.activate(repo, state.activebookmark)
+
+            if repo.vfs.exists('unshelverebasestate'):
+                repo.vfs.rename('unshelverebasestate', 'rebasestate')
+                rebase.clearstatus(repo)
+
+            mergefiles(ui, repo, state.wctx, state.pendingctx)
+            if not phases.supportinternal(repo):
+                repair.strip(ui, repo, state.nodestoremove, backup=False,
+                             topic='shelve')
+        finally:
+            shelvedstate.clear(repo)
+            ui.warn(_("unshelve of '%s' aborted\n") % state.name)
+
+def mergefiles(ui, repo, wctx, shelvectx):
+    """updates to wctx and merges the changes from shelvectx into the
+    dirstate."""
+    with ui.configoverride({('ui', 'quiet'): True}):
+        hg.update(repo, wctx.node())
+        ui.pushbuffer(True)
+        cmdutil.revert(ui, repo, shelvectx, repo.dirstate.parents())
+        ui.popbuffer()
+
+def restorebranch(ui, repo, branchtorestore):
+    if branchtorestore and branchtorestore != repo.dirstate.branch():
+        repo.dirstate.setbranch(branchtorestore)
+        ui.status(_('marked working directory as branch %s\n')
+                  % branchtorestore)
+
+def unshelvecleanup(ui, repo, name, opts):
+    """remove related files after an unshelve"""
+    if not opts.get('keep'):
+        for filetype in shelvefileextensions:
+            shfile = shelvedfile(repo, name, filetype)
+            if shfile.exists():
+                shfile.movetobackup()
+        cleanupoldbackups(repo)
+
+def unshelvecontinue(ui, repo, state, opts):
+    """subcommand to continue an in-progress unshelve"""
+    # We're finishing off a merge. First parent is our original
+    # parent, second is the temporary "fake" commit we're unshelving.
+    with repo.lock():
+        checkparents(repo, state)
+        ms = merge.mergestate.read(repo)
+        if list(ms.unresolved()):
+            raise error.Abort(
+                _("unresolved conflicts, can't continue"),
+                hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
+
+        shelvectx = repo[state.parents[1]]
+        pendingctx = state.pendingctx
+
+        with repo.dirstate.parentchange():
+            repo.setparents(state.pendingctx.node(), nodemod.nullid)
+            repo.dirstate.write(repo.currenttransaction())
+
+        targetphase = phases.internal
+        if not phases.supportinternal(repo):
+            targetphase = phases.secret
+        overrides = {('phases', 'new-commit'): targetphase}
+        with repo.ui.configoverride(overrides, 'unshelve'):
+            with repo.dirstate.parentchange():
+                repo.setparents(state.parents[0], nodemod.nullid)
+                newnode = repo.commit(text=shelvectx.description(),
+                                      extra=shelvectx.extra(),
+                                      user=shelvectx.user(),
+                                      date=shelvectx.date())
+
+        if newnode is None:
+            # If it ended up being a no-op commit, then the normal
+            # merge state clean-up path doesn't happen, so do it
+            # here. Fix issue5494
+            merge.mergestate.clean(repo)
+            shelvectx = state.pendingctx
+            msg = _('note: unshelved changes already existed '
+                    'in the working copy\n')
+            ui.status(msg)
+        else:
+            # only strip the shelvectx if we produced one
+            state.nodestoremove.append(newnode)
+            shelvectx = repo[newnode]
+
+        hg.updaterepo(repo, pendingctx.node(), overwrite=False)
+
+        if repo.vfs.exists('unshelverebasestate'):
+            repo.vfs.rename('unshelverebasestate', 'rebasestate')
+            rebase.clearstatus(repo)
+
+        mergefiles(ui, repo, state.wctx, shelvectx)
+        restorebranch(ui, repo, state.branchtorestore)
+
+        if not phases.supportinternal(repo):
+            repair.strip(ui, repo, state.nodestoremove, backup=False,
+                         topic='shelve')
+        _restoreactivebookmark(repo, state.activebookmark)
+        shelvedstate.clear(repo)
+        unshelvecleanup(ui, repo, state.name, opts)
+        ui.status(_("unshelve of '%s' complete\n") % state.name)
+
+def _commitworkingcopychanges(ui, repo, opts, tmpwctx):
+    """Temporarily commit working copy changes before moving unshelve commit"""
+    # Store pending changes in a commit and remember added in case a shelve
+    # contains unknown files that are part of the pending change
+    s = repo.status()
+    addedbefore = frozenset(s.added)
+    if not (s.modified or s.added or s.removed):
+        return tmpwctx, addedbefore
+    ui.status(_("temporarily committing pending changes "
+                "(restore with 'hg unshelve --abort')\n"))
+    extra = {'internal': 'shelve'}
+    commitfunc = getcommitfunc(extra=extra, interactive=False,
+                               editor=False)
+    tempopts = {}
+    tempopts['message'] = "pending changes temporary commit"
+    tempopts['date'] = opts.get('date')
+    with ui.configoverride({('ui', 'quiet'): True}):
+        node = cmdutil.commit(ui, repo, commitfunc, [], tempopts)
+    tmpwctx = repo[node]
+    return tmpwctx, addedbefore
+
+def _unshelverestorecommit(ui, repo, tr, basename):
+    """Recreate commit in the repository during the unshelve"""
+    repo = repo.unfiltered()
+    node = None
+    if shelvedfile(repo, basename, 'shelve').exists():
+        node = shelvedfile(repo, basename, 'shelve').readinfo()['node']
+    if node is None or node not in repo:
+        with ui.configoverride({('ui', 'quiet'): True}):
+            shelvectx = shelvedfile(repo, basename, 'hg').applybundle(tr)
+        # We might not strip the unbundled changeset, so we should keep track of
+        # the unshelve node in case we need to reuse it (eg: unshelve --keep)
+        if node is None:
+            info = {'node': nodemod.hex(shelvectx.node())}
+            shelvedfile(repo, basename, 'shelve').writeinfo(info)
+    else:
+        shelvectx = repo[node]
+
+    return repo, shelvectx
+
+def _rebaserestoredcommit(ui, repo, opts, tr, oldtiprev, basename, pctx,
+                          tmpwctx, shelvectx, branchtorestore,
+                          activebookmark):
+    """Rebase restored commit from its original location to a destination"""
+    # If the shelve is not immediately on top of the commit
+    # we'll be merging with, rebase it to be on top.
+    if tmpwctx.node() == shelvectx.p1().node():
+        return shelvectx
+
+    overrides = {
+        ('ui', 'forcemerge'): opts.get('tool', ''),
+        ('phases', 'new-commit'): phases.secret,
+    }
+    with repo.ui.configoverride(overrides, 'unshelve'):
+        ui.status(_('rebasing shelved changes\n'))
+        stats = merge.graft(repo, shelvectx, shelvectx.p1(),
+                           labels=['shelve', 'working-copy'],
+                           keepconflictparent=True)
+        if stats.unresolvedcount:
+            tr.close()
+
+            nodestoremove = [repo.changelog.node(rev)
+                             for rev in pycompat.xrange(oldtiprev, len(repo))]
+            shelvedstate.save(repo, basename, pctx, tmpwctx, nodestoremove,
+                              branchtorestore, opts.get('keep'), activebookmark)
+            raise error.InterventionRequired(
+                _("unresolved conflicts (see 'hg resolve', then "
+                  "'hg unshelve --continue')"))
+
+        with repo.dirstate.parentchange():
+            repo.setparents(tmpwctx.node(), nodemod.nullid)
+            newnode = repo.commit(text=shelvectx.description(),
+                                  extra=shelvectx.extra(),
+                                  user=shelvectx.user(),
+                                  date=shelvectx.date())
+
+        if newnode is None:
+            # If it ended up being a no-op commit, then the normal
+            # merge state clean-up path doesn't happen, so do it
+            # here. Fix issue5494
+            merge.mergestate.clean(repo)
+            shelvectx = tmpwctx
+            msg = _('note: unshelved changes already existed '
+                    'in the working copy\n')
+            ui.status(msg)
+        else:
+            shelvectx = repo[newnode]
+            hg.updaterepo(repo, tmpwctx.node(), False)
+
+    return shelvectx
+
+def _forgetunknownfiles(repo, shelvectx, addedbefore):
+    # Forget any files that were unknown before the shelve, unknown before
+    # unshelve started, but are now added.
+    shelveunknown = shelvectx.extra().get('shelve_unknown')
+    if not shelveunknown:
+        return
+    shelveunknown = frozenset(shelveunknown.split('\0'))
+    addedafter = frozenset(repo.status().added)
+    toforget = (addedafter & shelveunknown) - addedbefore
+    repo[None].forget(toforget)
+
+def _finishunshelve(repo, oldtiprev, tr, activebookmark):
+    _restoreactivebookmark(repo, activebookmark)
+    # The transaction aborting will strip all the commits for us,
+    # but it doesn't update the inmemory structures, so addchangegroup
+    # hooks still fire and try to operate on the missing commits.
+    # Clean up manually to prevent this.
+    repo.unfiltered().changelog.strip(oldtiprev, tr)
+    _aborttransaction(repo, tr)
+
+def _checkunshelveuntrackedproblems(ui, repo, shelvectx):
+    """Check potential problems which may result from working
+    copy having untracked changes."""
+    wcdeleted = set(repo.status().deleted)
+    shelvetouched = set(shelvectx.files())
+    intersection = wcdeleted.intersection(shelvetouched)
+    if intersection:
+        m = _("shelved change touches missing files")
+        hint = _("run hg status to see which files are missing")
+        raise error.Abort(m, hint=hint)
+
+def _dounshelve(ui, repo, *shelved, **opts):
+    opts = pycompat.byteskwargs(opts)
+    abortf = opts.get('abort')
+    continuef = opts.get('continue')
+    if not abortf and not continuef:
+        cmdutil.checkunfinished(repo)
+    shelved = list(shelved)
+    if opts.get("name"):
+        shelved.append(opts["name"])
+
+    if abortf or continuef:
+        if abortf and continuef:
+            raise error.Abort(_('cannot use both abort and continue'))
+        if shelved:
+            raise error.Abort(_('cannot combine abort/continue with '
+                               'naming a shelved change'))
+        if abortf and opts.get('tool', False):
+            ui.warn(_('tool option will be ignored\n'))
+
+        try:
+            state = shelvedstate.load(repo)
+            if opts.get('keep') is None:
+                opts['keep'] = state.keep
+        except IOError as err:
+            if err.errno != errno.ENOENT:
+                raise
+            cmdutil.wrongtooltocontinue(repo, _('unshelve'))
+        except error.CorruptedState as err:
+            ui.debug(pycompat.bytestr(err) + '\n')
+            if continuef:
+                msg = _('corrupted shelved state file')
+                hint = _('please run hg unshelve --abort to abort unshelve '
+                         'operation')
+                raise error.Abort(msg, hint=hint)
+            elif abortf:
+                msg = _('could not read shelved state file, your working copy '
+                        'may be in an unexpected state\nplease update to some '
+                        'commit\n')
+                ui.warn(msg)
+                shelvedstate.clear(repo)
+            return
+
+        if abortf:
+            return unshelveabort(ui, repo, state, opts)
+        elif continuef:
+            return unshelvecontinue(ui, repo, state, opts)
+    elif len(shelved) > 1:
+        raise error.Abort(_('can only unshelve one change at a time'))
+
+    # abort unshelve while merging (issue5123)
+    parents = repo[None].parents()
+    if len(parents) > 1:
+        raise error.Abort(_('cannot unshelve while merging'))
+
+    elif not shelved:
+        shelved = listshelves(repo)
+        if not shelved:
+            raise error.Abort(_('no shelved changes to apply!'))
+        basename = util.split(shelved[0][1])[1]
+        ui.status(_("unshelving change '%s'\n") % basename)
+    else:
+        basename = shelved[0]
+
+    if not shelvedfile(repo, basename, patchextension).exists():
+        raise error.Abort(_("shelved change '%s' not found") % basename)
+
+    repo = repo.unfiltered()
+    lock = tr = None
+    try:
+        lock = repo.lock()
+        tr = repo.transaction('unshelve', report=lambda x: None)
+        oldtiprev = len(repo)
+
+        pctx = repo['.']
+        tmpwctx = pctx
+        # The goal is to have a commit structure like so:
+        # ...-> pctx -> tmpwctx -> shelvectx
+        # where tmpwctx is an optional commit with the user's pending changes
+        # and shelvectx is the unshelved changes. Then we merge it all down
+        # to the original pctx.
+
+        activebookmark = _backupactivebookmark(repo)
+        tmpwctx, addedbefore = _commitworkingcopychanges(ui, repo, opts,
+                                                         tmpwctx)
+        repo, shelvectx = _unshelverestorecommit(ui, repo, tr, basename)
+        _checkunshelveuntrackedproblems(ui, repo, shelvectx)
+        branchtorestore = ''
+        if shelvectx.branch() != shelvectx.p1().branch():
+            branchtorestore = shelvectx.branch()
+
+        shelvectx = _rebaserestoredcommit(ui, repo, opts, tr, oldtiprev,
+                                          basename, pctx, tmpwctx,
+                                          shelvectx, branchtorestore,
+                                          activebookmark)
+        overrides = {('ui', 'forcemerge'): opts.get('tool', '')}
+        with ui.configoverride(overrides, 'unshelve'):
+            mergefiles(ui, repo, pctx, shelvectx)
+        restorebranch(ui, repo, branchtorestore)
+        _forgetunknownfiles(repo, shelvectx, addedbefore)
+
+        shelvedstate.clear(repo)
+        _finishunshelve(repo, oldtiprev, tr, activebookmark)
+        unshelvecleanup(ui, repo, basename, opts)
+    finally:
+        if tr:
+            tr.release()
+        lockmod.release(lock)
diff --git a/mercurial/configitems.py b/mercurial/configitems.py
--- a/mercurial/configitems.py
+++ b/mercurial/configitems.py
@@ -1082,6 +1082,9 @@
 coreconfigitem('share', 'poolnaming',
     default='identity',
 )
+coreconfigitem('shelve','maxbackups',
+    default=10,
+)
 coreconfigitem('smtp', 'host',
     default=None,
 )
diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -58,6 +58,7 @@
     rewriteutil,
     scmutil,
     server,
+    shelve as shelvemod,
     state as statemod,
     streamclone,
     tags as tagsmod,
@@ -5307,6 +5308,106 @@
     service = server.createservice(ui, repo, opts)
     return server.runservice(opts, initfn=service.init, runfn=service.run)
 
+ at command('shelve',
+         [('A', 'addremove', None,
+           _('mark new/missing files as added/removed before shelving')),
+          ('u', 'unknown', None,
+           _('store unknown files in the shelve')),
+          ('', 'cleanup', None,
+           _('delete all shelved changes')),
+          ('', 'date', '',
+           _('shelve with the specified commit date'), _('DATE')),
+          ('d', 'delete', None,
+           _('delete the named shelved change(s)')),
+          ('e', 'edit', False,
+           _('invoke editor on commit messages')),
+          ('k', 'keep', False,
+           _('shelve, but keep changes in the working directory')),
+          ('l', 'list', None,
+           _('list current shelves')),
+          ('m', 'message', '',
+           _('use text as shelve message'), _('TEXT')),
+          ('n', 'name', '',
+           _('use the given name for the shelved commit'), _('NAME')),
+          ('p', 'patch', None,
+           _('output patches for changes (provide the names of the shelved '
+             'changes as positional arguments)')),
+          ('i', 'interactive', None,
+           _('interactive mode, only works while creating a shelve')),
+          ('', 'stat', None,
+           _('output diffstat-style summary of changes (provide the names of '
+             'the shelved changes as positional arguments)')
+           )] + cmdutil.walkopts,
+         _('hg shelve [OPTION]... [FILE]...'),
+         helpcategory=command.CATEGORY_WORKING_DIRECTORY)
+def shelve(ui, repo, *pats, **opts):
+    '''save and set aside changes from the working directory
+
+    Shelving takes files that "hg status" reports as not clean, saves
+    the modifications to a bundle (a shelved change), and reverts the
+    files so that their state in the working directory becomes clean.
+
+    To restore these changes to the working directory, using "hg
+    unshelve"; this will work even if you switch to a different
+    commit.
+
+    When no files are specified, "hg shelve" saves all not-clean
+    files. If specific files or directories are named, only changes to
+    those files are shelved.
+
+    In bare shelve (when no files are specified, without interactive,
+    include and exclude option), shelving remembers information if the
+    working directory was on newly created branch, in other words working
+    directory was on different branch than its first parent. In this
+    situation unshelving restores branch information to the working directory.
+
+    Each shelved change has a name that makes it easier to find later.
+    The name of a shelved change defaults to being based on the active
+    bookmark, or if there is no active bookmark, the current named
+    branch.  To specify a different name, use ``--name``.
+
+    To see a list of existing shelved changes, use the ``--list``
+    option. For each shelved change, this will print its name, age,
+    and description; use ``--patch`` or ``--stat`` for more details.
+
+    To delete specific shelved changes, use ``--delete``. To delete
+    all shelved changes, use ``--cleanup``.
+    '''
+    opts = pycompat.byteskwargs(opts)
+    allowables = [
+        ('addremove', {'create'}), # 'create' is pseudo action
+        ('unknown', {'create'}),
+        ('cleanup', {'cleanup'}),
+#       ('date', {'create'}), # ignored for passing '--date "0 0"' in tests
+        ('delete', {'delete'}),
+        ('edit', {'create'}),
+        ('keep', {'create'}),
+        ('list', {'list'}),
+        ('message', {'create'}),
+        ('name', {'create'}),
+        ('patch', {'patch', 'list'}),
+        ('stat', {'stat', 'list'}),
+    ]
+    def checkopt(opt):
+        if opts.get(opt):
+            for i, allowable in allowables:
+                if opts[i] and opt not in allowable:
+                    raise error.Abort(_("options '--%s' and '--%s' may not be "
+                                       "used together") % (opt, i))
+            return True
+    if checkopt('cleanup'):
+        if pats:
+            raise error.Abort(_("cannot specify names when using '--cleanup'"))
+        return shelvemod.cleanupcmd(ui, repo)
+    elif checkopt('delete'):
+        return shelvemod.deletecmd(ui, repo, pats)
+    elif checkopt('list'):
+        return shelvemod.listcmd(ui, repo, pats, opts)
+    elif checkopt('patch') or checkopt('stat'):
+        return shelvemod.patchcmds(ui, repo, pats, opts)
+    else:
+        return shelvemod.createcmd(ui, repo, pats, opts)
+
 _NOTTERSE = 'nothing'
 
 @command('status|st',
@@ -6035,6 +6136,59 @@
 
     return postincoming(ui, repo, modheads, opts.get(r'update'), None, None)
 
+ at command('unshelve',
+         [('a', 'abort', None,
+           _('abort an incomplete unshelve operation')),
+          ('c', 'continue', None,
+           _('continue an incomplete unshelve operation')),
+          ('k', 'keep', None,
+           _('keep shelve after unshelving')),
+          ('n', 'name', '',
+           _('restore shelved change with given name'), _('NAME')),
+          ('t', 'tool', '', _('specify merge tool')),
+          ('', 'date', '',
+           _('set date for temporary commits (DEPRECATED)'), _('DATE'))],
+         _('hg unshelve [[-n] SHELVED]'),
+         helpcategory=command.CATEGORY_WORKING_DIRECTORY)
+def unshelve(ui, repo, *shelved, **opts):
+    """restore a shelved change to the working directory
+
+    This command accepts an optional name of a shelved change to
+    restore. If none is given, the most recent shelved change is used.
+
+    If a shelved change is applied successfully, the bundle that
+    contains the shelved changes is moved to a backup location
+    (.hg/shelve-backup).
+
+    Since you can restore a shelved change on top of an arbitrary
+    commit, it is possible that unshelving will result in a conflict
+    between your changes and the commits you are unshelving onto. If
+    this occurs, you must resolve the conflict, then use
+    ``--continue`` to complete the unshelve operation. (The bundle
+    will not be moved until you successfully complete the unshelve.)
+
+    (Alternatively, you can use ``--abort`` to abandon an unshelve
+    that causes a conflict. This reverts the unshelved changes, and
+    leaves the bundle in place.)
+
+    If bare shelved change (when no files are specified, without interactive,
+    include and exclude option) was done on newly created branch it would
+    restore branch information to the working directory.
+
+    After a successful unshelve, the shelved changes are stored in a
+    backup directory. Only the N most recent backups are kept. N
+    defaults to 10 but can be overridden using the ``shelve.maxbackups``
+    configuration option.
+
+    .. container:: verbose
+
+       Timestamp in seconds is used to decide order of backups. More
+       than ``maxbackups`` backups are kept, if same timestamp
+       prevents from deciding exact order of them, for safety.
+    """
+    with repo.wlock():
+        return shelvemod._dounshelve(ui, repo, *shelved, **opts)
+
 @command('update|up|checkout|co',
     [('C', 'clean', None, _('discard uncommitted changes (no backup)')),
     ('c', 'check', None, _('require clean working directory')),
diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -3296,6 +3296,9 @@
 unfinishedstates = [
     ('graftstate', True, False, _('graft in progress'),
      _("use 'hg graft --continue' or 'hg graft --stop' to stop")),
+    ('shelvedstate', False, False,
+     _('unshelve already in progress'),
+     _("use 'hg unshelve --continue' or 'hg unshelve --abort'")),
     ('updatestate', True, False, _('last update was interrupted'),
      _("use 'hg update' to get a consistent checkout"))
     ]
@@ -3333,6 +3336,8 @@
 afterresolvedstates = [
     ('graftstate',
      _('hg graft --continue')),
+    ('shelvedstate',
+     _('hg unshelve --continue')),
     ]
 
 def howtocontinue(repo):
diff --git a/hgext/shelve.py b/hgext/shelve.py
deleted file mode 100644
--- a/hgext/shelve.py
+++ /dev/null
@@ -1,1147 +0,0 @@
-# shelve.py - save/restore working directory state
-#
-# Copyright 2013 Facebook, Inc.
-#
-# This software may be used and distributed according to the terms of the
-# GNU General Public License version 2 or any later version.
-
-"""save and restore changes to the working directory
-
-The "hg shelve" command saves changes made to the working directory
-and reverts those changes, resetting the working directory to a clean
-state.
-
-Later on, the "hg unshelve" command restores the changes saved by "hg
-shelve". Changes can be restored even after updating to a different
-parent, in which case Mercurial's merge machinery will resolve any
-conflicts if necessary.
-
-You can have more than one shelved change outstanding at a time; each
-shelved change has a distinct name. For details, see the help for "hg
-shelve".
-"""
-from __future__ import absolute_import
-
-import collections
-import errno
-import itertools
-import stat
-
-from mercurial.i18n import _
-from mercurial import (
-    bookmarks,
-    bundle2,
-    bundlerepo,
-    changegroup,
-    cmdutil,
-    discovery,
-    error,
-    exchange,
-    hg,
-    lock as lockmod,
-    mdiff,
-    merge,
-    node as nodemod,
-    patch,
-    phases,
-    pycompat,
-    registrar,
-    repair,
-    scmutil,
-    templatefilters,
-    util,
-    vfs as vfsmod,
-)
-
-from . import (
-    rebase,
-)
-from mercurial.utils import (
-    dateutil,
-    stringutil,
-)
-
-cmdtable = {}
-command = registrar.command(cmdtable)
-# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
-# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
-# be specifying the version(s) of Mercurial they are tested with, or
-# leave the attribute unspecified.
-testedwith = 'ships-with-hg-core'
-
-configtable = {}
-configitem = registrar.configitem(configtable)
-
-configitem('shelve', 'maxbackups',
-    default=10,
-)
-
-backupdir = 'shelve-backup'
-shelvedir = 'shelved'
-shelvefileextensions = ['hg', 'patch', 'shelve']
-# universal extension is present in all types of shelves
-patchextension = 'patch'
-
-# we never need the user, so we use a
-# generic user for all shelve operations
-shelveuser = 'shelve at localhost'
-
-class shelvedfile(object):
-    """Helper for the file storing a single shelve
-
-    Handles common functions on shelve files (.hg/.patch) using
-    the vfs layer"""
-    def __init__(self, repo, name, filetype=None):
-        self.repo = repo
-        self.name = name
-        self.vfs = vfsmod.vfs(repo.vfs.join(shelvedir))
-        self.backupvfs = vfsmod.vfs(repo.vfs.join(backupdir))
-        self.ui = self.repo.ui
-        if filetype:
-            self.fname = name + '.' + filetype
-        else:
-            self.fname = name
-
-    def exists(self):
-        return self.vfs.exists(self.fname)
-
-    def filename(self):
-        return self.vfs.join(self.fname)
-
-    def backupfilename(self):
-        def gennames(base):
-            yield base
-            base, ext = base.rsplit('.', 1)
-            for i in itertools.count(1):
-                yield '%s-%d.%s' % (base, i, ext)
-
-        name = self.backupvfs.join(self.fname)
-        for n in gennames(name):
-            if not self.backupvfs.exists(n):
-                return n
-
-    def movetobackup(self):
-        if not self.backupvfs.isdir():
-            self.backupvfs.makedir()
-        util.rename(self.filename(), self.backupfilename())
-
-    def stat(self):
-        return self.vfs.stat(self.fname)
-
-    def opener(self, mode='rb'):
-        try:
-            return self.vfs(self.fname, mode)
-        except IOError as err:
-            if err.errno != errno.ENOENT:
-                raise
-            raise error.Abort(_("shelved change '%s' not found") % self.name)
-
-    def applybundle(self, tr):
-        fp = self.opener()
-        try:
-            targetphase = phases.internal
-            if not phases.supportinternal(self.repo):
-                targetphase = phases.secret
-            gen = exchange.readbundle(self.repo.ui, fp, self.fname, self.vfs)
-            pretip = self.repo['tip']
-            bundle2.applybundle(self.repo, gen, tr,
-                                source='unshelve',
-                                url='bundle:' + self.vfs.join(self.fname),
-                                targetphase=targetphase)
-            shelvectx = self.repo['tip']
-            if pretip == shelvectx:
-                shelverev = tr.changes['revduplicates'][-1]
-                shelvectx = self.repo[shelverev]
-            return shelvectx
-        finally:
-            fp.close()
-
-    def bundlerepo(self):
-        path = self.vfs.join(self.fname)
-        return bundlerepo.instance(self.repo.baseui,
-                                   'bundle://%s+%s' % (self.repo.root, path))
-
-    def writebundle(self, bases, node):
-        cgversion = changegroup.safeversion(self.repo)
-        if cgversion == '01':
-            btype = 'HG10BZ'
-            compression = None
-        else:
-            btype = 'HG20'
-            compression = 'BZ'
-
-        repo = self.repo.unfiltered()
-
-        outgoing = discovery.outgoing(repo, missingroots=bases,
-                                      missingheads=[node])
-        cg = changegroup.makechangegroup(repo, outgoing, cgversion, 'shelve')
-
-        bundle2.writebundle(self.ui, cg, self.fname, btype, self.vfs,
-                                compression=compression)
-
-    def writeinfo(self, info):
-        scmutil.simplekeyvaluefile(self.vfs, self.fname).write(info)
-
-    def readinfo(self):
-        return scmutil.simplekeyvaluefile(self.vfs, self.fname).read()
-
-class shelvedstate(object):
-    """Handle persistence during unshelving operations.
-
-    Handles saving and restoring a shelved state. Ensures that different
-    versions of a shelved state are possible and handles them appropriately.
-    """
-    _version = 2
-    _filename = 'shelvedstate'
-    _keep = 'keep'
-    _nokeep = 'nokeep'
-    # colon is essential to differentiate from a real bookmark name
-    _noactivebook = ':no-active-bookmark'
-
-    @classmethod
-    def _verifyandtransform(cls, d):
-        """Some basic shelvestate syntactic verification and transformation"""
-        try:
-            d['originalwctx'] = nodemod.bin(d['originalwctx'])
-            d['pendingctx'] = nodemod.bin(d['pendingctx'])
-            d['parents'] = [nodemod.bin(h)
-                            for h in d['parents'].split(' ')]
-            d['nodestoremove'] = [nodemod.bin(h)
-                                  for h in d['nodestoremove'].split(' ')]
-        except (ValueError, TypeError, KeyError) as err:
-            raise error.CorruptedState(pycompat.bytestr(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(pycompat.bytestr(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']
-            obj.wctx = repo[d['originalwctx']]
-            obj.pendingctx = repo[d['pendingctx']]
-            obj.parents = d['parents']
-            obj.nodestoremove = d['nodestoremove']
-            obj.branchtorestore = d.get('branchtorestore', '')
-            obj.keep = d.get('keep') == cls._keep
-            obj.activebookmark = ''
-            if d.get('activebook', '') != cls._noactivebook:
-                obj.activebookmark = d.get('activebook', '')
-        except (error.RepoLookupError, KeyError) as err:
-            raise error.CorruptedState(pycompat.bytestr(err))
-
-        return obj
-
-    @classmethod
-    def save(cls, repo, name, originalwctx, pendingctx, nodestoremove,
-             branchtorestore, keep=False, activebook=''):
-        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=("%d" % cls._version))
-
-    @classmethod
-    def clear(cls, repo):
-        repo.vfs.unlinkpath(cls._filename, ignoremissing=True)
-
-def cleanupoldbackups(repo):
-    vfs = vfsmod.vfs(repo.vfs.join(backupdir))
-    maxbackups = repo.ui.configint('shelve', 'maxbackups')
-    hgfiles = [f for f in vfs.listdir()
-               if f.endswith('.' + patchextension)]
-    hgfiles = sorted([(vfs.stat(f)[stat.ST_MTIME], f) for f in hgfiles])
-    if maxbackups > 0 and maxbackups < len(hgfiles):
-        bordermtime = hgfiles[-maxbackups][0]
-    else:
-        bordermtime = None
-    for mtime, f in hgfiles[:len(hgfiles) - maxbackups]:
-        if mtime == bordermtime:
-            # keep it, because timestamp can't decide exact order of backups
-            continue
-        base = f[:-(1 + len(patchextension))]
-        for ext in shelvefileextensions:
-            vfs.tryunlink(base + '.' + ext)
-
-def _backupactivebookmark(repo):
-    activebookmark = repo._activebookmark
-    if activebookmark:
-        bookmarks.deactivate(repo)
-    return activebookmark
-
-def _restoreactivebookmark(repo, mark):
-    if mark:
-        bookmarks.activate(repo, mark)
-
-def _aborttransaction(repo, tr):
-    '''Abort current transaction for shelve/unshelve, but keep dirstate
-    '''
-    dirstatebackupname = 'dirstate.shelve'
-    repo.dirstate.savebackup(tr, dirstatebackupname)
-    tr.abort()
-    repo.dirstate.restorebackup(None, dirstatebackupname)
-
-def getshelvename(repo, parent, opts):
-    """Decide on the name this shelve is going to have"""
-    def gennames():
-        yield label
-        for i in itertools.count(1):
-            yield '%s-%02d' % (label, i)
-    name = opts.get('name')
-    label = repo._activebookmark or parent.branch() or 'default'
-    # slashes aren't allowed in filenames, therefore we rename it
-    label = label.replace('/', '_')
-    label = label.replace('\\', '_')
-    # filenames must not start with '.' as it should not be hidden
-    if label.startswith('.'):
-        label = label.replace('.', '_', 1)
-
-    if name:
-        if shelvedfile(repo, name, patchextension).exists():
-            e = _("a shelved change named '%s' already exists") % name
-            raise error.Abort(e)
-
-        # ensure we are not creating a subdirectory or a hidden file
-        if '/' in name or '\\' in name:
-            raise error.Abort(_('shelved change names can not contain slashes'))
-        if name.startswith('.'):
-            raise error.Abort(_("shelved change names can not start with '.'"))
-
-    else:
-        for n in gennames():
-            if not shelvedfile(repo, n, patchextension).exists():
-                name = n
-                break
-
-    return name
-
-def mutableancestors(ctx):
-    """return all mutable ancestors for ctx (included)
-
-    Much faster than the revset ancestors(ctx) & draft()"""
-    seen = {nodemod.nullrev}
-    visit = collections.deque()
-    visit.append(ctx)
-    while visit:
-        ctx = visit.popleft()
-        yield ctx.node()
-        for parent in ctx.parents():
-            rev = parent.rev()
-            if rev not in seen:
-                seen.add(rev)
-                if parent.mutable():
-                    visit.append(parent)
-
-def getcommitfunc(extra, interactive, editor=False):
-    def commitfunc(ui, repo, message, match, opts):
-        hasmq = util.safehasattr(repo, 'mq')
-        if hasmq:
-            saved, repo.mq.checkapplied = repo.mq.checkapplied, False
-
-        targetphase = phases.internal
-        if not phases.supportinternal(repo):
-            targetphase = phases.secret
-        overrides = {('phases', 'new-commit'): targetphase}
-        try:
-            editor_ = False
-            if editor:
-                editor_ = cmdutil.getcommiteditor(editform='shelve.shelve',
-                                                  **pycompat.strkwargs(opts))
-            with repo.ui.configoverride(overrides):
-                return repo.commit(message, shelveuser, opts.get('date'),
-                                   match, editor=editor_, extra=extra)
-        finally:
-            if hasmq:
-                repo.mq.checkapplied = saved
-
-    def interactivecommitfunc(ui, repo, *pats, **opts):
-        opts = pycompat.byteskwargs(opts)
-        match = scmutil.match(repo['.'], pats, {})
-        message = opts['message']
-        return commitfunc(ui, repo, message, match, opts)
-
-    return interactivecommitfunc if interactive else commitfunc
-
-def _nothingtoshelvemessaging(ui, repo, pats, opts):
-    stat = repo.status(match=scmutil.match(repo[None], pats, opts))
-    if stat.deleted:
-        ui.status(_("nothing changed (%d missing files, see "
-                    "'hg status')\n") % len(stat.deleted))
-    else:
-        ui.status(_("nothing changed\n"))
-
-def _shelvecreatedcommit(repo, node, name, match):
-    info = {'node': nodemod.hex(node)}
-    shelvedfile(repo, name, 'shelve').writeinfo(info)
-    bases = list(mutableancestors(repo[node]))
-    shelvedfile(repo, name, 'hg').writebundle(bases, node)
-    with shelvedfile(repo, name, patchextension).opener('wb') as fp:
-        cmdutil.exportfile(repo, [node], fp, opts=mdiff.diffopts(git=True),
-                           match=match)
-
-def _includeunknownfiles(repo, pats, opts, extra):
-    s = repo.status(match=scmutil.match(repo[None], pats, opts),
-                    unknown=True)
-    if s.unknown:
-        extra['shelve_unknown'] = '\0'.join(s.unknown)
-        repo[None].add(s.unknown)
-
-def _finishshelve(repo, tr):
-    if phases.supportinternal(repo):
-        tr.close()
-    else:
-        _aborttransaction(repo, tr)
-
-def createcmd(ui, repo, pats, opts):
-    """subcommand that creates a new shelve"""
-    with repo.wlock():
-        cmdutil.checkunfinished(repo)
-        return _docreatecmd(ui, repo, pats, opts)
-
-def _docreatecmd(ui, repo, pats, opts):
-    wctx = repo[None]
-    parents = wctx.parents()
-    if len(parents) > 1:
-        raise error.Abort(_('cannot shelve while merging'))
-    parent = parents[0]
-    origbranch = wctx.branch()
-
-    if parent.node() != nodemod.nullid:
-        desc = "changes to: %s" % parent.description().split('\n', 1)[0]
-    else:
-        desc = '(changes in empty repository)'
-
-    if not opts.get('message'):
-        opts['message'] = desc
-
-    lock = tr = activebookmark = None
-    try:
-        lock = repo.lock()
-
-        # use an uncommitted transaction to generate the bundle to avoid
-        # pull races. ensure we don't print the abort message to stderr.
-        tr = repo.transaction('shelve', report=lambda x: None)
-
-        interactive = opts.get('interactive', False)
-        includeunknown = (opts.get('unknown', False) and
-                          not opts.get('addremove', False))
-
-        name = getshelvename(repo, parent, opts)
-        activebookmark = _backupactivebookmark(repo)
-        extra = {'internal': 'shelve'}
-        if includeunknown:
-            _includeunknownfiles(repo, pats, opts, extra)
-
-        if _iswctxonnewbranch(repo) and not _isbareshelve(pats, opts):
-            # In non-bare shelve we don't store newly created branch
-            # at bundled commit
-            repo.dirstate.setbranch(repo['.'].branch())
-
-        commitfunc = getcommitfunc(extra, interactive, editor=True)
-        if not interactive:
-            node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
-        else:
-            node = cmdutil.dorecord(ui, repo, commitfunc, None,
-                                    False, cmdutil.recordfilter, *pats,
-                                    **pycompat.strkwargs(opts))
-        if not node:
-            _nothingtoshelvemessaging(ui, repo, pats, opts)
-            return 1
-
-        # Create a matcher so that prefetch doesn't attempt to fetch
-        # the entire repository pointlessly, and as an optimisation
-        # for movedirstate, if needed.
-        match = scmutil.matchfiles(repo, repo[node].files())
-        _shelvecreatedcommit(repo, node, name, match)
-
-        if ui.formatted():
-            desc = stringutil.ellipsis(desc, ui.termwidth())
-        ui.status(_('shelved as %s\n') % name)
-        if opts['keep']:
-            with repo.dirstate.parentchange():
-                scmutil.movedirstate(repo, parent, match)
-        else:
-            hg.update(repo, parent.node())
-        if origbranch != repo['.'].branch() and not _isbareshelve(pats, opts):
-            repo.dirstate.setbranch(origbranch)
-
-        _finishshelve(repo, tr)
-    finally:
-        _restoreactivebookmark(repo, activebookmark)
-        lockmod.release(tr, lock)
-
-def _isbareshelve(pats, opts):
-    return (not pats
-            and not opts.get('interactive', False)
-            and not opts.get('include', False)
-            and not opts.get('exclude', False))
-
-def _iswctxonnewbranch(repo):
-    return repo[None].branch() != repo['.'].branch()
-
-def cleanupcmd(ui, repo):
-    """subcommand that deletes all shelves"""
-
-    with repo.wlock():
-        for (name, _type) in repo.vfs.readdir(shelvedir):
-            suffix = name.rsplit('.', 1)[-1]
-            if suffix in shelvefileextensions:
-                shelvedfile(repo, name).movetobackup()
-            cleanupoldbackups(repo)
-
-def deletecmd(ui, repo, pats):
-    """subcommand that deletes a specific shelve"""
-    if not pats:
-        raise error.Abort(_('no shelved changes specified!'))
-    with repo.wlock():
-        try:
-            for name in pats:
-                for suffix in shelvefileextensions:
-                    shfile = shelvedfile(repo, name, suffix)
-                    # patch file is necessary, as it should
-                    # be present for any kind of shelve,
-                    # but the .hg file is optional as in future we
-                    # will add obsolete shelve with does not create a
-                    # bundle
-                    if shfile.exists() or suffix == patchextension:
-                        shfile.movetobackup()
-            cleanupoldbackups(repo)
-        except OSError as err:
-            if err.errno != errno.ENOENT:
-                raise
-            raise error.Abort(_("shelved change '%s' not found") % name)
-
-def listshelves(repo):
-    """return all shelves in repo as list of (time, filename)"""
-    try:
-        names = repo.vfs.readdir(shelvedir)
-    except OSError as err:
-        if err.errno != errno.ENOENT:
-            raise
-        return []
-    info = []
-    for (name, _type) in names:
-        pfx, sfx = name.rsplit('.', 1)
-        if not pfx or sfx != patchextension:
-            continue
-        st = shelvedfile(repo, name).stat()
-        info.append((st[stat.ST_MTIME], shelvedfile(repo, pfx).filename()))
-    return sorted(info, reverse=True)
-
-def listcmd(ui, repo, pats, opts):
-    """subcommand that displays the list of shelves"""
-    pats = set(pats)
-    width = 80
-    if not ui.plain():
-        width = ui.termwidth()
-    namelabel = 'shelve.newest'
-    ui.pager('shelve')
-    for mtime, name in listshelves(repo):
-        sname = util.split(name)[1]
-        if pats and sname not in pats:
-            continue
-        ui.write(sname, label=namelabel)
-        namelabel = 'shelve.name'
-        if ui.quiet:
-            ui.write('\n')
-            continue
-        ui.write(' ' * (16 - len(sname)))
-        used = 16
-        date = dateutil.makedate(mtime)
-        age = '(%s)' % templatefilters.age(date, abbrev=True)
-        ui.write(age, label='shelve.age')
-        ui.write(' ' * (12 - len(age)))
-        used += 12
-        with open(name + '.' + patchextension, 'rb') as fp:
-            while True:
-                line = fp.readline()
-                if not line:
-                    break
-                if not line.startswith('#'):
-                    desc = line.rstrip()
-                    if ui.formatted():
-                        desc = stringutil.ellipsis(desc, width - used)
-                    ui.write(desc)
-                    break
-            ui.write('\n')
-            if not (opts['patch'] or opts['stat']):
-                continue
-            difflines = fp.readlines()
-            if opts['patch']:
-                for chunk, label in patch.difflabel(iter, difflines):
-                    ui.write(chunk, label=label)
-            if opts['stat']:
-                for chunk, label in patch.diffstatui(difflines, width=width):
-                    ui.write(chunk, label=label)
-
-def patchcmds(ui, repo, pats, opts):
-    """subcommand that displays shelves"""
-    if len(pats) == 0:
-        shelves = listshelves(repo)
-        if not shelves:
-            raise error.Abort(_("there are no shelves to show"))
-        mtime, name = shelves[0]
-        sname = util.split(name)[1]
-        pats = [sname]
-
-    for shelfname in pats:
-        if not shelvedfile(repo, shelfname, patchextension).exists():
-            raise error.Abort(_("cannot find shelf %s") % shelfname)
-
-    listcmd(ui, repo, pats, opts)
-
-def checkparents(repo, state):
-    """check parent while resuming an unshelve"""
-    if state.parents != repo.dirstate.parents():
-        raise error.Abort(_('working directory parents do not match unshelve '
-                           'state'))
-
-def unshelveabort(ui, repo, state, opts):
-    """subcommand that abort an in-progress unshelve"""
-    with repo.lock():
-        try:
-            checkparents(repo, state)
-
-            merge.update(repo, state.pendingctx, branchmerge=False, force=True)
-            if (state.activebookmark
-                    and state.activebookmark in repo._bookmarks):
-                bookmarks.activate(repo, state.activebookmark)
-
-            if repo.vfs.exists('unshelverebasestate'):
-                repo.vfs.rename('unshelverebasestate', 'rebasestate')
-                rebase.clearstatus(repo)
-
-            mergefiles(ui, repo, state.wctx, state.pendingctx)
-            if not phases.supportinternal(repo):
-                repair.strip(ui, repo, state.nodestoremove, backup=False,
-                             topic='shelve')
-        finally:
-            shelvedstate.clear(repo)
-            ui.warn(_("unshelve of '%s' aborted\n") % state.name)
-
-def mergefiles(ui, repo, wctx, shelvectx):
-    """updates to wctx and merges the changes from shelvectx into the
-    dirstate."""
-    with ui.configoverride({('ui', 'quiet'): True}):
-        hg.update(repo, wctx.node())
-        ui.pushbuffer(True)
-        cmdutil.revert(ui, repo, shelvectx, repo.dirstate.parents())
-        ui.popbuffer()
-
-def restorebranch(ui, repo, branchtorestore):
-    if branchtorestore and branchtorestore != repo.dirstate.branch():
-        repo.dirstate.setbranch(branchtorestore)
-        ui.status(_('marked working directory as branch %s\n')
-                  % branchtorestore)
-
-def unshelvecleanup(ui, repo, name, opts):
-    """remove related files after an unshelve"""
-    if not opts.get('keep'):
-        for filetype in shelvefileextensions:
-            shfile = shelvedfile(repo, name, filetype)
-            if shfile.exists():
-                shfile.movetobackup()
-        cleanupoldbackups(repo)
-
-def unshelvecontinue(ui, repo, state, opts):
-    """subcommand to continue an in-progress unshelve"""
-    # We're finishing off a merge. First parent is our original
-    # parent, second is the temporary "fake" commit we're unshelving.
-    with repo.lock():
-        checkparents(repo, state)
-        ms = merge.mergestate.read(repo)
-        if list(ms.unresolved()):
-            raise error.Abort(
-                _("unresolved conflicts, can't continue"),
-                hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
-
-        shelvectx = repo[state.parents[1]]
-        pendingctx = state.pendingctx
-
-        with repo.dirstate.parentchange():
-            repo.setparents(state.pendingctx.node(), nodemod.nullid)
-            repo.dirstate.write(repo.currenttransaction())
-
-        targetphase = phases.internal
-        if not phases.supportinternal(repo):
-            targetphase = phases.secret
-        overrides = {('phases', 'new-commit'): targetphase}
-        with repo.ui.configoverride(overrides, 'unshelve'):
-            with repo.dirstate.parentchange():
-                repo.setparents(state.parents[0], nodemod.nullid)
-                newnode = repo.commit(text=shelvectx.description(),
-                                      extra=shelvectx.extra(),
-                                      user=shelvectx.user(),
-                                      date=shelvectx.date())
-
-        if newnode is None:
-            # If it ended up being a no-op commit, then the normal
-            # merge state clean-up path doesn't happen, so do it
-            # here. Fix issue5494
-            merge.mergestate.clean(repo)
-            shelvectx = state.pendingctx
-            msg = _('note: unshelved changes already existed '
-                    'in the working copy\n')
-            ui.status(msg)
-        else:
-            # only strip the shelvectx if we produced one
-            state.nodestoremove.append(newnode)
-            shelvectx = repo[newnode]
-
-        hg.updaterepo(repo, pendingctx.node(), overwrite=False)
-
-        if repo.vfs.exists('unshelverebasestate'):
-            repo.vfs.rename('unshelverebasestate', 'rebasestate')
-            rebase.clearstatus(repo)
-
-        mergefiles(ui, repo, state.wctx, shelvectx)
-        restorebranch(ui, repo, state.branchtorestore)
-
-        if not phases.supportinternal(repo):
-            repair.strip(ui, repo, state.nodestoremove, backup=False,
-                         topic='shelve')
-        _restoreactivebookmark(repo, state.activebookmark)
-        shelvedstate.clear(repo)
-        unshelvecleanup(ui, repo, state.name, opts)
-        ui.status(_("unshelve of '%s' complete\n") % state.name)
-
-def _commitworkingcopychanges(ui, repo, opts, tmpwctx):
-    """Temporarily commit working copy changes before moving unshelve commit"""
-    # Store pending changes in a commit and remember added in case a shelve
-    # contains unknown files that are part of the pending change
-    s = repo.status()
-    addedbefore = frozenset(s.added)
-    if not (s.modified or s.added or s.removed):
-        return tmpwctx, addedbefore
-    ui.status(_("temporarily committing pending changes "
-                "(restore with 'hg unshelve --abort')\n"))
-    extra = {'internal': 'shelve'}
-    commitfunc = getcommitfunc(extra=extra, interactive=False,
-                               editor=False)
-    tempopts = {}
-    tempopts['message'] = "pending changes temporary commit"
-    tempopts['date'] = opts.get('date')
-    with ui.configoverride({('ui', 'quiet'): True}):
-        node = cmdutil.commit(ui, repo, commitfunc, [], tempopts)
-    tmpwctx = repo[node]
-    return tmpwctx, addedbefore
-
-def _unshelverestorecommit(ui, repo, tr, basename):
-    """Recreate commit in the repository during the unshelve"""
-    repo = repo.unfiltered()
-    node = None
-    if shelvedfile(repo, basename, 'shelve').exists():
-        node = shelvedfile(repo, basename, 'shelve').readinfo()['node']
-    if node is None or node not in repo:
-        with ui.configoverride({('ui', 'quiet'): True}):
-            shelvectx = shelvedfile(repo, basename, 'hg').applybundle(tr)
-        # We might not strip the unbundled changeset, so we should keep track of
-        # the unshelve node in case we need to reuse it (eg: unshelve --keep)
-        if node is None:
-            info = {'node': nodemod.hex(shelvectx.node())}
-            shelvedfile(repo, basename, 'shelve').writeinfo(info)
-    else:
-        shelvectx = repo[node]
-
-    return repo, shelvectx
-
-def _rebaserestoredcommit(ui, repo, opts, tr, oldtiprev, basename, pctx,
-                          tmpwctx, shelvectx, branchtorestore,
-                          activebookmark):
-    """Rebase restored commit from its original location to a destination"""
-    # If the shelve is not immediately on top of the commit
-    # we'll be merging with, rebase it to be on top.
-    if tmpwctx.node() == shelvectx.p1().node():
-        return shelvectx
-
-    overrides = {
-        ('ui', 'forcemerge'): opts.get('tool', ''),
-        ('phases', 'new-commit'): phases.secret,
-    }
-    with repo.ui.configoverride(overrides, 'unshelve'):
-        ui.status(_('rebasing shelved changes\n'))
-        stats = merge.graft(repo, shelvectx, shelvectx.p1(),
-                           labels=['shelve', 'working-copy'],
-                           keepconflictparent=True)
-        if stats.unresolvedcount:
-            tr.close()
-
-            nodestoremove = [repo.changelog.node(rev)
-                             for rev in pycompat.xrange(oldtiprev, len(repo))]
-            shelvedstate.save(repo, basename, pctx, tmpwctx, nodestoremove,
-                              branchtorestore, opts.get('keep'), activebookmark)
-            raise error.InterventionRequired(
-                _("unresolved conflicts (see 'hg resolve', then "
-                  "'hg unshelve --continue')"))
-
-        with repo.dirstate.parentchange():
-            repo.setparents(tmpwctx.node(), nodemod.nullid)
-            newnode = repo.commit(text=shelvectx.description(),
-                                  extra=shelvectx.extra(),
-                                  user=shelvectx.user(),
-                                  date=shelvectx.date())
-
-        if newnode is None:
-            # If it ended up being a no-op commit, then the normal
-            # merge state clean-up path doesn't happen, so do it
-            # here. Fix issue5494
-            merge.mergestate.clean(repo)
-            shelvectx = tmpwctx
-            msg = _('note: unshelved changes already existed '
-                    'in the working copy\n')
-            ui.status(msg)
-        else:
-            shelvectx = repo[newnode]
-            hg.updaterepo(repo, tmpwctx.node(), False)
-
-    return shelvectx
-
-def _forgetunknownfiles(repo, shelvectx, addedbefore):
-    # Forget any files that were unknown before the shelve, unknown before
-    # unshelve started, but are now added.
-    shelveunknown = shelvectx.extra().get('shelve_unknown')
-    if not shelveunknown:
-        return
-    shelveunknown = frozenset(shelveunknown.split('\0'))
-    addedafter = frozenset(repo.status().added)
-    toforget = (addedafter & shelveunknown) - addedbefore
-    repo[None].forget(toforget)
-
-def _finishunshelve(repo, oldtiprev, tr, activebookmark):
-    _restoreactivebookmark(repo, activebookmark)
-    # The transaction aborting will strip all the commits for us,
-    # but it doesn't update the inmemory structures, so addchangegroup
-    # hooks still fire and try to operate on the missing commits.
-    # Clean up manually to prevent this.
-    repo.unfiltered().changelog.strip(oldtiprev, tr)
-    _aborttransaction(repo, tr)
-
-def _checkunshelveuntrackedproblems(ui, repo, shelvectx):
-    """Check potential problems which may result from working
-    copy having untracked changes."""
-    wcdeleted = set(repo.status().deleted)
-    shelvetouched = set(shelvectx.files())
-    intersection = wcdeleted.intersection(shelvetouched)
-    if intersection:
-        m = _("shelved change touches missing files")
-        hint = _("run hg status to see which files are missing")
-        raise error.Abort(m, hint=hint)
-
- at command('unshelve',
-         [('a', 'abort', None,
-           _('abort an incomplete unshelve operation')),
-          ('c', 'continue', None,
-           _('continue an incomplete unshelve operation')),
-          ('k', 'keep', None,
-           _('keep shelve after unshelving')),
-          ('n', 'name', '',
-           _('restore shelved change with given name'), _('NAME')),
-          ('t', 'tool', '', _('specify merge tool')),
-          ('', 'date', '',
-           _('set date for temporary commits (DEPRECATED)'), _('DATE'))],
-         _('hg unshelve [[-n] SHELVED]'),
-         helpcategory=command.CATEGORY_WORKING_DIRECTORY)
-def unshelve(ui, repo, *shelved, **opts):
-    """restore a shelved change to the working directory
-
-    This command accepts an optional name of a shelved change to
-    restore. If none is given, the most recent shelved change is used.
-
-    If a shelved change is applied successfully, the bundle that
-    contains the shelved changes is moved to a backup location
-    (.hg/shelve-backup).
-
-    Since you can restore a shelved change on top of an arbitrary
-    commit, it is possible that unshelving will result in a conflict
-    between your changes and the commits you are unshelving onto. If
-    this occurs, you must resolve the conflict, then use
-    ``--continue`` to complete the unshelve operation. (The bundle
-    will not be moved until you successfully complete the unshelve.)
-
-    (Alternatively, you can use ``--abort`` to abandon an unshelve
-    that causes a conflict. This reverts the unshelved changes, and
-    leaves the bundle in place.)
-
-    If bare shelved change(when no files are specified, without interactive,
-    include and exclude option) was done on newly created branch it would
-    restore branch information to the working directory.
-
-    After a successful unshelve, the shelved changes are stored in a
-    backup directory. Only the N most recent backups are kept. N
-    defaults to 10 but can be overridden using the ``shelve.maxbackups``
-    configuration option.
-
-    .. container:: verbose
-
-       Timestamp in seconds is used to decide order of backups. More
-       than ``maxbackups`` backups are kept, if same timestamp
-       prevents from deciding exact order of them, for safety.
-    """
-    with repo.wlock():
-        return _dounshelve(ui, repo, *shelved, **opts)
-
-def _dounshelve(ui, repo, *shelved, **opts):
-    opts = pycompat.byteskwargs(opts)
-    abortf = opts.get('abort')
-    continuef = opts.get('continue')
-    if not abortf and not continuef:
-        cmdutil.checkunfinished(repo)
-    shelved = list(shelved)
-    if opts.get("name"):
-        shelved.append(opts["name"])
-
-    if abortf or continuef:
-        if abortf and continuef:
-            raise error.Abort(_('cannot use both abort and continue'))
-        if shelved:
-            raise error.Abort(_('cannot combine abort/continue with '
-                               'naming a shelved change'))
-        if abortf and opts.get('tool', False):
-            ui.warn(_('tool option will be ignored\n'))
-
-        try:
-            state = shelvedstate.load(repo)
-            if opts.get('keep') is None:
-                opts['keep'] = state.keep
-        except IOError as err:
-            if err.errno != errno.ENOENT:
-                raise
-            cmdutil.wrongtooltocontinue(repo, _('unshelve'))
-        except error.CorruptedState as err:
-            ui.debug(pycompat.bytestr(err) + '\n')
-            if continuef:
-                msg = _('corrupted shelved state file')
-                hint = _('please run hg unshelve --abort to abort unshelve '
-                         'operation')
-                raise error.Abort(msg, hint=hint)
-            elif abortf:
-                msg = _('could not read shelved state file, your working copy '
-                        'may be in an unexpected state\nplease update to some '
-                        'commit\n')
-                ui.warn(msg)
-                shelvedstate.clear(repo)
-            return
-
-        if abortf:
-            return unshelveabort(ui, repo, state, opts)
-        elif continuef:
-            return unshelvecontinue(ui, repo, state, opts)
-    elif len(shelved) > 1:
-        raise error.Abort(_('can only unshelve one change at a time'))
-
-    # abort unshelve while merging (issue5123)
-    parents = repo[None].parents()
-    if len(parents) > 1:
-        raise error.Abort(_('cannot unshelve while merging'))
-
-    elif not shelved:
-        shelved = listshelves(repo)
-        if not shelved:
-            raise error.Abort(_('no shelved changes to apply!'))
-        basename = util.split(shelved[0][1])[1]
-        ui.status(_("unshelving change '%s'\n") % basename)
-    else:
-        basename = shelved[0]
-
-    if not shelvedfile(repo, basename, patchextension).exists():
-        raise error.Abort(_("shelved change '%s' not found") % basename)
-
-    repo = repo.unfiltered()
-    lock = tr = None
-    try:
-        lock = repo.lock()
-        tr = repo.transaction('unshelve', report=lambda x: None)
-        oldtiprev = len(repo)
-
-        pctx = repo['.']
-        tmpwctx = pctx
-        # The goal is to have a commit structure like so:
-        # ...-> pctx -> tmpwctx -> shelvectx
-        # where tmpwctx is an optional commit with the user's pending changes
-        # and shelvectx is the unshelved changes. Then we merge it all down
-        # to the original pctx.
-
-        activebookmark = _backupactivebookmark(repo)
-        tmpwctx, addedbefore = _commitworkingcopychanges(ui, repo, opts,
-                                                         tmpwctx)
-        repo, shelvectx = _unshelverestorecommit(ui, repo, tr, basename)
-        _checkunshelveuntrackedproblems(ui, repo, shelvectx)
-        branchtorestore = ''
-        if shelvectx.branch() != shelvectx.p1().branch():
-            branchtorestore = shelvectx.branch()
-
-        shelvectx = _rebaserestoredcommit(ui, repo, opts, tr, oldtiprev,
-                                          basename, pctx, tmpwctx,
-                                          shelvectx, branchtorestore,
-                                          activebookmark)
-        overrides = {('ui', 'forcemerge'): opts.get('tool', '')}
-        with ui.configoverride(overrides, 'unshelve'):
-            mergefiles(ui, repo, pctx, shelvectx)
-        restorebranch(ui, repo, branchtorestore)
-        _forgetunknownfiles(repo, shelvectx, addedbefore)
-
-        shelvedstate.clear(repo)
-        _finishunshelve(repo, oldtiprev, tr, activebookmark)
-        unshelvecleanup(ui, repo, basename, opts)
-    finally:
-        if tr:
-            tr.release()
-        lockmod.release(lock)
-
- at command('shelve',
-         [('A', 'addremove', None,
-           _('mark new/missing files as added/removed before shelving')),
-          ('u', 'unknown', None,
-           _('store unknown files in the shelve')),
-          ('', 'cleanup', None,
-           _('delete all shelved changes')),
-          ('', 'date', '',
-           _('shelve with the specified commit date'), _('DATE')),
-          ('d', 'delete', None,
-           _('delete the named shelved change(s)')),
-          ('e', 'edit', False,
-           _('invoke editor on commit messages')),
-          ('k', 'keep', False,
-           _('shelve, but keep changes in the working directory')),
-          ('l', 'list', None,
-           _('list current shelves')),
-          ('m', 'message', '',
-           _('use text as shelve message'), _('TEXT')),
-          ('n', 'name', '',
-           _('use the given name for the shelved commit'), _('NAME')),
-          ('p', 'patch', None,
-           _('output patches for changes (provide the names of the shelved '
-             'changes as positional arguments)')),
-          ('i', 'interactive', None,
-           _('interactive mode, only works while creating a shelve')),
-          ('', 'stat', None,
-           _('output diffstat-style summary of changes (provide the names of '
-             'the shelved changes as positional arguments)')
-           )] + cmdutil.walkopts,
-         _('hg shelve [OPTION]... [FILE]...'),
-         helpcategory=command.CATEGORY_WORKING_DIRECTORY)
-def shelvecmd(ui, repo, *pats, **opts):
-    '''save and set aside changes from the working directory
-
-    Shelving takes files that "hg status" reports as not clean, saves
-    the modifications to a bundle (a shelved change), and reverts the
-    files so that their state in the working directory becomes clean.
-
-    To restore these changes to the working directory, using "hg
-    unshelve"; this will work even if you switch to a different
-    commit.
-
-    When no files are specified, "hg shelve" saves all not-clean
-    files. If specific files or directories are named, only changes to
-    those files are shelved.
-
-    In bare shelve (when no files are specified, without interactive,
-    include and exclude option), shelving remembers information if the
-    working directory was on newly created branch, in other words working
-    directory was on different branch than its first parent. In this
-    situation unshelving restores branch information to the working directory.
-
-    Each shelved change has a name that makes it easier to find later.
-    The name of a shelved change defaults to being based on the active
-    bookmark, or if there is no active bookmark, the current named
-    branch.  To specify a different name, use ``--name``.
-
-    To see a list of existing shelved changes, use the ``--list``
-    option. For each shelved change, this will print its name, age,
-    and description; use ``--patch`` or ``--stat`` for more details.
-
-    To delete specific shelved changes, use ``--delete``. To delete
-    all shelved changes, use ``--cleanup``.
-    '''
-    opts = pycompat.byteskwargs(opts)
-    allowables = [
-        ('addremove', {'create'}), # 'create' is pseudo action
-        ('unknown', {'create'}),
-        ('cleanup', {'cleanup'}),
-#       ('date', {'create'}), # ignored for passing '--date "0 0"' in tests
-        ('delete', {'delete'}),
-        ('edit', {'create'}),
-        ('keep', {'create'}),
-        ('list', {'list'}),
-        ('message', {'create'}),
-        ('name', {'create'}),
-        ('patch', {'patch', 'list'}),
-        ('stat', {'stat', 'list'}),
-    ]
-    def checkopt(opt):
-        if opts.get(opt):
-            for i, allowable in allowables:
-                if opts[i] and opt not in allowable:
-                    raise error.Abort(_("options '--%s' and '--%s' may not be "
-                                       "used together") % (opt, i))
-            return True
-    if checkopt('cleanup'):
-        if pats:
-            raise error.Abort(_("cannot specify names when using '--cleanup'"))
-        return cleanupcmd(ui, repo)
-    elif checkopt('delete'):
-        return deletecmd(ui, repo, pats)
-    elif checkopt('list'):
-        return listcmd(ui, repo, pats, opts)
-    elif checkopt('patch') or checkopt('stat'):
-        return patchcmds(ui, repo, pats, opts)
-    else:
-        return createcmd(ui, repo, pats, opts)
-
-def extsetup(ui):
-    cmdutil.unfinishedstates.append(
-        [shelvedstate._filename, False, False,
-         _('unshelve already in progress'),
-         _("use 'hg unshelve --continue' or 'hg unshelve --abort'")])
-    cmdutil.afterresolvedstates.append(
-        [shelvedstate._filename, _('hg unshelve --continue')])



To: navaneeth.suresh, #hg-reviewers
Cc: mjpieters, mercurial-devel


More information about the Mercurial-devel mailing list