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