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