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