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