mercurial/shelve.py
changeset 42541 3de4f17f4824
parent 42540 80e0ea08b55c
child 42560 70f1a84d0794
equal deleted inserted replaced
42540:80e0ea08b55c 42541:3de4f17f4824
       
     1 # shelve.py - save/restore working directory state
       
     2 #
       
     3 # Copyright 2013 Facebook, Inc.
       
     4 #
       
     5 # This software may be used and distributed according to the terms of the
       
     6 # GNU General Public License version 2 or any later version.
       
     7 
       
     8 """save and restore changes to the working directory
       
     9 
       
    10 The "hg shelve" command saves changes made to the working directory
       
    11 and reverts those changes, resetting the working directory to a clean
       
    12 state.
       
    13 
       
    14 Later on, the "hg unshelve" command restores the changes saved by "hg
       
    15 shelve". Changes can be restored even after updating to a different
       
    16 parent, in which case Mercurial's merge machinery will resolve any
       
    17 conflicts if necessary.
       
    18 
       
    19 You can have more than one shelved change outstanding at a time; each
       
    20 shelved change has a distinct name. For details, see the help for "hg
       
    21 shelve".
       
    22 """
       
    23 from __future__ import absolute_import
       
    24 
       
    25 import collections
       
    26 import errno
       
    27 import itertools
       
    28 import stat
       
    29 
       
    30 from .i18n import _
       
    31 from . import (
       
    32     bookmarks,
       
    33     bundle2,
       
    34     bundlerepo,
       
    35     changegroup,
       
    36     cmdutil,
       
    37     discovery,
       
    38     error,
       
    39     exchange,
       
    40     hg,
       
    41     lock as lockmod,
       
    42     mdiff,
       
    43     merge,
       
    44     node as nodemod,
       
    45     patch,
       
    46     phases,
       
    47     pycompat,
       
    48     repair,
       
    49     scmutil,
       
    50     templatefilters,
       
    51     util,
       
    52     vfs as vfsmod,
       
    53 )
       
    54 from .utils import (
       
    55     dateutil,
       
    56     stringutil,
       
    57 )
       
    58 
       
    59 backupdir = 'shelve-backup'
       
    60 shelvedir = 'shelved'
       
    61 shelvefileextensions = ['hg', 'patch', 'shelve']
       
    62 # universal extension is present in all types of shelves
       
    63 patchextension = 'patch'
       
    64 
       
    65 # we never need the user, so we use a
       
    66 # generic user for all shelve operations
       
    67 shelveuser = 'shelve@localhost'
       
    68 
       
    69 class shelvedfile(object):
       
    70     """Helper for the file storing a single shelve
       
    71 
       
    72     Handles common functions on shelve files (.hg/.patch) using
       
    73     the vfs layer"""
       
    74     def __init__(self, repo, name, filetype=None):
       
    75         self.repo = repo
       
    76         self.name = name
       
    77         self.vfs = vfsmod.vfs(repo.vfs.join(shelvedir))
       
    78         self.backupvfs = vfsmod.vfs(repo.vfs.join(backupdir))
       
    79         self.ui = self.repo.ui
       
    80         if filetype:
       
    81             self.fname = name + '.' + filetype
       
    82         else:
       
    83             self.fname = name
       
    84 
       
    85     def exists(self):
       
    86         return self.vfs.exists(self.fname)
       
    87 
       
    88     def filename(self):
       
    89         return self.vfs.join(self.fname)
       
    90 
       
    91     def backupfilename(self):
       
    92         def gennames(base):
       
    93             yield base
       
    94             base, ext = base.rsplit('.', 1)
       
    95             for i in itertools.count(1):
       
    96                 yield '%s-%d.%s' % (base, i, ext)
       
    97 
       
    98         name = self.backupvfs.join(self.fname)
       
    99         for n in gennames(name):
       
   100             if not self.backupvfs.exists(n):
       
   101                 return n
       
   102 
       
   103     def movetobackup(self):
       
   104         if not self.backupvfs.isdir():
       
   105             self.backupvfs.makedir()
       
   106         util.rename(self.filename(), self.backupfilename())
       
   107 
       
   108     def stat(self):
       
   109         return self.vfs.stat(self.fname)
       
   110 
       
   111     def opener(self, mode='rb'):
       
   112         try:
       
   113             return self.vfs(self.fname, mode)
       
   114         except IOError as err:
       
   115             if err.errno != errno.ENOENT:
       
   116                 raise
       
   117             raise error.Abort(_("shelved change '%s' not found") % self.name)
       
   118 
       
   119     def applybundle(self, tr):
       
   120         fp = self.opener()
       
   121         try:
       
   122             targetphase = phases.internal
       
   123             if not phases.supportinternal(self.repo):
       
   124                 targetphase = phases.secret
       
   125             gen = exchange.readbundle(self.repo.ui, fp, self.fname, self.vfs)
       
   126             pretip = self.repo['tip']
       
   127             bundle2.applybundle(self.repo, gen, tr,
       
   128                                 source='unshelve',
       
   129                                 url='bundle:' + self.vfs.join(self.fname),
       
   130                                 targetphase=targetphase)
       
   131             shelvectx = self.repo['tip']
       
   132             if pretip == shelvectx:
       
   133                 shelverev = tr.changes['revduplicates'][-1]
       
   134                 shelvectx = self.repo[shelverev]
       
   135             return shelvectx
       
   136         finally:
       
   137             fp.close()
       
   138 
       
   139     def bundlerepo(self):
       
   140         path = self.vfs.join(self.fname)
       
   141         return bundlerepo.instance(self.repo.baseui,
       
   142                                    'bundle://%s+%s' % (self.repo.root, path))
       
   143 
       
   144     def writebundle(self, bases, node):
       
   145         cgversion = changegroup.safeversion(self.repo)
       
   146         if cgversion == '01':
       
   147             btype = 'HG10BZ'
       
   148             compression = None
       
   149         else:
       
   150             btype = 'HG20'
       
   151             compression = 'BZ'
       
   152 
       
   153         repo = self.repo.unfiltered()
       
   154 
       
   155         outgoing = discovery.outgoing(repo, missingroots=bases,
       
   156                                       missingheads=[node])
       
   157         cg = changegroup.makechangegroup(repo, outgoing, cgversion, 'shelve')
       
   158 
       
   159         bundle2.writebundle(self.ui, cg, self.fname, btype, self.vfs,
       
   160                                 compression=compression)
       
   161 
       
   162     def writeinfo(self, info):
       
   163         scmutil.simplekeyvaluefile(self.vfs, self.fname).write(info)
       
   164 
       
   165     def readinfo(self):
       
   166         return scmutil.simplekeyvaluefile(self.vfs, self.fname).read()
       
   167 
       
   168 class shelvedstate(object):
       
   169     """Handle persistence during unshelving operations.
       
   170 
       
   171     Handles saving and restoring a shelved state. Ensures that different
       
   172     versions of a shelved state are possible and handles them appropriately.
       
   173     """
       
   174     _version = 2
       
   175     _filename = 'shelvedstate'
       
   176     _keep = 'keep'
       
   177     _nokeep = 'nokeep'
       
   178     # colon is essential to differentiate from a real bookmark name
       
   179     _noactivebook = ':no-active-bookmark'
       
   180 
       
   181     @classmethod
       
   182     def _verifyandtransform(cls, d):
       
   183         """Some basic shelvestate syntactic verification and transformation"""
       
   184         try:
       
   185             d['originalwctx'] = nodemod.bin(d['originalwctx'])
       
   186             d['pendingctx'] = nodemod.bin(d['pendingctx'])
       
   187             d['parents'] = [nodemod.bin(h)
       
   188                             for h in d['parents'].split(' ')]
       
   189             d['nodestoremove'] = [nodemod.bin(h)
       
   190                                   for h in d['nodestoremove'].split(' ')]
       
   191         except (ValueError, TypeError, KeyError) as err:
       
   192             raise error.CorruptedState(pycompat.bytestr(err))
       
   193 
       
   194     @classmethod
       
   195     def _getversion(cls, repo):
       
   196         """Read version information from shelvestate file"""
       
   197         fp = repo.vfs(cls._filename)
       
   198         try:
       
   199             version = int(fp.readline().strip())
       
   200         except ValueError as err:
       
   201             raise error.CorruptedState(pycompat.bytestr(err))
       
   202         finally:
       
   203             fp.close()
       
   204         return version
       
   205 
       
   206     @classmethod
       
   207     def _readold(cls, repo):
       
   208         """Read the old position-based version of a shelvestate file"""
       
   209         # Order is important, because old shelvestate file uses it
       
   210         # to detemine values of fields (i.g. name is on the second line,
       
   211         # originalwctx is on the third and so forth). Please do not change.
       
   212         keys = ['version', 'name', 'originalwctx', 'pendingctx', 'parents',
       
   213                 'nodestoremove', 'branchtorestore', 'keep', 'activebook']
       
   214         # this is executed only seldomly, so it is not a big deal
       
   215         # that we open this file twice
       
   216         fp = repo.vfs(cls._filename)
       
   217         d = {}
       
   218         try:
       
   219             for key in keys:
       
   220                 d[key] = fp.readline().strip()
       
   221         finally:
       
   222             fp.close()
       
   223         return d
       
   224 
       
   225     @classmethod
       
   226     def load(cls, repo):
       
   227         version = cls._getversion(repo)
       
   228         if version < cls._version:
       
   229             d = cls._readold(repo)
       
   230         elif version == cls._version:
       
   231             d = scmutil.simplekeyvaluefile(
       
   232                 repo.vfs, cls._filename).read(firstlinenonkeyval=True)
       
   233         else:
       
   234             raise error.Abort(_('this version of shelve is incompatible '
       
   235                                 'with the version used in this repo'))
       
   236 
       
   237         cls._verifyandtransform(d)
       
   238         try:
       
   239             obj = cls()
       
   240             obj.name = d['name']
       
   241             obj.wctx = repo[d['originalwctx']]
       
   242             obj.pendingctx = repo[d['pendingctx']]
       
   243             obj.parents = d['parents']
       
   244             obj.nodestoremove = d['nodestoremove']
       
   245             obj.branchtorestore = d.get('branchtorestore', '')
       
   246             obj.keep = d.get('keep') == cls._keep
       
   247             obj.activebookmark = ''
       
   248             if d.get('activebook', '') != cls._noactivebook:
       
   249                 obj.activebookmark = d.get('activebook', '')
       
   250         except (error.RepoLookupError, KeyError) as err:
       
   251             raise error.CorruptedState(pycompat.bytestr(err))
       
   252 
       
   253         return obj
       
   254 
       
   255     @classmethod
       
   256     def save(cls, repo, name, originalwctx, pendingctx, nodestoremove,
       
   257              branchtorestore, keep=False, activebook=''):
       
   258         info = {
       
   259             "name": name,
       
   260             "originalwctx": nodemod.hex(originalwctx.node()),
       
   261             "pendingctx": nodemod.hex(pendingctx.node()),
       
   262             "parents": ' '.join([nodemod.hex(p)
       
   263                                  for p in repo.dirstate.parents()]),
       
   264             "nodestoremove": ' '.join([nodemod.hex(n)
       
   265                                       for n in nodestoremove]),
       
   266             "branchtorestore": branchtorestore,
       
   267             "keep": cls._keep if keep else cls._nokeep,
       
   268             "activebook": activebook or cls._noactivebook
       
   269         }
       
   270         scmutil.simplekeyvaluefile(
       
   271             repo.vfs, cls._filename).write(info,
       
   272                                            firstline=("%d" % cls._version))
       
   273 
       
   274     @classmethod
       
   275     def clear(cls, repo):
       
   276         repo.vfs.unlinkpath(cls._filename, ignoremissing=True)
       
   277 
       
   278 def cleanupoldbackups(repo):
       
   279     vfs = vfsmod.vfs(repo.vfs.join(backupdir))
       
   280     maxbackups = repo.ui.configint('shelve', 'maxbackups')
       
   281     hgfiles = [f for f in vfs.listdir()
       
   282                if f.endswith('.' + patchextension)]
       
   283     hgfiles = sorted([(vfs.stat(f)[stat.ST_MTIME], f) for f in hgfiles])
       
   284     if maxbackups > 0 and maxbackups < len(hgfiles):
       
   285         bordermtime = hgfiles[-maxbackups][0]
       
   286     else:
       
   287         bordermtime = None
       
   288     for mtime, f in hgfiles[:len(hgfiles) - maxbackups]:
       
   289         if mtime == bordermtime:
       
   290             # keep it, because timestamp can't decide exact order of backups
       
   291             continue
       
   292         base = f[:-(1 + len(patchextension))]
       
   293         for ext in shelvefileextensions:
       
   294             vfs.tryunlink(base + '.' + ext)
       
   295 
       
   296 def _backupactivebookmark(repo):
       
   297     activebookmark = repo._activebookmark
       
   298     if activebookmark:
       
   299         bookmarks.deactivate(repo)
       
   300     return activebookmark
       
   301 
       
   302 def _restoreactivebookmark(repo, mark):
       
   303     if mark:
       
   304         bookmarks.activate(repo, mark)
       
   305 
       
   306 def _aborttransaction(repo, tr):
       
   307     '''Abort current transaction for shelve/unshelve, but keep dirstate
       
   308     '''
       
   309     dirstatebackupname = 'dirstate.shelve'
       
   310     repo.dirstate.savebackup(tr, dirstatebackupname)
       
   311     tr.abort()
       
   312     repo.dirstate.restorebackup(None, dirstatebackupname)
       
   313 
       
   314 def getshelvename(repo, parent, opts):
       
   315     """Decide on the name this shelve is going to have"""
       
   316     def gennames():
       
   317         yield label
       
   318         for i in itertools.count(1):
       
   319             yield '%s-%02d' % (label, i)
       
   320     name = opts.get('name')
       
   321     label = repo._activebookmark or parent.branch() or 'default'
       
   322     # slashes aren't allowed in filenames, therefore we rename it
       
   323     label = label.replace('/', '_')
       
   324     label = label.replace('\\', '_')
       
   325     # filenames must not start with '.' as it should not be hidden
       
   326     if label.startswith('.'):
       
   327         label = label.replace('.', '_', 1)
       
   328 
       
   329     if name:
       
   330         if shelvedfile(repo, name, patchextension).exists():
       
   331             e = _("a shelved change named '%s' already exists") % name
       
   332             raise error.Abort(e)
       
   333 
       
   334         # ensure we are not creating a subdirectory or a hidden file
       
   335         if '/' in name or '\\' in name:
       
   336             raise error.Abort(_('shelved change names can not contain slashes'))
       
   337         if name.startswith('.'):
       
   338             raise error.Abort(_("shelved change names can not start with '.'"))
       
   339 
       
   340     else:
       
   341         for n in gennames():
       
   342             if not shelvedfile(repo, n, patchextension).exists():
       
   343                 name = n
       
   344                 break
       
   345 
       
   346     return name
       
   347 
       
   348 def mutableancestors(ctx):
       
   349     """return all mutable ancestors for ctx (included)
       
   350 
       
   351     Much faster than the revset ancestors(ctx) & draft()"""
       
   352     seen = {nodemod.nullrev}
       
   353     visit = collections.deque()
       
   354     visit.append(ctx)
       
   355     while visit:
       
   356         ctx = visit.popleft()
       
   357         yield ctx.node()
       
   358         for parent in ctx.parents():
       
   359             rev = parent.rev()
       
   360             if rev not in seen:
       
   361                 seen.add(rev)
       
   362                 if parent.mutable():
       
   363                     visit.append(parent)
       
   364 
       
   365 def getcommitfunc(extra, interactive, editor=False):
       
   366     def commitfunc(ui, repo, message, match, opts):
       
   367         hasmq = util.safehasattr(repo, 'mq')
       
   368         if hasmq:
       
   369             saved, repo.mq.checkapplied = repo.mq.checkapplied, False
       
   370 
       
   371         targetphase = phases.internal
       
   372         if not phases.supportinternal(repo):
       
   373             targetphase = phases.secret
       
   374         overrides = {('phases', 'new-commit'): targetphase}
       
   375         try:
       
   376             editor_ = False
       
   377             if editor:
       
   378                 editor_ = cmdutil.getcommiteditor(editform='shelve.shelve',
       
   379                                                   **pycompat.strkwargs(opts))
       
   380             with repo.ui.configoverride(overrides):
       
   381                 return repo.commit(message, shelveuser, opts.get('date'),
       
   382                                    match, editor=editor_, extra=extra)
       
   383         finally:
       
   384             if hasmq:
       
   385                 repo.mq.checkapplied = saved
       
   386 
       
   387     def interactivecommitfunc(ui, repo, *pats, **opts):
       
   388         opts = pycompat.byteskwargs(opts)
       
   389         match = scmutil.match(repo['.'], pats, {})
       
   390         message = opts['message']
       
   391         return commitfunc(ui, repo, message, match, opts)
       
   392 
       
   393     return interactivecommitfunc if interactive else commitfunc
       
   394 
       
   395 def _nothingtoshelvemessaging(ui, repo, pats, opts):
       
   396     stat = repo.status(match=scmutil.match(repo[None], pats, opts))
       
   397     if stat.deleted:
       
   398         ui.status(_("nothing changed (%d missing files, see "
       
   399                     "'hg status')\n") % len(stat.deleted))
       
   400     else:
       
   401         ui.status(_("nothing changed\n"))
       
   402 
       
   403 def _shelvecreatedcommit(repo, node, name, match):
       
   404     info = {'node': nodemod.hex(node)}
       
   405     shelvedfile(repo, name, 'shelve').writeinfo(info)
       
   406     bases = list(mutableancestors(repo[node]))
       
   407     shelvedfile(repo, name, 'hg').writebundle(bases, node)
       
   408     with shelvedfile(repo, name, patchextension).opener('wb') as fp:
       
   409         cmdutil.exportfile(repo, [node], fp, opts=mdiff.diffopts(git=True),
       
   410                            match=match)
       
   411 
       
   412 def _includeunknownfiles(repo, pats, opts, extra):
       
   413     s = repo.status(match=scmutil.match(repo[None], pats, opts),
       
   414                     unknown=True)
       
   415     if s.unknown:
       
   416         extra['shelve_unknown'] = '\0'.join(s.unknown)
       
   417         repo[None].add(s.unknown)
       
   418 
       
   419 def _finishshelve(repo, tr):
       
   420     if phases.supportinternal(repo):
       
   421         tr.close()
       
   422     else:
       
   423         _aborttransaction(repo, tr)
       
   424 
       
   425 def createcmd(ui, repo, pats, opts):
       
   426     """subcommand that creates a new shelve"""
       
   427     with repo.wlock():
       
   428         cmdutil.checkunfinished(repo)
       
   429         return _docreatecmd(ui, repo, pats, opts)
       
   430 
       
   431 def _docreatecmd(ui, repo, pats, opts):
       
   432     wctx = repo[None]
       
   433     parents = wctx.parents()
       
   434     parent = parents[0]
       
   435     origbranch = wctx.branch()
       
   436 
       
   437     if parent.node() != nodemod.nullid:
       
   438         desc = "changes to: %s" % parent.description().split('\n', 1)[0]
       
   439     else:
       
   440         desc = '(changes in empty repository)'
       
   441 
       
   442     if not opts.get('message'):
       
   443         opts['message'] = desc
       
   444 
       
   445     lock = tr = activebookmark = None
       
   446     try:
       
   447         lock = repo.lock()
       
   448 
       
   449         # use an uncommitted transaction to generate the bundle to avoid
       
   450         # pull races. ensure we don't print the abort message to stderr.
       
   451         tr = repo.transaction('shelve', report=lambda x: None)
       
   452 
       
   453         interactive = opts.get('interactive', False)
       
   454         includeunknown = (opts.get('unknown', False) and
       
   455                           not opts.get('addremove', False))
       
   456 
       
   457         name = getshelvename(repo, parent, opts)
       
   458         activebookmark = _backupactivebookmark(repo)
       
   459         extra = {'internal': 'shelve'}
       
   460         if includeunknown:
       
   461             _includeunknownfiles(repo, pats, opts, extra)
       
   462 
       
   463         if _iswctxonnewbranch(repo) and not _isbareshelve(pats, opts):
       
   464             # In non-bare shelve we don't store newly created branch
       
   465             # at bundled commit
       
   466             repo.dirstate.setbranch(repo['.'].branch())
       
   467 
       
   468         commitfunc = getcommitfunc(extra, interactive, editor=True)
       
   469         if not interactive:
       
   470             node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
       
   471         else:
       
   472             node = cmdutil.dorecord(ui, repo, commitfunc, None,
       
   473                                     False, cmdutil.recordfilter, *pats,
       
   474                                     **pycompat.strkwargs(opts))
       
   475         if not node:
       
   476             _nothingtoshelvemessaging(ui, repo, pats, opts)
       
   477             return 1
       
   478 
       
   479         # Create a matcher so that prefetch doesn't attempt to fetch
       
   480         # the entire repository pointlessly, and as an optimisation
       
   481         # for movedirstate, if needed.
       
   482         match = scmutil.matchfiles(repo, repo[node].files())
       
   483         _shelvecreatedcommit(repo, node, name, match)
       
   484 
       
   485         if ui.formatted():
       
   486             desc = stringutil.ellipsis(desc, ui.termwidth())
       
   487         ui.status(_('shelved as %s\n') % name)
       
   488         if opts['keep']:
       
   489             with repo.dirstate.parentchange():
       
   490                 scmutil.movedirstate(repo, parent, match)
       
   491         else:
       
   492             hg.update(repo, parent.node())
       
   493         if origbranch != repo['.'].branch() and not _isbareshelve(pats, opts):
       
   494             repo.dirstate.setbranch(origbranch)
       
   495 
       
   496         _finishshelve(repo, tr)
       
   497     finally:
       
   498         _restoreactivebookmark(repo, activebookmark)
       
   499         lockmod.release(tr, lock)
       
   500 
       
   501 def _isbareshelve(pats, opts):
       
   502     return (not pats
       
   503             and not opts.get('interactive', False)
       
   504             and not opts.get('include', False)
       
   505             and not opts.get('exclude', False))
       
   506 
       
   507 def _iswctxonnewbranch(repo):
       
   508     return repo[None].branch() != repo['.'].branch()
       
   509 
       
   510 def cleanupcmd(ui, repo):
       
   511     """subcommand that deletes all shelves"""
       
   512 
       
   513     with repo.wlock():
       
   514         for (name, _type) in repo.vfs.readdir(shelvedir):
       
   515             suffix = name.rsplit('.', 1)[-1]
       
   516             if suffix in shelvefileextensions:
       
   517                 shelvedfile(repo, name).movetobackup()
       
   518             cleanupoldbackups(repo)
       
   519 
       
   520 def deletecmd(ui, repo, pats):
       
   521     """subcommand that deletes a specific shelve"""
       
   522     if not pats:
       
   523         raise error.Abort(_('no shelved changes specified!'))
       
   524     with repo.wlock():
       
   525         try:
       
   526             for name in pats:
       
   527                 for suffix in shelvefileextensions:
       
   528                     shfile = shelvedfile(repo, name, suffix)
       
   529                     # patch file is necessary, as it should
       
   530                     # be present for any kind of shelve,
       
   531                     # but the .hg file is optional as in future we
       
   532                     # will add obsolete shelve with does not create a
       
   533                     # bundle
       
   534                     if shfile.exists() or suffix == patchextension:
       
   535                         shfile.movetobackup()
       
   536             cleanupoldbackups(repo)
       
   537         except OSError as err:
       
   538             if err.errno != errno.ENOENT:
       
   539                 raise
       
   540             raise error.Abort(_("shelved change '%s' not found") % name)
       
   541 
       
   542 def listshelves(repo):
       
   543     """return all shelves in repo as list of (time, filename)"""
       
   544     try:
       
   545         names = repo.vfs.readdir(shelvedir)
       
   546     except OSError as err:
       
   547         if err.errno != errno.ENOENT:
       
   548             raise
       
   549         return []
       
   550     info = []
       
   551     for (name, _type) in names:
       
   552         pfx, sfx = name.rsplit('.', 1)
       
   553         if not pfx or sfx != patchextension:
       
   554             continue
       
   555         st = shelvedfile(repo, name).stat()
       
   556         info.append((st[stat.ST_MTIME], shelvedfile(repo, pfx).filename()))
       
   557     return sorted(info, reverse=True)
       
   558 
       
   559 def listcmd(ui, repo, pats, opts):
       
   560     """subcommand that displays the list of shelves"""
       
   561     pats = set(pats)
       
   562     width = 80
       
   563     if not ui.plain():
       
   564         width = ui.termwidth()
       
   565     namelabel = 'shelve.newest'
       
   566     ui.pager('shelve')
       
   567     for mtime, name in listshelves(repo):
       
   568         sname = util.split(name)[1]
       
   569         if pats and sname not in pats:
       
   570             continue
       
   571         ui.write(sname, label=namelabel)
       
   572         namelabel = 'shelve.name'
       
   573         if ui.quiet:
       
   574             ui.write('\n')
       
   575             continue
       
   576         ui.write(' ' * (16 - len(sname)))
       
   577         used = 16
       
   578         date = dateutil.makedate(mtime)
       
   579         age = '(%s)' % templatefilters.age(date, abbrev=True)
       
   580         ui.write(age, label='shelve.age')
       
   581         ui.write(' ' * (12 - len(age)))
       
   582         used += 12
       
   583         with open(name + '.' + patchextension, 'rb') as fp:
       
   584             while True:
       
   585                 line = fp.readline()
       
   586                 if not line:
       
   587                     break
       
   588                 if not line.startswith('#'):
       
   589                     desc = line.rstrip()
       
   590                     if ui.formatted():
       
   591                         desc = stringutil.ellipsis(desc, width - used)
       
   592                     ui.write(desc)
       
   593                     break
       
   594             ui.write('\n')
       
   595             if not (opts['patch'] or opts['stat']):
       
   596                 continue
       
   597             difflines = fp.readlines()
       
   598             if opts['patch']:
       
   599                 for chunk, label in patch.difflabel(iter, difflines):
       
   600                     ui.write(chunk, label=label)
       
   601             if opts['stat']:
       
   602                 for chunk, label in patch.diffstatui(difflines, width=width):
       
   603                     ui.write(chunk, label=label)
       
   604 
       
   605 def patchcmds(ui, repo, pats, opts):
       
   606     """subcommand that displays shelves"""
       
   607     if len(pats) == 0:
       
   608         shelves = listshelves(repo)
       
   609         if not shelves:
       
   610             raise error.Abort(_("there are no shelves to show"))
       
   611         mtime, name = shelves[0]
       
   612         sname = util.split(name)[1]
       
   613         pats = [sname]
       
   614 
       
   615     for shelfname in pats:
       
   616         if not shelvedfile(repo, shelfname, patchextension).exists():
       
   617             raise error.Abort(_("cannot find shelf %s") % shelfname)
       
   618 
       
   619     listcmd(ui, repo, pats, opts)
       
   620 
       
   621 def checkparents(repo, state):
       
   622     """check parent while resuming an unshelve"""
       
   623     if state.parents != repo.dirstate.parents():
       
   624         raise error.Abort(_('working directory parents do not match unshelve '
       
   625                            'state'))
       
   626 
       
   627 def unshelveabort(ui, repo, state, opts):
       
   628     """subcommand that abort an in-progress unshelve"""
       
   629     with repo.lock():
       
   630         try:
       
   631             checkparents(repo, state)
       
   632 
       
   633             merge.update(repo, state.pendingctx, branchmerge=False, force=True)
       
   634             if (state.activebookmark
       
   635                     and state.activebookmark in repo._bookmarks):
       
   636                 bookmarks.activate(repo, state.activebookmark)
       
   637             mergefiles(ui, repo, state.wctx, state.pendingctx)
       
   638             if not phases.supportinternal(repo):
       
   639                 repair.strip(ui, repo, state.nodestoremove, backup=False,
       
   640                              topic='shelve')
       
   641         finally:
       
   642             shelvedstate.clear(repo)
       
   643             ui.warn(_("unshelve of '%s' aborted\n") % state.name)
       
   644 
       
   645 def mergefiles(ui, repo, wctx, shelvectx):
       
   646     """updates to wctx and merges the changes from shelvectx into the
       
   647     dirstate."""
       
   648     with ui.configoverride({('ui', 'quiet'): True}):
       
   649         hg.update(repo, wctx.node())
       
   650         ui.pushbuffer(True)
       
   651         cmdutil.revert(ui, repo, shelvectx, repo.dirstate.parents())
       
   652         ui.popbuffer()
       
   653 
       
   654 def restorebranch(ui, repo, branchtorestore):
       
   655     if branchtorestore and branchtorestore != repo.dirstate.branch():
       
   656         repo.dirstate.setbranch(branchtorestore)
       
   657         ui.status(_('marked working directory as branch %s\n')
       
   658                   % branchtorestore)
       
   659 
       
   660 def unshelvecleanup(ui, repo, name, opts):
       
   661     """remove related files after an unshelve"""
       
   662     if not opts.get('keep'):
       
   663         for filetype in shelvefileextensions:
       
   664             shfile = shelvedfile(repo, name, filetype)
       
   665             if shfile.exists():
       
   666                 shfile.movetobackup()
       
   667         cleanupoldbackups(repo)
       
   668 
       
   669 def unshelvecontinue(ui, repo, state, opts):
       
   670     """subcommand to continue an in-progress unshelve"""
       
   671     # We're finishing off a merge. First parent is our original
       
   672     # parent, second is the temporary "fake" commit we're unshelving.
       
   673     with repo.lock():
       
   674         checkparents(repo, state)
       
   675         ms = merge.mergestate.read(repo)
       
   676         if list(ms.unresolved()):
       
   677             raise error.Abort(
       
   678                 _("unresolved conflicts, can't continue"),
       
   679                 hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
       
   680 
       
   681         shelvectx = repo[state.parents[1]]
       
   682         pendingctx = state.pendingctx
       
   683 
       
   684         with repo.dirstate.parentchange():
       
   685             repo.setparents(state.pendingctx.node(), nodemod.nullid)
       
   686             repo.dirstate.write(repo.currenttransaction())
       
   687 
       
   688         targetphase = phases.internal
       
   689         if not phases.supportinternal(repo):
       
   690             targetphase = phases.secret
       
   691         overrides = {('phases', 'new-commit'): targetphase}
       
   692         with repo.ui.configoverride(overrides, 'unshelve'):
       
   693             with repo.dirstate.parentchange():
       
   694                 repo.setparents(state.parents[0], nodemod.nullid)
       
   695                 newnode = repo.commit(text=shelvectx.description(),
       
   696                                       extra=shelvectx.extra(),
       
   697                                       user=shelvectx.user(),
       
   698                                       date=shelvectx.date())
       
   699 
       
   700         if newnode is None:
       
   701             # If it ended up being a no-op commit, then the normal
       
   702             # merge state clean-up path doesn't happen, so do it
       
   703             # here. Fix issue5494
       
   704             merge.mergestate.clean(repo)
       
   705             shelvectx = state.pendingctx
       
   706             msg = _('note: unshelved changes already existed '
       
   707                     'in the working copy\n')
       
   708             ui.status(msg)
       
   709         else:
       
   710             # only strip the shelvectx if we produced one
       
   711             state.nodestoremove.append(newnode)
       
   712             shelvectx = repo[newnode]
       
   713 
       
   714         hg.updaterepo(repo, pendingctx.node(), overwrite=False)
       
   715         mergefiles(ui, repo, state.wctx, shelvectx)
       
   716         restorebranch(ui, repo, state.branchtorestore)
       
   717 
       
   718         if not phases.supportinternal(repo):
       
   719             repair.strip(ui, repo, state.nodestoremove, backup=False,
       
   720                          topic='shelve')
       
   721         _restoreactivebookmark(repo, state.activebookmark)
       
   722         shelvedstate.clear(repo)
       
   723         unshelvecleanup(ui, repo, state.name, opts)
       
   724         ui.status(_("unshelve of '%s' complete\n") % state.name)
       
   725 
       
   726 def _commitworkingcopychanges(ui, repo, opts, tmpwctx):
       
   727     """Temporarily commit working copy changes before moving unshelve commit"""
       
   728     # Store pending changes in a commit and remember added in case a shelve
       
   729     # contains unknown files that are part of the pending change
       
   730     s = repo.status()
       
   731     addedbefore = frozenset(s.added)
       
   732     if not (s.modified or s.added or s.removed):
       
   733         return tmpwctx, addedbefore
       
   734     ui.status(_("temporarily committing pending changes "
       
   735                 "(restore with 'hg unshelve --abort')\n"))
       
   736     extra = {'internal': 'shelve'}
       
   737     commitfunc = getcommitfunc(extra=extra, interactive=False,
       
   738                                editor=False)
       
   739     tempopts = {}
       
   740     tempopts['message'] = "pending changes temporary commit"
       
   741     tempopts['date'] = opts.get('date')
       
   742     with ui.configoverride({('ui', 'quiet'): True}):
       
   743         node = cmdutil.commit(ui, repo, commitfunc, [], tempopts)
       
   744     tmpwctx = repo[node]
       
   745     return tmpwctx, addedbefore
       
   746 
       
   747 def _unshelverestorecommit(ui, repo, tr, basename):
       
   748     """Recreate commit in the repository during the unshelve"""
       
   749     repo = repo.unfiltered()
       
   750     node = None
       
   751     if shelvedfile(repo, basename, 'shelve').exists():
       
   752         node = shelvedfile(repo, basename, 'shelve').readinfo()['node']
       
   753     if node is None or node not in repo:
       
   754         with ui.configoverride({('ui', 'quiet'): True}):
       
   755             shelvectx = shelvedfile(repo, basename, 'hg').applybundle(tr)
       
   756         # We might not strip the unbundled changeset, so we should keep track of
       
   757         # the unshelve node in case we need to reuse it (eg: unshelve --keep)
       
   758         if node is None:
       
   759             info = {'node': nodemod.hex(shelvectx.node())}
       
   760             shelvedfile(repo, basename, 'shelve').writeinfo(info)
       
   761     else:
       
   762         shelvectx = repo[node]
       
   763 
       
   764     return repo, shelvectx
       
   765 
       
   766 def _rebaserestoredcommit(ui, repo, opts, tr, oldtiprev, basename, pctx,
       
   767                           tmpwctx, shelvectx, branchtorestore,
       
   768                           activebookmark):
       
   769     """Rebase restored commit from its original location to a destination"""
       
   770     # If the shelve is not immediately on top of the commit
       
   771     # we'll be merging with, rebase it to be on top.
       
   772     if tmpwctx.node() == shelvectx.p1().node():
       
   773         return shelvectx
       
   774 
       
   775     overrides = {
       
   776         ('ui', 'forcemerge'): opts.get('tool', ''),
       
   777         ('phases', 'new-commit'): phases.secret,
       
   778     }
       
   779     with repo.ui.configoverride(overrides, 'unshelve'):
       
   780         ui.status(_('rebasing shelved changes\n'))
       
   781         stats = merge.graft(repo, shelvectx, shelvectx.p1(),
       
   782                            labels=['shelve', 'working-copy'],
       
   783                            keepconflictparent=True)
       
   784         if stats.unresolvedcount:
       
   785             tr.close()
       
   786 
       
   787             nodestoremove = [repo.changelog.node(rev)
       
   788                              for rev in pycompat.xrange(oldtiprev, len(repo))]
       
   789             shelvedstate.save(repo, basename, pctx, tmpwctx, nodestoremove,
       
   790                               branchtorestore, opts.get('keep'), activebookmark)
       
   791             raise error.InterventionRequired(
       
   792                 _("unresolved conflicts (see 'hg resolve', then "
       
   793                   "'hg unshelve --continue')"))
       
   794 
       
   795         with repo.dirstate.parentchange():
       
   796             repo.setparents(tmpwctx.node(), nodemod.nullid)
       
   797             newnode = repo.commit(text=shelvectx.description(),
       
   798                                   extra=shelvectx.extra(),
       
   799                                   user=shelvectx.user(),
       
   800                                   date=shelvectx.date())
       
   801 
       
   802         if newnode is None:
       
   803             # If it ended up being a no-op commit, then the normal
       
   804             # merge state clean-up path doesn't happen, so do it
       
   805             # here. Fix issue5494
       
   806             merge.mergestate.clean(repo)
       
   807             shelvectx = tmpwctx
       
   808             msg = _('note: unshelved changes already existed '
       
   809                     'in the working copy\n')
       
   810             ui.status(msg)
       
   811         else:
       
   812             shelvectx = repo[newnode]
       
   813             hg.updaterepo(repo, tmpwctx.node(), False)
       
   814 
       
   815     return shelvectx
       
   816 
       
   817 def _forgetunknownfiles(repo, shelvectx, addedbefore):
       
   818     # Forget any files that were unknown before the shelve, unknown before
       
   819     # unshelve started, but are now added.
       
   820     shelveunknown = shelvectx.extra().get('shelve_unknown')
       
   821     if not shelveunknown:
       
   822         return
       
   823     shelveunknown = frozenset(shelveunknown.split('\0'))
       
   824     addedafter = frozenset(repo.status().added)
       
   825     toforget = (addedafter & shelveunknown) - addedbefore
       
   826     repo[None].forget(toforget)
       
   827 
       
   828 def _finishunshelve(repo, oldtiprev, tr, activebookmark):
       
   829     _restoreactivebookmark(repo, activebookmark)
       
   830     # The transaction aborting will strip all the commits for us,
       
   831     # but it doesn't update the inmemory structures, so addchangegroup
       
   832     # hooks still fire and try to operate on the missing commits.
       
   833     # Clean up manually to prevent this.
       
   834     repo.unfiltered().changelog.strip(oldtiprev, tr)
       
   835     _aborttransaction(repo, tr)
       
   836 
       
   837 def _checkunshelveuntrackedproblems(ui, repo, shelvectx):
       
   838     """Check potential problems which may result from working
       
   839     copy having untracked changes."""
       
   840     wcdeleted = set(repo.status().deleted)
       
   841     shelvetouched = set(shelvectx.files())
       
   842     intersection = wcdeleted.intersection(shelvetouched)
       
   843     if intersection:
       
   844         m = _("shelved change touches missing files")
       
   845         hint = _("run hg status to see which files are missing")
       
   846         raise error.Abort(m, hint=hint)
       
   847 
       
   848 def _dounshelve(ui, repo, *shelved, **opts):
       
   849     opts = pycompat.byteskwargs(opts)
       
   850     abortf = opts.get('abort')
       
   851     continuef = opts.get('continue')
       
   852     if not abortf and not continuef:
       
   853         cmdutil.checkunfinished(repo)
       
   854     shelved = list(shelved)
       
   855     if opts.get("name"):
       
   856         shelved.append(opts["name"])
       
   857 
       
   858     if abortf or continuef:
       
   859         if abortf and continuef:
       
   860             raise error.Abort(_('cannot use both abort and continue'))
       
   861         if shelved:
       
   862             raise error.Abort(_('cannot combine abort/continue with '
       
   863                                'naming a shelved change'))
       
   864         if abortf and opts.get('tool', False):
       
   865             ui.warn(_('tool option will be ignored\n'))
       
   866 
       
   867         try:
       
   868             state = shelvedstate.load(repo)
       
   869             if opts.get('keep') is None:
       
   870                 opts['keep'] = state.keep
       
   871         except IOError as err:
       
   872             if err.errno != errno.ENOENT:
       
   873                 raise
       
   874             cmdutil.wrongtooltocontinue(repo, _('unshelve'))
       
   875         except error.CorruptedState as err:
       
   876             ui.debug(pycompat.bytestr(err) + '\n')
       
   877             if continuef:
       
   878                 msg = _('corrupted shelved state file')
       
   879                 hint = _('please run hg unshelve --abort to abort unshelve '
       
   880                          'operation')
       
   881                 raise error.Abort(msg, hint=hint)
       
   882             elif abortf:
       
   883                 msg = _('could not read shelved state file, your working copy '
       
   884                         'may be in an unexpected state\nplease update to some '
       
   885                         'commit\n')
       
   886                 ui.warn(msg)
       
   887                 shelvedstate.clear(repo)
       
   888             return
       
   889 
       
   890         if abortf:
       
   891             return unshelveabort(ui, repo, state, opts)
       
   892         elif continuef:
       
   893             return unshelvecontinue(ui, repo, state, opts)
       
   894     elif len(shelved) > 1:
       
   895         raise error.Abort(_('can only unshelve one change at a time'))
       
   896     elif not shelved:
       
   897         shelved = listshelves(repo)
       
   898         if not shelved:
       
   899             raise error.Abort(_('no shelved changes to apply!'))
       
   900         basename = util.split(shelved[0][1])[1]
       
   901         ui.status(_("unshelving change '%s'\n") % basename)
       
   902     else:
       
   903         basename = shelved[0]
       
   904 
       
   905     if not shelvedfile(repo, basename, patchextension).exists():
       
   906         raise error.Abort(_("shelved change '%s' not found") % basename)
       
   907 
       
   908     repo = repo.unfiltered()
       
   909     lock = tr = None
       
   910     try:
       
   911         lock = repo.lock()
       
   912         tr = repo.transaction('unshelve', report=lambda x: None)
       
   913         oldtiprev = len(repo)
       
   914 
       
   915         pctx = repo['.']
       
   916         tmpwctx = pctx
       
   917         # The goal is to have a commit structure like so:
       
   918         # ...-> pctx -> tmpwctx -> shelvectx
       
   919         # where tmpwctx is an optional commit with the user's pending changes
       
   920         # and shelvectx is the unshelved changes. Then we merge it all down
       
   921         # to the original pctx.
       
   922 
       
   923         activebookmark = _backupactivebookmark(repo)
       
   924         tmpwctx, addedbefore = _commitworkingcopychanges(ui, repo, opts,
       
   925                                                          tmpwctx)
       
   926         repo, shelvectx = _unshelverestorecommit(ui, repo, tr, basename)
       
   927         _checkunshelveuntrackedproblems(ui, repo, shelvectx)
       
   928         branchtorestore = ''
       
   929         if shelvectx.branch() != shelvectx.p1().branch():
       
   930             branchtorestore = shelvectx.branch()
       
   931 
       
   932         shelvectx = _rebaserestoredcommit(ui, repo, opts, tr, oldtiprev,
       
   933                                           basename, pctx, tmpwctx,
       
   934                                           shelvectx, branchtorestore,
       
   935                                           activebookmark)
       
   936         overrides = {('ui', 'forcemerge'): opts.get('tool', '')}
       
   937         with ui.configoverride(overrides, 'unshelve'):
       
   938             mergefiles(ui, repo, pctx, shelvectx)
       
   939         restorebranch(ui, repo, branchtorestore)
       
   940         _forgetunknownfiles(repo, shelvectx, addedbefore)
       
   941 
       
   942         shelvedstate.clear(repo)
       
   943         _finishunshelve(repo, oldtiprev, tr, activebookmark)
       
   944         unshelvecleanup(ui, repo, basename, opts)
       
   945     finally:
       
   946         if tr:
       
   947             tr.release()
       
   948         lockmod.release(lock)