##// END OF EJS Templates
dirstate: use `dirstate.change_files` to scope the change in `unshelve`...
marmoute -
r50948:5327ae76 default
parent child Browse files
Show More
@@ -1,1227 +1,1228 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 dirstatebackupname = b'dirstate.shelve'
436 436 repo.dirstate.savebackup(None, dirstatebackupname)
437 437 tr.abort()
438 438 repo.dirstate.restorebackup(None, dirstatebackupname)
439 439
440 440
441 441 def getshelvename(repo, parent, opts):
442 442 """Decide on the name this shelve is going to have"""
443 443
444 444 def gennames():
445 445 yield label
446 446 for i in itertools.count(1):
447 447 yield b'%s-%02d' % (label, i)
448 448
449 449 name = opts.get(b'name')
450 450 label = repo._activebookmark or parent.branch() or b'default'
451 451 # slashes aren't allowed in filenames, therefore we rename it
452 452 label = label.replace(b'/', b'_')
453 453 label = label.replace(b'\\', b'_')
454 454 # filenames must not start with '.' as it should not be hidden
455 455 if label.startswith(b'.'):
456 456 label = label.replace(b'.', b'_', 1)
457 457
458 458 if name:
459 459 if ShelfDir(repo).get(name).exists():
460 460 e = _(b"a shelved change named '%s' already exists") % name
461 461 raise error.Abort(e)
462 462
463 463 # ensure we are not creating a subdirectory or a hidden file
464 464 if b'/' in name or b'\\' in name:
465 465 raise error.Abort(
466 466 _(b'shelved change names can not contain slashes')
467 467 )
468 468 if name.startswith(b'.'):
469 469 raise error.Abort(_(b"shelved change names can not start with '.'"))
470 470
471 471 else:
472 472 shelf_dir = ShelfDir(repo)
473 473 for n in gennames():
474 474 if not shelf_dir.get(n).exists():
475 475 name = n
476 476 break
477 477
478 478 return name
479 479
480 480
481 481 def mutableancestors(ctx):
482 482 """return all mutable ancestors for ctx (included)
483 483
484 484 Much faster than the revset ancestors(ctx) & draft()"""
485 485 seen = {nullrev}
486 486 visit = collections.deque()
487 487 visit.append(ctx)
488 488 while visit:
489 489 ctx = visit.popleft()
490 490 yield ctx.node()
491 491 for parent in ctx.parents():
492 492 rev = parent.rev()
493 493 if rev not in seen:
494 494 seen.add(rev)
495 495 if parent.mutable():
496 496 visit.append(parent)
497 497
498 498
499 499 def getcommitfunc(extra, interactive, editor=False):
500 500 def commitfunc(ui, repo, message, match, opts):
501 501 hasmq = util.safehasattr(repo, b'mq')
502 502 if hasmq:
503 503 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
504 504
505 505 targetphase = _target_phase(repo)
506 506 overrides = {(b'phases', b'new-commit'): targetphase}
507 507 try:
508 508 editor_ = False
509 509 if editor:
510 510 editor_ = cmdutil.getcommiteditor(
511 511 editform=b'shelve.shelve', **pycompat.strkwargs(opts)
512 512 )
513 513 with repo.ui.configoverride(overrides):
514 514 return repo.commit(
515 515 message,
516 516 shelveuser,
517 517 opts.get(b'date'),
518 518 match,
519 519 editor=editor_,
520 520 extra=extra,
521 521 )
522 522 finally:
523 523 if hasmq:
524 524 repo.mq.checkapplied = saved
525 525
526 526 def interactivecommitfunc(ui, repo, *pats, **opts):
527 527 opts = pycompat.byteskwargs(opts)
528 528 match = scmutil.match(repo[b'.'], pats, {})
529 529 message = opts[b'message']
530 530 return commitfunc(ui, repo, message, match, opts)
531 531
532 532 return interactivecommitfunc if interactive else commitfunc
533 533
534 534
535 535 def _nothingtoshelvemessaging(ui, repo, pats, opts):
536 536 stat = repo.status(match=scmutil.match(repo[None], pats, opts))
537 537 if stat.deleted:
538 538 ui.status(
539 539 _(b"nothing changed (%d missing files, see 'hg status')\n")
540 540 % len(stat.deleted)
541 541 )
542 542 else:
543 543 ui.status(_(b"nothing changed\n"))
544 544
545 545
546 546 def _shelvecreatedcommit(repo, node, name, match):
547 547 info = {b'node': hex(node)}
548 548 shelf = ShelfDir(repo).get(name)
549 549 shelf.writeinfo(info)
550 550 bases = list(mutableancestors(repo[node]))
551 551 shelf.writebundle(repo, bases, node)
552 552 with shelf.open_patch(b'wb') as fp:
553 553 cmdutil.exportfile(
554 554 repo, [node], fp, opts=mdiff.diffopts(git=True), match=match
555 555 )
556 556
557 557
558 558 def _includeunknownfiles(repo, pats, opts, extra):
559 559 s = repo.status(match=scmutil.match(repo[None], pats, opts), unknown=True)
560 560 if s.unknown:
561 561 extra[b'shelve_unknown'] = b'\0'.join(s.unknown)
562 562 repo[None].add(s.unknown)
563 563
564 564
565 565 def _finishshelve(repo, tr):
566 566 if _use_internal_phase(repo):
567 567 tr.close()
568 568 else:
569 569 _aborttransaction(repo, tr)
570 570
571 571
572 572 def createcmd(ui, repo, pats, opts):
573 573 """subcommand that creates a new shelve"""
574 574 with repo.wlock():
575 575 cmdutil.checkunfinished(repo)
576 576 return _docreatecmd(ui, repo, pats, opts)
577 577
578 578
579 579 def _docreatecmd(ui, repo, pats, opts):
580 580 wctx = repo[None]
581 581 parents = wctx.parents()
582 582 parent = parents[0]
583 583 origbranch = wctx.branch()
584 584
585 585 if parent.rev() != nullrev:
586 586 desc = b"changes to: %s" % parent.description().split(b'\n', 1)[0]
587 587 else:
588 588 desc = b'(changes in empty repository)'
589 589
590 590 if not opts.get(b'message'):
591 591 opts[b'message'] = desc
592 592
593 593 lock = tr = activebookmark = None
594 594 try:
595 595 lock = repo.lock()
596 596
597 597 # use an uncommitted transaction to generate the bundle to avoid
598 598 # pull races. ensure we don't print the abort message to stderr.
599 599 tr = repo.transaction(b'shelve', report=lambda x: None)
600 600
601 601 interactive = opts.get(b'interactive', False)
602 602 includeunknown = opts.get(b'unknown', False) and not opts.get(
603 603 b'addremove', False
604 604 )
605 605
606 606 name = getshelvename(repo, parent, opts)
607 607 activebookmark = _backupactivebookmark(repo)
608 608 extra = {b'internal': b'shelve'}
609 609 if includeunknown:
610 610 _includeunknownfiles(repo, pats, opts, extra)
611 611
612 612 if _iswctxonnewbranch(repo) and not _isbareshelve(pats, opts):
613 613 # In non-bare shelve we don't store newly created branch
614 614 # at bundled commit
615 615 repo.dirstate.setbranch(repo[b'.'].branch())
616 616
617 617 commitfunc = getcommitfunc(extra, interactive, editor=True)
618 618 if not interactive:
619 619 node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
620 620 else:
621 621 node = cmdutil.dorecord(
622 622 ui,
623 623 repo,
624 624 commitfunc,
625 625 None,
626 626 False,
627 627 cmdutil.recordfilter,
628 628 *pats,
629 629 **pycompat.strkwargs(opts)
630 630 )
631 631 if not node:
632 632 _nothingtoshelvemessaging(ui, repo, pats, opts)
633 633 return 1
634 634
635 635 match = _optimized_match(repo, node)
636 636 _shelvecreatedcommit(repo, node, name, match)
637 637
638 638 ui.status(_(b'shelved as %s\n') % name)
639 639 if opts[b'keep']:
640 640 with repo.dirstate.changing_parents(repo):
641 641 scmutil.movedirstate(repo, parent, match)
642 642 else:
643 643 hg.update(repo, parent.node())
644 644 ms = mergestatemod.mergestate.read(repo)
645 645 if not ms.unresolvedcount():
646 646 ms.reset()
647 647
648 648 if origbranch != repo[b'.'].branch() and not _isbareshelve(pats, opts):
649 649 repo.dirstate.setbranch(origbranch)
650 650
651 651 _finishshelve(repo, tr)
652 652 finally:
653 653 _restoreactivebookmark(repo, activebookmark)
654 654 lockmod.release(tr, lock)
655 655
656 656
657 657 def _isbareshelve(pats, opts):
658 658 return (
659 659 not pats
660 660 and not opts.get(b'interactive', False)
661 661 and not opts.get(b'include', False)
662 662 and not opts.get(b'exclude', False)
663 663 )
664 664
665 665
666 666 def _iswctxonnewbranch(repo):
667 667 return repo[None].branch() != repo[b'.'].branch()
668 668
669 669
670 670 def cleanupcmd(ui, repo):
671 671 """subcommand that deletes all shelves"""
672 672
673 673 with repo.wlock():
674 674 shelf_dir = ShelfDir(repo)
675 675 backupvfs = vfsmod.vfs(repo.vfs.join(backupdir))
676 676 for _mtime, name in shelf_dir.listshelves():
677 677 shelf_dir.get(name).movetobackup(backupvfs)
678 678 cleanupoldbackups(repo)
679 679
680 680
681 681 def deletecmd(ui, repo, pats):
682 682 """subcommand that deletes a specific shelve"""
683 683 if not pats:
684 684 raise error.InputError(_(b'no shelved changes specified!'))
685 685 with repo.wlock():
686 686 backupvfs = vfsmod.vfs(repo.vfs.join(backupdir))
687 687 for name in pats:
688 688 shelf = ShelfDir(repo).get(name)
689 689 if not shelf.exists():
690 690 raise error.InputError(
691 691 _(b"shelved change '%s' not found") % name
692 692 )
693 693 shelf.movetobackup(backupvfs)
694 694 cleanupoldbackups(repo)
695 695
696 696
697 697 def listcmd(ui, repo, pats, opts):
698 698 """subcommand that displays the list of shelves"""
699 699 pats = set(pats)
700 700 width = 80
701 701 if not ui.plain():
702 702 width = ui.termwidth()
703 703 namelabel = b'shelve.newest'
704 704 ui.pager(b'shelve')
705 705 shelf_dir = ShelfDir(repo)
706 706 for mtime, name in shelf_dir.listshelves():
707 707 if pats and name not in pats:
708 708 continue
709 709 ui.write(name, label=namelabel)
710 710 namelabel = b'shelve.name'
711 711 if ui.quiet:
712 712 ui.write(b'\n')
713 713 continue
714 714 ui.write(b' ' * (16 - len(name)))
715 715 used = 16
716 716 date = dateutil.makedate(mtime)
717 717 age = b'(%s)' % templatefilters.age(date, abbrev=True)
718 718 ui.write(age, label=b'shelve.age')
719 719 ui.write(b' ' * (12 - len(age)))
720 720 used += 12
721 721 with shelf_dir.get(name).load_patch(repo) as fp:
722 722 while True:
723 723 line = fp.readline()
724 724 if not line:
725 725 break
726 726 if not line.startswith(b'#'):
727 727 desc = line.rstrip()
728 728 if ui.formatted():
729 729 desc = stringutil.ellipsis(desc, width - used)
730 730 ui.write(desc)
731 731 break
732 732 ui.write(b'\n')
733 733 if not (opts[b'patch'] or opts[b'stat']):
734 734 continue
735 735 difflines = fp.readlines()
736 736 if opts[b'patch']:
737 737 for chunk, label in patch.difflabel(iter, difflines):
738 738 ui.write(chunk, label=label)
739 739 if opts[b'stat']:
740 740 for chunk, label in patch.diffstatui(difflines, width=width):
741 741 ui.write(chunk, label=label)
742 742
743 743
744 744 def patchcmds(ui, repo, pats, opts):
745 745 """subcommand that displays shelves"""
746 746 shelf_dir = ShelfDir(repo)
747 747 if len(pats) == 0:
748 748 shelves = shelf_dir.listshelves()
749 749 if not shelves:
750 750 raise error.Abort(_(b"there are no shelves to show"))
751 751 mtime, name = shelves[0]
752 752 pats = [name]
753 753
754 754 for shelfname in pats:
755 755 if not shelf_dir.get(shelfname).exists():
756 756 raise error.Abort(_(b"cannot find shelf %s") % shelfname)
757 757
758 758 listcmd(ui, repo, pats, opts)
759 759
760 760
761 761 def checkparents(repo, state):
762 762 """check parent while resuming an unshelve"""
763 763 if state.parents != repo.dirstate.parents():
764 764 raise error.Abort(
765 765 _(b'working directory parents do not match unshelve state')
766 766 )
767 767
768 768
769 769 def _loadshelvedstate(ui, repo, opts):
770 770 try:
771 771 state = shelvedstate.load(repo)
772 772 if opts.get(b'keep') is None:
773 773 opts[b'keep'] = state.keep
774 774 except FileNotFoundError:
775 775 cmdutil.wrongtooltocontinue(repo, _(b'unshelve'))
776 776 except error.CorruptedState as err:
777 777 ui.debug(pycompat.bytestr(err) + b'\n')
778 778 if opts.get(b'continue'):
779 779 msg = _(b'corrupted shelved state file')
780 780 hint = _(
781 781 b'please run hg unshelve --abort to abort unshelve '
782 782 b'operation'
783 783 )
784 784 raise error.Abort(msg, hint=hint)
785 785 elif opts.get(b'abort'):
786 786 shelvedstate.clear(repo)
787 787 raise error.Abort(
788 788 _(
789 789 b'could not read shelved state file, your '
790 790 b'working copy may be in an unexpected state\n'
791 791 b'please update to some commit\n'
792 792 )
793 793 )
794 794 return state
795 795
796 796
797 797 def unshelveabort(ui, repo, state):
798 798 """subcommand that abort an in-progress unshelve"""
799 799 with repo.lock():
800 800 try:
801 801 checkparents(repo, state)
802 802
803 803 merge.clean_update(state.pendingctx)
804 804 if state.activebookmark and state.activebookmark in repo._bookmarks:
805 805 bookmarks.activate(repo, state.activebookmark)
806 806 mergefiles(ui, repo, state.wctx, state.pendingctx)
807 807 if not _use_internal_phase(repo):
808 808 repair.strip(
809 809 ui, repo, state.nodestoremove, backup=False, topic=b'shelve'
810 810 )
811 811 finally:
812 812 shelvedstate.clear(repo)
813 813 ui.warn(_(b"unshelve of '%s' aborted\n") % state.name)
814 814
815 815
816 816 def hgabortunshelve(ui, repo):
817 817 """logic to abort unshelve using 'hg abort"""
818 818 with repo.wlock():
819 819 state = _loadshelvedstate(ui, repo, {b'abort': True})
820 820 return unshelveabort(ui, repo, state)
821 821
822 822
823 823 def mergefiles(ui, repo, wctx, shelvectx):
824 824 """updates to wctx and merges the changes from shelvectx into the
825 825 dirstate."""
826 826 with ui.configoverride({(b'ui', b'quiet'): True}):
827 827 hg.update(repo, wctx.node())
828 828 cmdutil.revert(ui, repo, shelvectx)
829 829
830 830
831 831 def restorebranch(ui, repo, branchtorestore):
832 832 if branchtorestore and branchtorestore != repo.dirstate.branch():
833 833 repo.dirstate.setbranch(branchtorestore)
834 834 ui.status(
835 835 _(b'marked working directory as branch %s\n') % branchtorestore
836 836 )
837 837
838 838
839 839 def unshelvecleanup(ui, repo, name, opts):
840 840 """remove related files after an unshelve"""
841 841 if not opts.get(b'keep'):
842 842 backupvfs = vfsmod.vfs(repo.vfs.join(backupdir))
843 843 ShelfDir(repo).get(name).movetobackup(backupvfs)
844 844 cleanupoldbackups(repo)
845 845
846 846
847 847 def unshelvecontinue(ui, repo, state, opts):
848 848 """subcommand to continue an in-progress unshelve"""
849 849 # We're finishing off a merge. First parent is our original
850 850 # parent, second is the temporary "fake" commit we're unshelving.
851 851 interactive = state.interactive
852 852 basename = state.name
853 853 with repo.lock():
854 854 checkparents(repo, state)
855 855 ms = mergestatemod.mergestate.read(repo)
856 856 if ms.unresolvedcount():
857 857 raise error.Abort(
858 858 _(b"unresolved conflicts, can't continue"),
859 859 hint=_(b"see 'hg resolve', then 'hg unshelve --continue'"),
860 860 )
861 861
862 862 shelvectx = repo[state.parents[1]]
863 863 pendingctx = state.pendingctx
864 864
865 865 with repo.dirstate.changing_parents(repo):
866 866 repo.setparents(state.pendingctx.node(), repo.nullid)
867 867 repo.dirstate.write(repo.currenttransaction())
868 868
869 869 targetphase = _target_phase(repo)
870 870 overrides = {(b'phases', b'new-commit'): targetphase}
871 871 with repo.ui.configoverride(overrides, b'unshelve'):
872 872 with repo.dirstate.changing_parents(repo):
873 873 repo.setparents(state.parents[0], repo.nullid)
874 874 newnode, ispartialunshelve = _createunshelvectx(
875 875 ui, repo, shelvectx, basename, interactive, opts
876 876 )
877 877
878 878 if newnode is None:
879 879 shelvectx = state.pendingctx
880 880 msg = _(
881 881 b'note: unshelved changes already existed '
882 882 b'in the working copy\n'
883 883 )
884 884 ui.status(msg)
885 885 else:
886 886 # only strip the shelvectx if we produced one
887 887 state.nodestoremove.append(newnode)
888 888 shelvectx = repo[newnode]
889 889
890 890 merge.update(pendingctx)
891 891 mergefiles(ui, repo, state.wctx, shelvectx)
892 892 restorebranch(ui, repo, state.branchtorestore)
893 893
894 894 if not _use_internal_phase(repo):
895 895 repair.strip(
896 896 ui, repo, state.nodestoremove, backup=False, topic=b'shelve'
897 897 )
898 898 shelvedstate.clear(repo)
899 899 if not ispartialunshelve:
900 900 unshelvecleanup(ui, repo, state.name, opts)
901 901 _restoreactivebookmark(repo, state.activebookmark)
902 902 ui.status(_(b"unshelve of '%s' complete\n") % state.name)
903 903
904 904
905 905 def hgcontinueunshelve(ui, repo):
906 906 """logic to resume unshelve using 'hg continue'"""
907 907 with repo.wlock():
908 908 state = _loadshelvedstate(ui, repo, {b'continue': True})
909 909 return unshelvecontinue(ui, repo, state, {b'keep': state.keep})
910 910
911 911
912 912 def _commitworkingcopychanges(ui, repo, opts, tmpwctx):
913 913 """Temporarily commit working copy changes before moving unshelve commit"""
914 914 # Store pending changes in a commit and remember added in case a shelve
915 915 # contains unknown files that are part of the pending change
916 916 s = repo.status()
917 917 addedbefore = frozenset(s.added)
918 918 if not (s.modified or s.added or s.removed):
919 919 return tmpwctx, addedbefore
920 920 ui.status(
921 921 _(
922 922 b"temporarily committing pending changes "
923 923 b"(restore with 'hg unshelve --abort')\n"
924 924 )
925 925 )
926 926 extra = {b'internal': b'shelve'}
927 927 commitfunc = getcommitfunc(extra=extra, interactive=False, editor=False)
928 928 tempopts = {}
929 929 tempopts[b'message'] = b"pending changes temporary commit"
930 930 tempopts[b'date'] = opts.get(b'date')
931 931 with ui.configoverride({(b'ui', b'quiet'): True}):
932 932 node = cmdutil.commit(ui, repo, commitfunc, [], tempopts)
933 933 tmpwctx = repo[node]
934 934 return tmpwctx, addedbefore
935 935
936 936
937 937 def _unshelverestorecommit(ui, repo, tr, basename):
938 938 """Recreate commit in the repository during the unshelve"""
939 939 repo = repo.unfiltered()
940 940 node = None
941 941 shelf = ShelfDir(repo).get(basename)
942 942 if shelf.hasinfo():
943 943 node = shelf.readinfo()[b'node']
944 944 if node is None or node not in repo:
945 945 with ui.configoverride({(b'ui', b'quiet'): True}):
946 946 shelvectx = shelf.applybundle(repo, tr)
947 947 # We might not strip the unbundled changeset, so we should keep track of
948 948 # the unshelve node in case we need to reuse it (eg: unshelve --keep)
949 949 if node is None:
950 950 info = {b'node': hex(shelvectx.node())}
951 951 shelf.writeinfo(info)
952 952 else:
953 953 shelvectx = repo[node]
954 954
955 955 return repo, shelvectx
956 956
957 957
958 958 def _createunshelvectx(ui, repo, shelvectx, basename, interactive, opts):
959 959 """Handles the creation of unshelve commit and updates the shelve if it
960 960 was partially unshelved.
961 961
962 962 If interactive is:
963 963
964 964 * False: Commits all the changes in the working directory.
965 965 * True: Prompts the user to select changes to unshelve and commit them.
966 966 Update the shelve with remaining changes.
967 967
968 968 Returns the node of the new commit formed and a bool indicating whether
969 969 the shelve was partially unshelved.Creates a commit ctx to unshelve
970 970 interactively or non-interactively.
971 971
972 972 The user might want to unshelve certain changes only from the stored
973 973 shelve in interactive. So, we would create two commits. One with requested
974 974 changes to unshelve at that time and the latter is shelved for future.
975 975
976 976 Here, we return both the newnode which is created interactively and a
977 977 bool to know whether the shelve is partly done or completely done.
978 978 """
979 979 opts[b'message'] = shelvectx.description()
980 980 opts[b'interactive-unshelve'] = True
981 981 pats = []
982 982 if not interactive:
983 983 newnode = repo.commit(
984 984 text=shelvectx.description(),
985 985 extra=shelvectx.extra(),
986 986 user=shelvectx.user(),
987 987 date=shelvectx.date(),
988 988 )
989 989 return newnode, False
990 990
991 991 commitfunc = getcommitfunc(shelvectx.extra(), interactive=True, editor=True)
992 992 newnode = cmdutil.dorecord(
993 993 ui,
994 994 repo,
995 995 commitfunc,
996 996 None,
997 997 False,
998 998 cmdutil.recordfilter,
999 999 *pats,
1000 1000 **pycompat.strkwargs(opts)
1001 1001 )
1002 1002 snode = repo.commit(
1003 1003 text=shelvectx.description(),
1004 1004 extra=shelvectx.extra(),
1005 1005 user=shelvectx.user(),
1006 1006 )
1007 1007 if snode:
1008 1008 m = _optimized_match(repo, snode)
1009 1009 _shelvecreatedcommit(repo, snode, basename, m)
1010 1010
1011 1011 return newnode, bool(snode)
1012 1012
1013 1013
1014 1014 def _rebaserestoredcommit(
1015 1015 ui,
1016 1016 repo,
1017 1017 opts,
1018 1018 tr,
1019 1019 oldtiprev,
1020 1020 basename,
1021 1021 pctx,
1022 1022 tmpwctx,
1023 1023 shelvectx,
1024 1024 branchtorestore,
1025 1025 activebookmark,
1026 1026 ):
1027 1027 """Rebase restored commit from its original location to a destination"""
1028 1028 # If the shelve is not immediately on top of the commit
1029 1029 # we'll be merging with, rebase it to be on top.
1030 1030 interactive = opts.get(b'interactive')
1031 1031 if tmpwctx.node() == shelvectx.p1().node() and not interactive:
1032 1032 # We won't skip on interactive mode because, the user might want to
1033 1033 # unshelve certain changes only.
1034 1034 return shelvectx, False
1035 1035
1036 1036 overrides = {
1037 1037 (b'ui', b'forcemerge'): opts.get(b'tool', b''),
1038 1038 (b'phases', b'new-commit'): phases.secret,
1039 1039 }
1040 1040 with repo.ui.configoverride(overrides, b'unshelve'):
1041 1041 ui.status(_(b'rebasing shelved changes\n'))
1042 1042 stats = merge.graft(
1043 1043 repo,
1044 1044 shelvectx,
1045 1045 labels=[
1046 1046 b'working-copy',
1047 1047 b'shelved change',
1048 1048 b'parent of shelved change',
1049 1049 ],
1050 1050 keepconflictparent=True,
1051 1051 )
1052 1052 if stats.unresolvedcount:
1053 1053 tr.close()
1054 1054
1055 1055 nodestoremove = [
1056 1056 repo.changelog.node(rev) for rev in range(oldtiprev, len(repo))
1057 1057 ]
1058 1058 shelvedstate.save(
1059 1059 repo,
1060 1060 basename,
1061 1061 pctx,
1062 1062 tmpwctx,
1063 1063 nodestoremove,
1064 1064 branchtorestore,
1065 1065 opts.get(b'keep'),
1066 1066 activebookmark,
1067 1067 interactive,
1068 1068 )
1069 1069 raise error.ConflictResolutionRequired(b'unshelve')
1070 1070
1071 1071 with repo.dirstate.changing_parents(repo):
1072 1072 repo.setparents(tmpwctx.node(), repo.nullid)
1073 1073 newnode, ispartialunshelve = _createunshelvectx(
1074 1074 ui, repo, shelvectx, basename, interactive, opts
1075 1075 )
1076 1076
1077 1077 if newnode is None:
1078 1078 shelvectx = tmpwctx
1079 1079 msg = _(
1080 1080 b'note: unshelved changes already existed '
1081 1081 b'in the working copy\n'
1082 1082 )
1083 1083 ui.status(msg)
1084 1084 else:
1085 1085 shelvectx = repo[newnode]
1086 1086 merge.update(tmpwctx)
1087 1087
1088 1088 return shelvectx, ispartialunshelve
1089 1089
1090 1090
1091 1091 def _forgetunknownfiles(repo, shelvectx, addedbefore):
1092 1092 # Forget any files that were unknown before the shelve, unknown before
1093 1093 # unshelve started, but are now added.
1094 1094 shelveunknown = shelvectx.extra().get(b'shelve_unknown')
1095 1095 if not shelveunknown:
1096 1096 return
1097 1097 shelveunknown = frozenset(shelveunknown.split(b'\0'))
1098 1098 addedafter = frozenset(repo.status().added)
1099 1099 toforget = (addedafter & shelveunknown) - addedbefore
1100 1100 repo[None].forget(toforget)
1101 1101
1102 1102
1103 1103 def _finishunshelve(repo, oldtiprev, tr, activebookmark):
1104 1104 _restoreactivebookmark(repo, activebookmark)
1105 1105 # The transaction aborting will strip all the commits for us,
1106 1106 # but it doesn't update the inmemory structures, so addchangegroup
1107 1107 # hooks still fire and try to operate on the missing commits.
1108 1108 # Clean up manually to prevent this.
1109 1109 repo.unfiltered().changelog.strip(oldtiprev, tr)
1110 1110 _aborttransaction(repo, tr)
1111 1111
1112 1112
1113 1113 def _checkunshelveuntrackedproblems(ui, repo, shelvectx):
1114 1114 """Check potential problems which may result from working
1115 1115 copy having untracked changes."""
1116 1116 wcdeleted = set(repo.status().deleted)
1117 1117 shelvetouched = set(shelvectx.files())
1118 1118 intersection = wcdeleted.intersection(shelvetouched)
1119 1119 if intersection:
1120 1120 m = _(b"shelved change touches missing files")
1121 1121 hint = _(b"run hg status to see which files are missing")
1122 1122 raise error.Abort(m, hint=hint)
1123 1123
1124 1124
1125 1125 def unshelvecmd(ui, repo, *shelved, **opts):
1126 1126 opts = pycompat.byteskwargs(opts)
1127 1127 abortf = opts.get(b'abort')
1128 1128 continuef = opts.get(b'continue')
1129 1129 interactive = opts.get(b'interactive')
1130 1130 if not abortf and not continuef:
1131 1131 cmdutil.checkunfinished(repo)
1132 1132 shelved = list(shelved)
1133 1133 if opts.get(b"name"):
1134 1134 shelved.append(opts[b"name"])
1135 1135
1136 1136 if interactive and opts.get(b'keep'):
1137 1137 raise error.InputError(
1138 1138 _(b'--keep on --interactive is not yet supported')
1139 1139 )
1140 1140 if abortf or continuef:
1141 1141 if abortf and continuef:
1142 1142 raise error.InputError(_(b'cannot use both abort and continue'))
1143 1143 if shelved:
1144 1144 raise error.InputError(
1145 1145 _(
1146 1146 b'cannot combine abort/continue with '
1147 1147 b'naming a shelved change'
1148 1148 )
1149 1149 )
1150 1150 if abortf and opts.get(b'tool', False):
1151 1151 ui.warn(_(b'tool option will be ignored\n'))
1152 1152
1153 1153 state = _loadshelvedstate(ui, repo, opts)
1154 1154 if abortf:
1155 1155 return unshelveabort(ui, repo, state)
1156 1156 elif continuef and interactive:
1157 1157 raise error.InputError(
1158 1158 _(b'cannot use both continue and interactive')
1159 1159 )
1160 1160 elif continuef:
1161 1161 return unshelvecontinue(ui, repo, state, opts)
1162 1162 elif len(shelved) > 1:
1163 1163 raise error.InputError(_(b'can only unshelve one change at a time'))
1164 1164 elif not shelved:
1165 1165 shelved = ShelfDir(repo).listshelves()
1166 1166 if not shelved:
1167 1167 raise error.StateError(_(b'no shelved changes to apply!'))
1168 1168 basename = shelved[0][1]
1169 1169 ui.status(_(b"unshelving change '%s'\n") % basename)
1170 1170 else:
1171 1171 basename = shelved[0]
1172 1172
1173 1173 if not ShelfDir(repo).get(basename).exists():
1174 1174 raise error.InputError(_(b"shelved change '%s' not found") % basename)
1175 1175
1176 1176 return _dounshelve(ui, repo, basename, opts)
1177 1177
1178 1178
1179 1179 def _dounshelve(ui, repo, basename, opts):
1180 1180 repo = repo.unfiltered()
1181 1181 lock = tr = None
1182 1182 try:
1183 1183 lock = repo.lock()
1184 1184 tr = repo.transaction(b'unshelve', report=lambda x: None)
1185 1185 oldtiprev = len(repo)
1186 1186
1187 1187 pctx = repo[b'.']
1188 1188 # The goal is to have a commit structure like so:
1189 1189 # ...-> pctx -> tmpwctx -> shelvectx
1190 1190 # where tmpwctx is an optional commit with the user's pending changes
1191 1191 # and shelvectx is the unshelved changes. Then we merge it all down
1192 1192 # to the original pctx.
1193 1193
1194 1194 activebookmark = _backupactivebookmark(repo)
1195 1195 tmpwctx, addedbefore = _commitworkingcopychanges(ui, repo, opts, pctx)
1196 1196 repo, shelvectx = _unshelverestorecommit(ui, repo, tr, basename)
1197 1197 _checkunshelveuntrackedproblems(ui, repo, shelvectx)
1198 1198 branchtorestore = b''
1199 1199 if shelvectx.branch() != shelvectx.p1().branch():
1200 1200 branchtorestore = shelvectx.branch()
1201 1201
1202 1202 shelvectx, ispartialunshelve = _rebaserestoredcommit(
1203 1203 ui,
1204 1204 repo,
1205 1205 opts,
1206 1206 tr,
1207 1207 oldtiprev,
1208 1208 basename,
1209 1209 pctx,
1210 1210 tmpwctx,
1211 1211 shelvectx,
1212 1212 branchtorestore,
1213 1213 activebookmark,
1214 1214 )
1215 1215 overrides = {(b'ui', b'forcemerge'): opts.get(b'tool', b'')}
1216 1216 with ui.configoverride(overrides, b'unshelve'):
1217 1217 mergefiles(ui, repo, pctx, shelvectx)
1218 1218 restorebranch(ui, repo, branchtorestore)
1219 1219 shelvedstate.clear(repo)
1220 1220 _finishunshelve(repo, oldtiprev, tr, activebookmark)
1221 _forgetunknownfiles(repo, shelvectx, addedbefore)
1221 with repo.dirstate.changing_files(repo):
1222 _forgetunknownfiles(repo, shelvectx, addedbefore)
1222 1223 if not ispartialunshelve:
1223 1224 unshelvecleanup(ui, repo, basename, opts)
1224 1225 finally:
1225 1226 if tr:
1226 1227 tr.release()
1227 1228 lockmod.release(lock)
General Comments 0
You need to be logged in to leave comments. Login now