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