--- a/hgext/shelve.py Fri Jun 28 22:57:48 2019 +0530
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1126 +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,
- state as statemod,
- templatefilters,
- util,
- vfs as vfsmod,
-)
-
-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@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)
-
-@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'))
- 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)
-
-@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):
- statemod.addunfinished(
- 'unshelve', fname=shelvedstate._filename, continueflag=True,
- cmdmsg=_('unshelve already in progress')
- )
-