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