##// END OF EJS Templates
shelve: move method for writing bundle to new shelf class...
Martin von Zweigbergk -
r46994:eb7b2929 default
parent child Browse files
Show More
@@ -1,1183 +1,1188 b''
1 1 # shelve.py - save/restore working directory state
2 2 #
3 3 # Copyright 2013 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """save and restore changes to the working directory
9 9
10 10 The "hg shelve" command saves changes made to the working directory
11 11 and reverts those changes, resetting the working directory to a clean
12 12 state.
13 13
14 14 Later on, the "hg unshelve" command restores the changes saved by "hg
15 15 shelve". Changes can be restored even after updating to a different
16 16 parent, in which case Mercurial's merge machinery will resolve any
17 17 conflicts if necessary.
18 18
19 19 You can have more than one shelved change outstanding at a time; each
20 20 shelved change has a distinct name. For details, see the help for "hg
21 21 shelve".
22 22 """
23 23 from __future__ import absolute_import
24 24
25 25 import collections
26 26 import errno
27 27 import itertools
28 28 import stat
29 29
30 30 from .i18n import _
31 31 from .pycompat import open
32 32 from .node import (
33 33 bin,
34 34 hex,
35 35 nullid,
36 36 nullrev,
37 37 )
38 38 from . import (
39 39 bookmarks,
40 40 bundle2,
41 41 changegroup,
42 42 cmdutil,
43 43 discovery,
44 44 error,
45 45 exchange,
46 46 hg,
47 47 lock as lockmod,
48 48 mdiff,
49 49 merge,
50 50 mergestate as mergestatemod,
51 51 patch,
52 52 phases,
53 53 pycompat,
54 54 repair,
55 55 scmutil,
56 56 templatefilters,
57 57 util,
58 58 vfs as vfsmod,
59 59 )
60 60 from .utils import (
61 61 dateutil,
62 62 stringutil,
63 63 )
64 64
65 65 backupdir = b'shelve-backup'
66 66 shelvedir = b'shelved'
67 67 shelvefileextensions = [b'hg', b'patch', b'shelve']
68 68 # universal extension is present in all types of shelves
69 69 patchextension = b'patch'
70 70
71 71 # we never need the user, so we use a
72 72 # generic user for all shelve operations
73 73 shelveuser = b'shelve@localhost'
74 74
75 75
76 76 class shelvedfile(object):
77 77 """Helper for the file storing a single shelve
78 78
79 79 Handles common functions on shelve files (.hg/.patch) using
80 80 the vfs layer"""
81 81
82 82 def __init__(self, repo, name, filetype=None):
83 83 self.repo = repo
84 84 self.name = name
85 85 self.vfs = vfsmod.vfs(repo.vfs.join(shelvedir))
86 86 self.backupvfs = vfsmod.vfs(repo.vfs.join(backupdir))
87 self.ui = self.repo.ui
88 87 if filetype:
89 88 self.fname = name + b'.' + filetype
90 89 else:
91 90 self.fname = name
92 91
93 92 def exists(self):
94 93 return self.vfs.exists(self.fname)
95 94
96 95 def filename(self):
97 96 return self.vfs.join(self.fname)
98 97
99 98 def backupfilename(self):
100 99 def gennames(base):
101 100 yield base
102 101 base, ext = base.rsplit(b'.', 1)
103 102 for i in itertools.count(1):
104 103 yield b'%s-%d.%s' % (base, i, ext)
105 104
106 105 name = self.backupvfs.join(self.fname)
107 106 for n in gennames(name):
108 107 if not self.backupvfs.exists(n):
109 108 return n
110 109
111 110 def movetobackup(self):
112 111 if not self.backupvfs.isdir():
113 112 self.backupvfs.makedir()
114 113 util.rename(self.filename(), self.backupfilename())
115 114
116 115 def stat(self):
117 116 return self.vfs.stat(self.fname)
118 117
119 118 def opener(self, mode=b'rb'):
120 119 return self.vfs(self.fname, mode)
121 120
122 121 def applybundle(self, tr):
123 122 fp = self.opener()
124 123 try:
125 124 targetphase = phases.internal
126 125 if not phases.supportinternal(self.repo):
127 126 targetphase = phases.secret
128 127 gen = exchange.readbundle(self.repo.ui, fp, self.fname, self.vfs)
129 128 pretip = self.repo[b'tip']
130 129 bundle2.applybundle(
131 130 self.repo,
132 131 gen,
133 132 tr,
134 133 source=b'unshelve',
135 134 url=b'bundle:' + self.vfs.join(self.fname),
136 135 targetphase=targetphase,
137 136 )
138 137 shelvectx = self.repo[b'tip']
139 138 if pretip == shelvectx:
140 139 shelverev = tr.changes[b'revduplicates'][-1]
141 140 shelvectx = self.repo[shelverev]
142 141 return shelvectx
143 142 finally:
144 143 fp.close()
145 144
146 def writebundle(self, bases, node):
147 cgversion = changegroup.safeversion(self.repo)
148 if cgversion == b'01':
149 btype = b'HG10BZ'
150 compression = None
151 else:
152 btype = b'HG20'
153 compression = b'BZ'
154
155 repo = self.repo.unfiltered()
156
157 outgoing = discovery.outgoing(
158 repo, missingroots=bases, ancestorsof=[node]
159 )
160 cg = changegroup.makechangegroup(repo, outgoing, cgversion, b'shelve')
161
162 bundle2.writebundle(
163 self.ui, cg, self.fname, btype, self.vfs, compression=compression
164 )
165
166 145
167 146 class Shelf(object):
168 147 """Represents a shelf, including possibly multiple files storing it.
169 148
170 149 Old shelves will have a .patch and a .hg file. Newer shelves will
171 150 also have a .shelve file. This class abstracts away some of the
172 151 differences and lets you work with the shelf as a whole.
173 152 """
174 153
175 154 def __init__(self, repo, name):
176 155 self.repo = repo
177 156 self.name = name
178 157 self.vfs = vfsmod.vfs(repo.vfs.join(shelvedir))
179 158
180 159 def exists(self):
181 160 return self.vfs.exists(self.name + b'.' + patchextension)
182 161
183 162 def writeinfo(self, info):
184 163 scmutil.simplekeyvaluefile(self.vfs, self.name + b'.shelve').write(info)
185 164
186 165 def readinfo(self):
187 166 return scmutil.simplekeyvaluefile(
188 167 self.vfs, self.name + b'.shelve'
189 168 ).read()
190 169
170 def writebundle(self, bases, node):
171 cgversion = changegroup.safeversion(self.repo)
172 if cgversion == b'01':
173 btype = b'HG10BZ'
174 compression = None
175 else:
176 btype = b'HG20'
177 compression = b'BZ'
178
179 repo = self.repo.unfiltered()
180
181 outgoing = discovery.outgoing(
182 repo, missingroots=bases, ancestorsof=[node]
183 )
184 cg = changegroup.makechangegroup(repo, outgoing, cgversion, b'shelve')
185
186 bundle_filename = self.vfs.join(self.name + b'.hg')
187 bundle2.writebundle(
188 self.repo.ui,
189 cg,
190 bundle_filename,
191 btype,
192 self.vfs,
193 compression=compression,
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 shelvedfile(repo, name, b'hg').writebundle(bases, node)
483 Shelf(repo, name).writebundle(bases, node)
479 484 with shelvedfile(repo, name, patchextension).opener(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 (name, _type) in repo.vfs.readdir(shelvedir):
605 610 suffix = name.rsplit(b'.', 1)[-1]
606 611 if suffix in shelvefileextensions:
607 612 shelvedfile(repo, name).movetobackup()
608 613 cleanupoldbackups(repo)
609 614
610 615
611 616 def deletecmd(ui, repo, pats):
612 617 """subcommand that deletes a specific shelve"""
613 618 if not pats:
614 619 raise error.InputError(_(b'no shelved changes specified!'))
615 620 with repo.wlock():
616 621 for name in pats:
617 622 if not Shelf(repo, name).exists():
618 623 raise error.InputError(
619 624 _(b"shelved change '%s' not found") % name
620 625 )
621 626 for suffix in shelvefileextensions:
622 627 shfile = shelvedfile(repo, name, suffix)
623 628 if shfile.exists():
624 629 shfile.movetobackup()
625 630 cleanupoldbackups(repo)
626 631
627 632
628 633 def listshelves(repo):
629 634 """return all shelves in repo as list of (time, filename)"""
630 635 try:
631 636 names = repo.vfs.readdir(shelvedir)
632 637 except OSError as err:
633 638 if err.errno != errno.ENOENT:
634 639 raise
635 640 return []
636 641 info = []
637 642 for (name, _type) in names:
638 643 pfx, sfx = name.rsplit(b'.', 1)
639 644 if not pfx or sfx != patchextension:
640 645 continue
641 646 st = shelvedfile(repo, name).stat()
642 647 info.append((st[stat.ST_MTIME], shelvedfile(repo, pfx).filename()))
643 648 return sorted(info, reverse=True)
644 649
645 650
646 651 def listcmd(ui, repo, pats, opts):
647 652 """subcommand that displays the list of shelves"""
648 653 pats = set(pats)
649 654 width = 80
650 655 if not ui.plain():
651 656 width = ui.termwidth()
652 657 namelabel = b'shelve.newest'
653 658 ui.pager(b'shelve')
654 659 for mtime, name in listshelves(repo):
655 660 sname = util.split(name)[1]
656 661 if pats and sname not in pats:
657 662 continue
658 663 ui.write(sname, label=namelabel)
659 664 namelabel = b'shelve.name'
660 665 if ui.quiet:
661 666 ui.write(b'\n')
662 667 continue
663 668 ui.write(b' ' * (16 - len(sname)))
664 669 used = 16
665 670 date = dateutil.makedate(mtime)
666 671 age = b'(%s)' % templatefilters.age(date, abbrev=True)
667 672 ui.write(age, label=b'shelve.age')
668 673 ui.write(b' ' * (12 - len(age)))
669 674 used += 12
670 675 with open(name + b'.' + patchextension, b'rb') as fp:
671 676 while True:
672 677 line = fp.readline()
673 678 if not line:
674 679 break
675 680 if not line.startswith(b'#'):
676 681 desc = line.rstrip()
677 682 if ui.formatted():
678 683 desc = stringutil.ellipsis(desc, width - used)
679 684 ui.write(desc)
680 685 break
681 686 ui.write(b'\n')
682 687 if not (opts[b'patch'] or opts[b'stat']):
683 688 continue
684 689 difflines = fp.readlines()
685 690 if opts[b'patch']:
686 691 for chunk, label in patch.difflabel(iter, difflines):
687 692 ui.write(chunk, label=label)
688 693 if opts[b'stat']:
689 694 for chunk, label in patch.diffstatui(difflines, width=width):
690 695 ui.write(chunk, label=label)
691 696
692 697
693 698 def patchcmds(ui, repo, pats, opts):
694 699 """subcommand that displays shelves"""
695 700 if len(pats) == 0:
696 701 shelves = listshelves(repo)
697 702 if not shelves:
698 703 raise error.Abort(_(b"there are no shelves to show"))
699 704 mtime, name = shelves[0]
700 705 sname = util.split(name)[1]
701 706 pats = [sname]
702 707
703 708 for shelfname in pats:
704 709 if not Shelf(repo, shelfname).exists():
705 710 raise error.Abort(_(b"cannot find shelf %s") % shelfname)
706 711
707 712 listcmd(ui, repo, pats, opts)
708 713
709 714
710 715 def checkparents(repo, state):
711 716 """check parent while resuming an unshelve"""
712 717 if state.parents != repo.dirstate.parents():
713 718 raise error.Abort(
714 719 _(b'working directory parents do not match unshelve state')
715 720 )
716 721
717 722
718 723 def _loadshelvedstate(ui, repo, opts):
719 724 try:
720 725 state = shelvedstate.load(repo)
721 726 if opts.get(b'keep') is None:
722 727 opts[b'keep'] = state.keep
723 728 except IOError as err:
724 729 if err.errno != errno.ENOENT:
725 730 raise
726 731 cmdutil.wrongtooltocontinue(repo, _(b'unshelve'))
727 732 except error.CorruptedState as err:
728 733 ui.debug(pycompat.bytestr(err) + b'\n')
729 734 if opts.get(b'continue'):
730 735 msg = _(b'corrupted shelved state file')
731 736 hint = _(
732 737 b'please run hg unshelve --abort to abort unshelve '
733 738 b'operation'
734 739 )
735 740 raise error.Abort(msg, hint=hint)
736 741 elif opts.get(b'abort'):
737 742 shelvedstate.clear(repo)
738 743 raise error.Abort(
739 744 _(
740 745 b'could not read shelved state file, your '
741 746 b'working copy may be in an unexpected state\n'
742 747 b'please update to some commit\n'
743 748 )
744 749 )
745 750 return state
746 751
747 752
748 753 def unshelveabort(ui, repo, state):
749 754 """subcommand that abort an in-progress unshelve"""
750 755 with repo.lock():
751 756 try:
752 757 checkparents(repo, state)
753 758
754 759 merge.clean_update(state.pendingctx)
755 760 if state.activebookmark and state.activebookmark in repo._bookmarks:
756 761 bookmarks.activate(repo, state.activebookmark)
757 762 mergefiles(ui, repo, state.wctx, state.pendingctx)
758 763 if not phases.supportinternal(repo):
759 764 repair.strip(
760 765 ui, repo, state.nodestoremove, backup=False, topic=b'shelve'
761 766 )
762 767 finally:
763 768 shelvedstate.clear(repo)
764 769 ui.warn(_(b"unshelve of '%s' aborted\n") % state.name)
765 770
766 771
767 772 def hgabortunshelve(ui, repo):
768 773 """logic to abort unshelve using 'hg abort"""
769 774 with repo.wlock():
770 775 state = _loadshelvedstate(ui, repo, {b'abort': True})
771 776 return unshelveabort(ui, repo, state)
772 777
773 778
774 779 def mergefiles(ui, repo, wctx, shelvectx):
775 780 """updates to wctx and merges the changes from shelvectx into the
776 781 dirstate."""
777 782 with ui.configoverride({(b'ui', b'quiet'): True}):
778 783 hg.update(repo, wctx.node())
779 784 ui.pushbuffer(True)
780 785 cmdutil.revert(ui, repo, shelvectx)
781 786 ui.popbuffer()
782 787
783 788
784 789 def restorebranch(ui, repo, branchtorestore):
785 790 if branchtorestore and branchtorestore != repo.dirstate.branch():
786 791 repo.dirstate.setbranch(branchtorestore)
787 792 ui.status(
788 793 _(b'marked working directory as branch %s\n') % branchtorestore
789 794 )
790 795
791 796
792 797 def unshelvecleanup(ui, repo, name, opts):
793 798 """remove related files after an unshelve"""
794 799 if not opts.get(b'keep'):
795 800 for filetype in shelvefileextensions:
796 801 shfile = shelvedfile(repo, name, filetype)
797 802 if shfile.exists():
798 803 shfile.movetobackup()
799 804 cleanupoldbackups(repo)
800 805
801 806
802 807 def unshelvecontinue(ui, repo, state, opts):
803 808 """subcommand to continue an in-progress unshelve"""
804 809 # We're finishing off a merge. First parent is our original
805 810 # parent, second is the temporary "fake" commit we're unshelving.
806 811 interactive = state.interactive
807 812 basename = state.name
808 813 with repo.lock():
809 814 checkparents(repo, state)
810 815 ms = mergestatemod.mergestate.read(repo)
811 816 if list(ms.unresolved()):
812 817 raise error.Abort(
813 818 _(b"unresolved conflicts, can't continue"),
814 819 hint=_(b"see 'hg resolve', then 'hg unshelve --continue'"),
815 820 )
816 821
817 822 shelvectx = repo[state.parents[1]]
818 823 pendingctx = state.pendingctx
819 824
820 825 with repo.dirstate.parentchange():
821 826 repo.setparents(state.pendingctx.node(), nullid)
822 827 repo.dirstate.write(repo.currenttransaction())
823 828
824 829 targetphase = phases.internal
825 830 if not phases.supportinternal(repo):
826 831 targetphase = phases.secret
827 832 overrides = {(b'phases', b'new-commit'): targetphase}
828 833 with repo.ui.configoverride(overrides, b'unshelve'):
829 834 with repo.dirstate.parentchange():
830 835 repo.setparents(state.parents[0], nullid)
831 836 newnode, ispartialunshelve = _createunshelvectx(
832 837 ui, repo, shelvectx, basename, interactive, opts
833 838 )
834 839
835 840 if newnode is None:
836 841 shelvectx = state.pendingctx
837 842 msg = _(
838 843 b'note: unshelved changes already existed '
839 844 b'in the working copy\n'
840 845 )
841 846 ui.status(msg)
842 847 else:
843 848 # only strip the shelvectx if we produced one
844 849 state.nodestoremove.append(newnode)
845 850 shelvectx = repo[newnode]
846 851
847 852 merge.update(pendingctx)
848 853 mergefiles(ui, repo, state.wctx, shelvectx)
849 854 restorebranch(ui, repo, state.branchtorestore)
850 855
851 856 if not phases.supportinternal(repo):
852 857 repair.strip(
853 858 ui, repo, state.nodestoremove, backup=False, topic=b'shelve'
854 859 )
855 860 shelvedstate.clear(repo)
856 861 if not ispartialunshelve:
857 862 unshelvecleanup(ui, repo, state.name, opts)
858 863 _restoreactivebookmark(repo, state.activebookmark)
859 864 ui.status(_(b"unshelve of '%s' complete\n") % state.name)
860 865
861 866
862 867 def hgcontinueunshelve(ui, repo):
863 868 """logic to resume unshelve using 'hg continue'"""
864 869 with repo.wlock():
865 870 state = _loadshelvedstate(ui, repo, {b'continue': True})
866 871 return unshelvecontinue(ui, repo, state, {b'keep': state.keep})
867 872
868 873
869 874 def _commitworkingcopychanges(ui, repo, opts, tmpwctx):
870 875 """Temporarily commit working copy changes before moving unshelve commit"""
871 876 # Store pending changes in a commit and remember added in case a shelve
872 877 # contains unknown files that are part of the pending change
873 878 s = repo.status()
874 879 addedbefore = frozenset(s.added)
875 880 if not (s.modified or s.added or s.removed):
876 881 return tmpwctx, addedbefore
877 882 ui.status(
878 883 _(
879 884 b"temporarily committing pending changes "
880 885 b"(restore with 'hg unshelve --abort')\n"
881 886 )
882 887 )
883 888 extra = {b'internal': b'shelve'}
884 889 commitfunc = getcommitfunc(extra=extra, interactive=False, editor=False)
885 890 tempopts = {}
886 891 tempopts[b'message'] = b"pending changes temporary commit"
887 892 tempopts[b'date'] = opts.get(b'date')
888 893 with ui.configoverride({(b'ui', b'quiet'): True}):
889 894 node = cmdutil.commit(ui, repo, commitfunc, [], tempopts)
890 895 tmpwctx = repo[node]
891 896 return tmpwctx, addedbefore
892 897
893 898
894 899 def _unshelverestorecommit(ui, repo, tr, basename):
895 900 """Recreate commit in the repository during the unshelve"""
896 901 repo = repo.unfiltered()
897 902 node = None
898 903 if shelvedfile(repo, basename, b'shelve').exists():
899 904 node = Shelf(repo, basename).readinfo()[b'node']
900 905 if node is None or node not in repo:
901 906 with ui.configoverride({(b'ui', b'quiet'): True}):
902 907 shelvectx = shelvedfile(repo, basename, b'hg').applybundle(tr)
903 908 # We might not strip the unbundled changeset, so we should keep track of
904 909 # the unshelve node in case we need to reuse it (eg: unshelve --keep)
905 910 if node is None:
906 911 info = {b'node': hex(shelvectx.node())}
907 912 Shelf(repo, basename).writeinfo(info)
908 913 else:
909 914 shelvectx = repo[node]
910 915
911 916 return repo, shelvectx
912 917
913 918
914 919 def _createunshelvectx(ui, repo, shelvectx, basename, interactive, opts):
915 920 """Handles the creation of unshelve commit and updates the shelve if it
916 921 was partially unshelved.
917 922
918 923 If interactive is:
919 924
920 925 * False: Commits all the changes in the working directory.
921 926 * True: Prompts the user to select changes to unshelve and commit them.
922 927 Update the shelve with remaining changes.
923 928
924 929 Returns the node of the new commit formed and a bool indicating whether
925 930 the shelve was partially unshelved.Creates a commit ctx to unshelve
926 931 interactively or non-interactively.
927 932
928 933 The user might want to unshelve certain changes only from the stored
929 934 shelve in interactive. So, we would create two commits. One with requested
930 935 changes to unshelve at that time and the latter is shelved for future.
931 936
932 937 Here, we return both the newnode which is created interactively and a
933 938 bool to know whether the shelve is partly done or completely done.
934 939 """
935 940 opts[b'message'] = shelvectx.description()
936 941 opts[b'interactive-unshelve'] = True
937 942 pats = []
938 943 if not interactive:
939 944 newnode = repo.commit(
940 945 text=shelvectx.description(),
941 946 extra=shelvectx.extra(),
942 947 user=shelvectx.user(),
943 948 date=shelvectx.date(),
944 949 )
945 950 return newnode, False
946 951
947 952 commitfunc = getcommitfunc(shelvectx.extra(), interactive=True, editor=True)
948 953 newnode = cmdutil.dorecord(
949 954 ui,
950 955 repo,
951 956 commitfunc,
952 957 None,
953 958 False,
954 959 cmdutil.recordfilter,
955 960 *pats,
956 961 **pycompat.strkwargs(opts)
957 962 )
958 963 snode = repo.commit(
959 964 text=shelvectx.description(),
960 965 extra=shelvectx.extra(),
961 966 user=shelvectx.user(),
962 967 )
963 968 if snode:
964 969 m = scmutil.matchfiles(repo, repo[snode].files())
965 970 _shelvecreatedcommit(repo, snode, basename, m)
966 971
967 972 return newnode, bool(snode)
968 973
969 974
970 975 def _rebaserestoredcommit(
971 976 ui,
972 977 repo,
973 978 opts,
974 979 tr,
975 980 oldtiprev,
976 981 basename,
977 982 pctx,
978 983 tmpwctx,
979 984 shelvectx,
980 985 branchtorestore,
981 986 activebookmark,
982 987 ):
983 988 """Rebase restored commit from its original location to a destination"""
984 989 # If the shelve is not immediately on top of the commit
985 990 # we'll be merging with, rebase it to be on top.
986 991 interactive = opts.get(b'interactive')
987 992 if tmpwctx.node() == shelvectx.p1().node() and not interactive:
988 993 # We won't skip on interactive mode because, the user might want to
989 994 # unshelve certain changes only.
990 995 return shelvectx, False
991 996
992 997 overrides = {
993 998 (b'ui', b'forcemerge'): opts.get(b'tool', b''),
994 999 (b'phases', b'new-commit'): phases.secret,
995 1000 }
996 1001 with repo.ui.configoverride(overrides, b'unshelve'):
997 1002 ui.status(_(b'rebasing shelved changes\n'))
998 1003 stats = merge.graft(
999 1004 repo,
1000 1005 shelvectx,
1001 1006 labels=[b'working-copy', b'shelve'],
1002 1007 keepconflictparent=True,
1003 1008 )
1004 1009 if stats.unresolvedcount:
1005 1010 tr.close()
1006 1011
1007 1012 nodestoremove = [
1008 1013 repo.changelog.node(rev)
1009 1014 for rev in pycompat.xrange(oldtiprev, len(repo))
1010 1015 ]
1011 1016 shelvedstate.save(
1012 1017 repo,
1013 1018 basename,
1014 1019 pctx,
1015 1020 tmpwctx,
1016 1021 nodestoremove,
1017 1022 branchtorestore,
1018 1023 opts.get(b'keep'),
1019 1024 activebookmark,
1020 1025 interactive,
1021 1026 )
1022 1027 raise error.ConflictResolutionRequired(b'unshelve')
1023 1028
1024 1029 with repo.dirstate.parentchange():
1025 1030 repo.setparents(tmpwctx.node(), nullid)
1026 1031 newnode, ispartialunshelve = _createunshelvectx(
1027 1032 ui, repo, shelvectx, basename, interactive, opts
1028 1033 )
1029 1034
1030 1035 if newnode is None:
1031 1036 shelvectx = tmpwctx
1032 1037 msg = _(
1033 1038 b'note: unshelved changes already existed '
1034 1039 b'in the working copy\n'
1035 1040 )
1036 1041 ui.status(msg)
1037 1042 else:
1038 1043 shelvectx = repo[newnode]
1039 1044 merge.update(tmpwctx)
1040 1045
1041 1046 return shelvectx, ispartialunshelve
1042 1047
1043 1048
1044 1049 def _forgetunknownfiles(repo, shelvectx, addedbefore):
1045 1050 # Forget any files that were unknown before the shelve, unknown before
1046 1051 # unshelve started, but are now added.
1047 1052 shelveunknown = shelvectx.extra().get(b'shelve_unknown')
1048 1053 if not shelveunknown:
1049 1054 return
1050 1055 shelveunknown = frozenset(shelveunknown.split(b'\0'))
1051 1056 addedafter = frozenset(repo.status().added)
1052 1057 toforget = (addedafter & shelveunknown) - addedbefore
1053 1058 repo[None].forget(toforget)
1054 1059
1055 1060
1056 1061 def _finishunshelve(repo, oldtiprev, tr, activebookmark):
1057 1062 _restoreactivebookmark(repo, activebookmark)
1058 1063 # The transaction aborting will strip all the commits for us,
1059 1064 # but it doesn't update the inmemory structures, so addchangegroup
1060 1065 # hooks still fire and try to operate on the missing commits.
1061 1066 # Clean up manually to prevent this.
1062 1067 repo.unfiltered().changelog.strip(oldtiprev, tr)
1063 1068 _aborttransaction(repo, tr)
1064 1069
1065 1070
1066 1071 def _checkunshelveuntrackedproblems(ui, repo, shelvectx):
1067 1072 """Check potential problems which may result from working
1068 1073 copy having untracked changes."""
1069 1074 wcdeleted = set(repo.status().deleted)
1070 1075 shelvetouched = set(shelvectx.files())
1071 1076 intersection = wcdeleted.intersection(shelvetouched)
1072 1077 if intersection:
1073 1078 m = _(b"shelved change touches missing files")
1074 1079 hint = _(b"run hg status to see which files are missing")
1075 1080 raise error.Abort(m, hint=hint)
1076 1081
1077 1082
1078 1083 def unshelvecmd(ui, repo, *shelved, **opts):
1079 1084 opts = pycompat.byteskwargs(opts)
1080 1085 abortf = opts.get(b'abort')
1081 1086 continuef = opts.get(b'continue')
1082 1087 interactive = opts.get(b'interactive')
1083 1088 if not abortf and not continuef:
1084 1089 cmdutil.checkunfinished(repo)
1085 1090 shelved = list(shelved)
1086 1091 if opts.get(b"name"):
1087 1092 shelved.append(opts[b"name"])
1088 1093
1089 1094 if interactive and opts.get(b'keep'):
1090 1095 raise error.InputError(
1091 1096 _(b'--keep on --interactive is not yet supported')
1092 1097 )
1093 1098 if abortf or continuef:
1094 1099 if abortf and continuef:
1095 1100 raise error.InputError(_(b'cannot use both abort and continue'))
1096 1101 if shelved:
1097 1102 raise error.InputError(
1098 1103 _(
1099 1104 b'cannot combine abort/continue with '
1100 1105 b'naming a shelved change'
1101 1106 )
1102 1107 )
1103 1108 if abortf and opts.get(b'tool', False):
1104 1109 ui.warn(_(b'tool option will be ignored\n'))
1105 1110
1106 1111 state = _loadshelvedstate(ui, repo, opts)
1107 1112 if abortf:
1108 1113 return unshelveabort(ui, repo, state)
1109 1114 elif continuef and interactive:
1110 1115 raise error.InputError(
1111 1116 _(b'cannot use both continue and interactive')
1112 1117 )
1113 1118 elif continuef:
1114 1119 return unshelvecontinue(ui, repo, state, opts)
1115 1120 elif len(shelved) > 1:
1116 1121 raise error.InputError(_(b'can only unshelve one change at a time'))
1117 1122 elif not shelved:
1118 1123 shelved = listshelves(repo)
1119 1124 if not shelved:
1120 1125 raise error.StateError(_(b'no shelved changes to apply!'))
1121 1126 basename = util.split(shelved[0][1])[1]
1122 1127 ui.status(_(b"unshelving change '%s'\n") % basename)
1123 1128 else:
1124 1129 basename = shelved[0]
1125 1130
1126 1131 if not Shelf(repo, basename).exists():
1127 1132 raise error.InputError(_(b"shelved change '%s' not found") % basename)
1128 1133
1129 1134 return _dounshelve(ui, repo, basename, opts)
1130 1135
1131 1136
1132 1137 def _dounshelve(ui, repo, basename, opts):
1133 1138 repo = repo.unfiltered()
1134 1139 lock = tr = None
1135 1140 try:
1136 1141 lock = repo.lock()
1137 1142 tr = repo.transaction(b'unshelve', report=lambda x: None)
1138 1143 oldtiprev = len(repo)
1139 1144
1140 1145 pctx = repo[b'.']
1141 1146 tmpwctx = pctx
1142 1147 # The goal is to have a commit structure like so:
1143 1148 # ...-> pctx -> tmpwctx -> shelvectx
1144 1149 # where tmpwctx is an optional commit with the user's pending changes
1145 1150 # and shelvectx is the unshelved changes. Then we merge it all down
1146 1151 # to the original pctx.
1147 1152
1148 1153 activebookmark = _backupactivebookmark(repo)
1149 1154 tmpwctx, addedbefore = _commitworkingcopychanges(
1150 1155 ui, repo, opts, tmpwctx
1151 1156 )
1152 1157 repo, shelvectx = _unshelverestorecommit(ui, repo, tr, basename)
1153 1158 _checkunshelveuntrackedproblems(ui, repo, shelvectx)
1154 1159 branchtorestore = b''
1155 1160 if shelvectx.branch() != shelvectx.p1().branch():
1156 1161 branchtorestore = shelvectx.branch()
1157 1162
1158 1163 shelvectx, ispartialunshelve = _rebaserestoredcommit(
1159 1164 ui,
1160 1165 repo,
1161 1166 opts,
1162 1167 tr,
1163 1168 oldtiprev,
1164 1169 basename,
1165 1170 pctx,
1166 1171 tmpwctx,
1167 1172 shelvectx,
1168 1173 branchtorestore,
1169 1174 activebookmark,
1170 1175 )
1171 1176 overrides = {(b'ui', b'forcemerge'): opts.get(b'tool', b'')}
1172 1177 with ui.configoverride(overrides, b'unshelve'):
1173 1178 mergefiles(ui, repo, pctx, shelvectx)
1174 1179 restorebranch(ui, repo, branchtorestore)
1175 1180 shelvedstate.clear(repo)
1176 1181 _finishunshelve(repo, oldtiprev, tr, activebookmark)
1177 1182 _forgetunknownfiles(repo, shelvectx, addedbefore)
1178 1183 if not ispartialunshelve:
1179 1184 unshelvecleanup(ui, repo, basename, opts)
1180 1185 finally:
1181 1186 if tr:
1182 1187 tr.release()
1183 1188 lockmod.release(lock)
General Comments 0
You need to be logged in to leave comments. Login now