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