Mercurial > public > mercurial-scm > hg
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) |