Mercurial > public > mercurial-scm > hg-stable
diff mercurial/shelve.py @ 42548:3de4f17f4824
shelve: move shelve extension to core
Until now, `shelve` was bootstrapped as an extension. This patch adds
`shelve` on core.
Differential Revision: https://phab.mercurial-scm.org/D6553
author | Navaneeth Suresh <navaneeths1998@gmail.com> |
---|---|
date | Fri, 28 Jun 2019 21:31:34 +0530 |
parents | hgext/shelve.py@80e0ea08b55c |
children | 70f1a84d0794 |
line wrap: on
line diff
--- /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)