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