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