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