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