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