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