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