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