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