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