comparison mercurial/shelve.py @ 42541: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
comparison
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)