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