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