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