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