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