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