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