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