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