mercurial/shelve.py
changeset 42541 3de4f17f4824
parent 42540 80e0ea08b55c
child 42560 70f1a84d0794
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/shelve.py	Fri Jun 28 21:31:34 2019 +0530
@@ -0,0 +1,948 @@
+# 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,
+    repair,
+    scmutil,
+    templatefilters,
+    util,
+    vfs as vfsmod,
+)
+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@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()
+    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)
+            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)
+        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'))
+    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)