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