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