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