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