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