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