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