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