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