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