##// END OF EJS Templates
shelve: move changegroup generation inside writebundle...
Pierre-Yves David -
r26506:edecf059 default
parent child Browse files
Show More
@@ -1,800 +1,800
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
24 24 import collections
25 25 import itertools
26 26 from mercurial.i18n import _
27 27 from mercurial.node import nullid, nullrev, bin, hex
28 28 from mercurial import changegroup, cmdutil, scmutil, phases, commands
29 29 from mercurial import error, hg, mdiff, merge, patch, repair, util
30 30 from mercurial import templatefilters, exchange, bundlerepo
31 31 from mercurial import lock as lockmod
32 32 from hgext import rebase
33 33 import errno
34 34
35 35 cmdtable = {}
36 36 command = cmdutil.command(cmdtable)
37 37 # Note for extension authors: ONLY specify testedwith = 'internal' for
38 38 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
39 39 # be specifying the version(s) of Mercurial they are tested with, or
40 40 # leave the attribute unspecified.
41 41 testedwith = 'internal'
42 42
43 43 backupdir = 'shelve-backup'
44 44
45 45 class shelvedfile(object):
46 46 """Helper for the file storing a single shelve
47 47
48 48 Handles common functions on shelve files (.hg/.patch) using
49 49 the vfs layer"""
50 50 def __init__(self, repo, name, filetype=None):
51 51 self.repo = repo
52 52 self.name = name
53 53 self.vfs = scmutil.vfs(repo.join('shelved'))
54 54 self.backupvfs = scmutil.vfs(repo.join(backupdir))
55 55 self.ui = self.repo.ui
56 56 if filetype:
57 57 self.fname = name + '.' + filetype
58 58 else:
59 59 self.fname = name
60 60
61 61 def exists(self):
62 62 return self.vfs.exists(self.fname)
63 63
64 64 def filename(self):
65 65 return self.vfs.join(self.fname)
66 66
67 67 def backupfilename(self):
68 68 def gennames(base):
69 69 yield base
70 70 base, ext = base.rsplit('.', 1)
71 71 for i in itertools.count(1):
72 72 yield '%s-%d.%s' % (base, i, ext)
73 73
74 74 name = self.backupvfs.join(self.fname)
75 75 for n in gennames(name):
76 76 if not self.backupvfs.exists(n):
77 77 return n
78 78
79 79 def movetobackup(self):
80 80 if not self.backupvfs.isdir():
81 81 self.backupvfs.makedir()
82 82 util.rename(self.filename(), self.backupfilename())
83 83
84 84 def stat(self):
85 85 return self.vfs.stat(self.fname)
86 86
87 87 def opener(self, mode='rb'):
88 88 try:
89 89 return self.vfs(self.fname, mode)
90 90 except IOError as err:
91 91 if err.errno != errno.ENOENT:
92 92 raise
93 93 raise util.Abort(_("shelved change '%s' not found") % self.name)
94 94
95 95 def applybundle(self):
96 96 fp = self.opener()
97 97 try:
98 98 gen = exchange.readbundle(self.repo.ui, fp, self.fname, self.vfs)
99 99 changegroup.addchangegroup(self.repo, gen, 'unshelve',
100 100 'bundle:' + self.vfs.join(self.fname),
101 101 targetphase=phases.secret)
102 102 finally:
103 103 fp.close()
104 104
105 105 def bundlerepo(self):
106 106 return bundlerepo.bundlerepository(self.repo.baseui, self.repo.root,
107 107 self.vfs.join(self.fname))
108 def writebundle(self, cg):
108 def writebundle(self, bases, node):
109 cg = changegroup.changegroupsubset(self.repo, bases, [node], 'shelve')
109 110 changegroup.writebundle(self.ui, cg, self.fname, 'HG10BZ', self.vfs)
110 111
111 112 class shelvedstate(object):
112 113 """Handle persistence during unshelving operations.
113 114
114 115 Handles saving and restoring a shelved state. Ensures that different
115 116 versions of a shelved state are possible and handles them appropriately.
116 117 """
117 118 _version = 1
118 119 _filename = 'shelvedstate'
119 120
120 121 @classmethod
121 122 def load(cls, repo):
122 123 fp = repo.vfs(cls._filename)
123 124 try:
124 125 version = int(fp.readline().strip())
125 126
126 127 if version != cls._version:
127 128 raise util.Abort(_('this version of shelve is incompatible '
128 129 'with the version used in this repo'))
129 130 name = fp.readline().strip()
130 131 wctx = fp.readline().strip()
131 132 pendingctx = fp.readline().strip()
132 133 parents = [bin(h) for h in fp.readline().split()]
133 134 stripnodes = [bin(h) for h in fp.readline().split()]
134 135 finally:
135 136 fp.close()
136 137
137 138 obj = cls()
138 139 obj.name = name
139 140 obj.wctx = repo[bin(wctx)]
140 141 obj.pendingctx = repo[bin(pendingctx)]
141 142 obj.parents = parents
142 143 obj.stripnodes = stripnodes
143 144
144 145 return obj
145 146
146 147 @classmethod
147 148 def save(cls, repo, name, originalwctx, pendingctx, stripnodes):
148 149 fp = repo.vfs(cls._filename, 'wb')
149 150 fp.write('%i\n' % cls._version)
150 151 fp.write('%s\n' % name)
151 152 fp.write('%s\n' % hex(originalwctx.node()))
152 153 fp.write('%s\n' % hex(pendingctx.node()))
153 154 fp.write('%s\n' % ' '.join([hex(p) for p in repo.dirstate.parents()]))
154 155 fp.write('%s\n' % ' '.join([hex(n) for n in stripnodes]))
155 156 fp.close()
156 157
157 158 @classmethod
158 159 def clear(cls, repo):
159 160 util.unlinkpath(repo.join(cls._filename), ignoremissing=True)
160 161
161 162 def cleanupoldbackups(repo):
162 163 vfs = scmutil.vfs(repo.join(backupdir))
163 164 maxbackups = repo.ui.configint('shelve', 'maxbackups', 10)
164 165 hgfiles = [f for f in vfs.listdir() if f.endswith('.hg')]
165 166 hgfiles = sorted([(vfs.stat(f).st_mtime, f) for f in hgfiles])
166 167 if 0 < maxbackups and maxbackups < len(hgfiles):
167 168 bordermtime = hgfiles[-maxbackups][0]
168 169 else:
169 170 bordermtime = None
170 171 for mtime, f in hgfiles[:len(hgfiles) - maxbackups]:
171 172 if mtime == bordermtime:
172 173 # keep it, because timestamp can't decide exact order of backups
173 174 continue
174 175 base = f[:-3]
175 176 for ext in 'hg patch'.split():
176 177 try:
177 178 vfs.unlink(base + '.' + ext)
178 179 except OSError as err:
179 180 if err.errno != errno.ENOENT:
180 181 raise
181 182
182 183 def createcmd(ui, repo, pats, opts):
183 184 """subcommand that creates a new shelve"""
184 185
185 186 def publicancestors(ctx):
186 187 """Compute the public ancestors of a commit.
187 188
188 189 Much faster than the revset ancestors(ctx) & draft()"""
189 190 seen = set([nullrev])
190 191 visit = collections.deque()
191 192 visit.append(ctx)
192 193 while visit:
193 194 ctx = visit.popleft()
194 195 yield ctx.node()
195 196 for parent in ctx.parents():
196 197 rev = parent.rev()
197 198 if rev not in seen:
198 199 seen.add(rev)
199 200 if parent.mutable():
200 201 visit.append(parent)
201 202
202 203 wctx = repo[None]
203 204 parents = wctx.parents()
204 205 if len(parents) > 1:
205 206 raise util.Abort(_('cannot shelve while merging'))
206 207 parent = parents[0]
207 208
208 209 # we never need the user, so we use a generic user for all shelve operations
209 210 user = 'shelve@localhost'
210 211 label = repo._activebookmark or parent.branch() or 'default'
211 212
212 213 # slashes aren't allowed in filenames, therefore we rename it
213 214 label = label.replace('/', '_')
214 215
215 216 def gennames():
216 217 yield label
217 218 for i in xrange(1, 100):
218 219 yield '%s-%02d' % (label, i)
219 220
220 221 def commitfunc(ui, repo, message, match, opts):
221 222 hasmq = util.safehasattr(repo, 'mq')
222 223 if hasmq:
223 224 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
224 225 backup = repo.ui.backupconfig('phases', 'new-commit')
225 226 try:
226 227 repo.ui. setconfig('phases', 'new-commit', phases.secret)
227 228 editor = cmdutil.getcommiteditor(editform='shelve.shelve', **opts)
228 229 return repo.commit(message, user, opts.get('date'), match,
229 230 editor=editor)
230 231 finally:
231 232 repo.ui.restoreconfig(backup)
232 233 if hasmq:
233 234 repo.mq.checkapplied = saved
234 235
235 236 if parent.node() != nullid:
236 237 desc = "changes to '%s'" % parent.description().split('\n', 1)[0]
237 238 else:
238 239 desc = '(changes in empty repository)'
239 240
240 241 if not opts['message']:
241 242 opts['message'] = desc
242 243
243 244 name = opts['name']
244 245
245 246 wlock = lock = tr = bms = None
246 247 try:
247 248 wlock = repo.wlock()
248 249 lock = repo.lock()
249 250
250 251 bms = repo._bookmarks.copy()
251 252 # use an uncommitted transaction to generate the bundle to avoid
252 253 # pull races. ensure we don't print the abort message to stderr.
253 254 tr = repo.transaction('commit', report=lambda x: None)
254 255
255 256 if name:
256 257 if shelvedfile(repo, name, 'hg').exists():
257 258 raise util.Abort(_("a shelved change named '%s' already exists")
258 259 % name)
259 260 else:
260 261 for n in gennames():
261 262 if not shelvedfile(repo, n, 'hg').exists():
262 263 name = n
263 264 break
264 265 else:
265 266 raise util.Abort(_("too many shelved changes named '%s'") %
266 267 label)
267 268
268 269 # ensure we are not creating a subdirectory or a hidden file
269 270 if '/' in name or '\\' in name:
270 271 raise util.Abort(_('shelved change names may not contain slashes'))
271 272 if name.startswith('.'):
272 273 raise util.Abort(_("shelved change names may not start with '.'"))
273 274 interactive = opts.get('interactive', False)
274 275
275 276 def interactivecommitfunc(ui, repo, *pats, **opts):
276 277 match = scmutil.match(repo['.'], pats, {})
277 278 message = opts['message']
278 279 return commitfunc(ui, repo, message, match, opts)
279 280 if not interactive:
280 281 node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
281 282 else:
282 283 node = cmdutil.dorecord(ui, repo, interactivecommitfunc, None,
283 284 False, cmdutil.recordfilter, *pats, **opts)
284 285 if not node:
285 286 stat = repo.status(match=scmutil.match(repo[None], pats, opts))
286 287 if stat.deleted:
287 288 ui.status(_("nothing changed (%d missing files, see "
288 289 "'hg status')\n") % len(stat.deleted))
289 290 else:
290 291 ui.status(_("nothing changed\n"))
291 292 return 1
292 293
293 294 bases = list(publicancestors(repo[node]))
294 cg = changegroup.changegroupsubset(repo, bases, [node], 'shelve')
295 shelvedfile(repo, name, 'hg').writebundle(cg)
295 shelvedfile(repo, name, 'hg').writebundle(bases, node)
296 296 cmdutil.export(repo, [node],
297 297 fp=shelvedfile(repo, name, 'patch').opener('wb'),
298 298 opts=mdiff.diffopts(git=True))
299 299
300 300
301 301 if ui.formatted():
302 302 desc = util.ellipsis(desc, ui.termwidth())
303 303 ui.status(_('shelved as %s\n') % name)
304 304 hg.update(repo, parent.node())
305 305 finally:
306 306 if bms:
307 307 # restore old bookmarks
308 308 repo._bookmarks.update(bms)
309 309 repo._bookmarks.write()
310 310 if tr:
311 311 tr.abort()
312 312 lockmod.release(lock, wlock)
313 313
314 314 def cleanupcmd(ui, repo):
315 315 """subcommand that deletes all shelves"""
316 316
317 317 wlock = None
318 318 try:
319 319 wlock = repo.wlock()
320 320 for (name, _type) in repo.vfs.readdir('shelved'):
321 321 suffix = name.rsplit('.', 1)[-1]
322 322 if suffix in ('hg', 'patch'):
323 323 shelvedfile(repo, name).movetobackup()
324 324 cleanupoldbackups(repo)
325 325 finally:
326 326 lockmod.release(wlock)
327 327
328 328 def deletecmd(ui, repo, pats):
329 329 """subcommand that deletes a specific shelve"""
330 330 if not pats:
331 331 raise util.Abort(_('no shelved changes specified!'))
332 332 wlock = repo.wlock()
333 333 try:
334 334 for name in pats:
335 335 for suffix in 'hg patch'.split():
336 336 shelvedfile(repo, name, suffix).movetobackup()
337 337 cleanupoldbackups(repo)
338 338 except OSError as err:
339 339 if err.errno != errno.ENOENT:
340 340 raise
341 341 raise util.Abort(_("shelved change '%s' not found") % name)
342 342 finally:
343 343 lockmod.release(wlock)
344 344
345 345 def listshelves(repo):
346 346 """return all shelves in repo as list of (time, filename)"""
347 347 try:
348 348 names = repo.vfs.readdir('shelved')
349 349 except OSError as err:
350 350 if err.errno != errno.ENOENT:
351 351 raise
352 352 return []
353 353 info = []
354 354 for (name, _type) in names:
355 355 pfx, sfx = name.rsplit('.', 1)
356 356 if not pfx or sfx != 'patch':
357 357 continue
358 358 st = shelvedfile(repo, name).stat()
359 359 info.append((st.st_mtime, shelvedfile(repo, pfx).filename()))
360 360 return sorted(info, reverse=True)
361 361
362 362 def listcmd(ui, repo, pats, opts):
363 363 """subcommand that displays the list of shelves"""
364 364 pats = set(pats)
365 365 width = 80
366 366 if not ui.plain():
367 367 width = ui.termwidth()
368 368 namelabel = 'shelve.newest'
369 369 for mtime, name in listshelves(repo):
370 370 sname = util.split(name)[1]
371 371 if pats and sname not in pats:
372 372 continue
373 373 ui.write(sname, label=namelabel)
374 374 namelabel = 'shelve.name'
375 375 if ui.quiet:
376 376 ui.write('\n')
377 377 continue
378 378 ui.write(' ' * (16 - len(sname)))
379 379 used = 16
380 380 age = '(%s)' % templatefilters.age(util.makedate(mtime), abbrev=True)
381 381 ui.write(age, label='shelve.age')
382 382 ui.write(' ' * (12 - len(age)))
383 383 used += 12
384 384 fp = open(name + '.patch', 'rb')
385 385 try:
386 386 while True:
387 387 line = fp.readline()
388 388 if not line:
389 389 break
390 390 if not line.startswith('#'):
391 391 desc = line.rstrip()
392 392 if ui.formatted():
393 393 desc = util.ellipsis(desc, width - used)
394 394 ui.write(desc)
395 395 break
396 396 ui.write('\n')
397 397 if not (opts['patch'] or opts['stat']):
398 398 continue
399 399 difflines = fp.readlines()
400 400 if opts['patch']:
401 401 for chunk, label in patch.difflabel(iter, difflines):
402 402 ui.write(chunk, label=label)
403 403 if opts['stat']:
404 404 for chunk, label in patch.diffstatui(difflines, width=width,
405 405 git=True):
406 406 ui.write(chunk, label=label)
407 407 finally:
408 408 fp.close()
409 409
410 410 def singlepatchcmds(ui, repo, pats, opts, subcommand):
411 411 """subcommand that displays a single shelf"""
412 412 if len(pats) != 1:
413 413 raise util.Abort(_("--%s expects a single shelf") % subcommand)
414 414 shelfname = pats[0]
415 415
416 416 if not shelvedfile(repo, shelfname, 'patch').exists():
417 417 raise util.Abort(_("cannot find shelf %s") % shelfname)
418 418
419 419 listcmd(ui, repo, pats, opts)
420 420
421 421 def checkparents(repo, state):
422 422 """check parent while resuming an unshelve"""
423 423 if state.parents != repo.dirstate.parents():
424 424 raise util.Abort(_('working directory parents do not match unshelve '
425 425 'state'))
426 426
427 427 def pathtofiles(repo, files):
428 428 cwd = repo.getcwd()
429 429 return [repo.pathto(f, cwd) for f in files]
430 430
431 431 def unshelveabort(ui, repo, state, opts):
432 432 """subcommand that abort an in-progress unshelve"""
433 433 wlock = repo.wlock()
434 434 lock = None
435 435 try:
436 436 checkparents(repo, state)
437 437
438 438 util.rename(repo.join('unshelverebasestate'),
439 439 repo.join('rebasestate'))
440 440 try:
441 441 rebase.rebase(ui, repo, **{
442 442 'abort' : True
443 443 })
444 444 except Exception:
445 445 util.rename(repo.join('rebasestate'),
446 446 repo.join('unshelverebasestate'))
447 447 raise
448 448
449 449 lock = repo.lock()
450 450
451 451 mergefiles(ui, repo, state.wctx, state.pendingctx)
452 452
453 453 repair.strip(ui, repo, state.stripnodes, backup=False, topic='shelve')
454 454 shelvedstate.clear(repo)
455 455 ui.warn(_("unshelve of '%s' aborted\n") % state.name)
456 456 finally:
457 457 lockmod.release(lock, wlock)
458 458
459 459 def mergefiles(ui, repo, wctx, shelvectx):
460 460 """updates to wctx and merges the changes from shelvectx into the
461 461 dirstate."""
462 462 oldquiet = ui.quiet
463 463 try:
464 464 ui.quiet = True
465 465 hg.update(repo, wctx.node())
466 466 files = []
467 467 files.extend(shelvectx.files())
468 468 files.extend(shelvectx.parents()[0].files())
469 469
470 470 # revert will overwrite unknown files, so move them out of the way
471 471 for file in repo.status(unknown=True).unknown:
472 472 if file in files:
473 473 util.rename(file, file + ".orig")
474 474 ui.pushbuffer(True)
475 475 cmdutil.revert(ui, repo, shelvectx, repo.dirstate.parents(),
476 476 *pathtofiles(repo, files),
477 477 **{'no_backup': True})
478 478 ui.popbuffer()
479 479 finally:
480 480 ui.quiet = oldquiet
481 481
482 482 def unshelvecleanup(ui, repo, name, opts):
483 483 """remove related files after an unshelve"""
484 484 if not opts['keep']:
485 485 for filetype in 'hg patch'.split():
486 486 shelvedfile(repo, name, filetype).movetobackup()
487 487 cleanupoldbackups(repo)
488 488
489 489 def unshelvecontinue(ui, repo, state, opts):
490 490 """subcommand to continue an in-progress unshelve"""
491 491 # We're finishing off a merge. First parent is our original
492 492 # parent, second is the temporary "fake" commit we're unshelving.
493 493 wlock = repo.wlock()
494 494 lock = None
495 495 try:
496 496 checkparents(repo, state)
497 497 ms = merge.mergestate(repo)
498 498 if [f for f in ms if ms[f] == 'u']:
499 499 raise util.Abort(
500 500 _("unresolved conflicts, can't continue"),
501 501 hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
502 502
503 503 lock = repo.lock()
504 504
505 505 util.rename(repo.join('unshelverebasestate'),
506 506 repo.join('rebasestate'))
507 507 try:
508 508 rebase.rebase(ui, repo, **{
509 509 'continue' : True
510 510 })
511 511 except Exception:
512 512 util.rename(repo.join('rebasestate'),
513 513 repo.join('unshelverebasestate'))
514 514 raise
515 515
516 516 shelvectx = repo['tip']
517 517 if not shelvectx in state.pendingctx.children():
518 518 # rebase was a no-op, so it produced no child commit
519 519 shelvectx = state.pendingctx
520 520 else:
521 521 # only strip the shelvectx if the rebase produced it
522 522 state.stripnodes.append(shelvectx.node())
523 523
524 524 mergefiles(ui, repo, state.wctx, shelvectx)
525 525
526 526 repair.strip(ui, repo, state.stripnodes, backup=False, topic='shelve')
527 527 shelvedstate.clear(repo)
528 528 unshelvecleanup(ui, repo, state.name, opts)
529 529 ui.status(_("unshelve of '%s' complete\n") % state.name)
530 530 finally:
531 531 lockmod.release(lock, wlock)
532 532
533 533 @command('unshelve',
534 534 [('a', 'abort', None,
535 535 _('abort an incomplete unshelve operation')),
536 536 ('c', 'continue', None,
537 537 _('continue an incomplete unshelve operation')),
538 538 ('', 'keep', None,
539 539 _('keep shelve after unshelving')),
540 540 ('', 'date', '',
541 541 _('set date for temporary commits (DEPRECATED)'), _('DATE'))],
542 542 _('hg unshelve [SHELVED]'))
543 543 def unshelve(ui, repo, *shelved, **opts):
544 544 """restore a shelved change to the working directory
545 545
546 546 This command accepts an optional name of a shelved change to
547 547 restore. If none is given, the most recent shelved change is used.
548 548
549 549 If a shelved change is applied successfully, the bundle that
550 550 contains the shelved changes is moved to a backup location
551 551 (.hg/shelve-backup).
552 552
553 553 Since you can restore a shelved change on top of an arbitrary
554 554 commit, it is possible that unshelving will result in a conflict
555 555 between your changes and the commits you are unshelving onto. If
556 556 this occurs, you must resolve the conflict, then use
557 557 ``--continue`` to complete the unshelve operation. (The bundle
558 558 will not be moved until you successfully complete the unshelve.)
559 559
560 560 (Alternatively, you can use ``--abort`` to abandon an unshelve
561 561 that causes a conflict. This reverts the unshelved changes, and
562 562 leaves the bundle in place.)
563 563
564 564 After a successful unshelve, the shelved changes are stored in a
565 565 backup directory. Only the N most recent backups are kept. N
566 566 defaults to 10 but can be overridden using the ``shelve.maxbackups``
567 567 configuration option.
568 568
569 569 .. container:: verbose
570 570
571 571 Timestamp in seconds is used to decide order of backups. More
572 572 than ``maxbackups`` backups are kept, if same timestamp
573 573 prevents from deciding exact order of them, for safety.
574 574 """
575 575 abortf = opts['abort']
576 576 continuef = opts['continue']
577 577 if not abortf and not continuef:
578 578 cmdutil.checkunfinished(repo)
579 579
580 580 if abortf or continuef:
581 581 if abortf and continuef:
582 582 raise util.Abort(_('cannot use both abort and continue'))
583 583 if shelved:
584 584 raise util.Abort(_('cannot combine abort/continue with '
585 585 'naming a shelved change'))
586 586
587 587 try:
588 588 state = shelvedstate.load(repo)
589 589 except IOError as err:
590 590 if err.errno != errno.ENOENT:
591 591 raise
592 592 raise util.Abort(_('no unshelve operation underway'))
593 593
594 594 if abortf:
595 595 return unshelveabort(ui, repo, state, opts)
596 596 elif continuef:
597 597 return unshelvecontinue(ui, repo, state, opts)
598 598 elif len(shelved) > 1:
599 599 raise util.Abort(_('can only unshelve one change at a time'))
600 600 elif not shelved:
601 601 shelved = listshelves(repo)
602 602 if not shelved:
603 603 raise util.Abort(_('no shelved changes to apply!'))
604 604 basename = util.split(shelved[0][1])[1]
605 605 ui.status(_("unshelving change '%s'\n") % basename)
606 606 else:
607 607 basename = shelved[0]
608 608
609 609 if not shelvedfile(repo, basename, 'patch').exists():
610 610 raise util.Abort(_("shelved change '%s' not found") % basename)
611 611
612 612 oldquiet = ui.quiet
613 613 wlock = lock = tr = None
614 614 try:
615 615 wlock = repo.wlock()
616 616 lock = repo.lock()
617 617
618 618 tr = repo.transaction('unshelve', report=lambda x: None)
619 619 oldtiprev = len(repo)
620 620
621 621 pctx = repo['.']
622 622 tmpwctx = pctx
623 623 # The goal is to have a commit structure like so:
624 624 # ...-> pctx -> tmpwctx -> shelvectx
625 625 # where tmpwctx is an optional commit with the user's pending changes
626 626 # and shelvectx is the unshelved changes. Then we merge it all down
627 627 # to the original pctx.
628 628
629 629 # Store pending changes in a commit
630 630 s = repo.status()
631 631 if s.modified or s.added or s.removed or s.deleted:
632 632 ui.status(_("temporarily committing pending changes "
633 633 "(restore with 'hg unshelve --abort')\n"))
634 634 def commitfunc(ui, repo, message, match, opts):
635 635 hasmq = util.safehasattr(repo, 'mq')
636 636 if hasmq:
637 637 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
638 638
639 639 backup = repo.ui.backupconfig('phases', 'new-commit')
640 640 try:
641 641 repo.ui. setconfig('phases', 'new-commit', phases.secret)
642 642 return repo.commit(message, 'shelve@localhost',
643 643 opts.get('date'), match)
644 644 finally:
645 645 repo.ui.restoreconfig(backup)
646 646 if hasmq:
647 647 repo.mq.checkapplied = saved
648 648
649 649 tempopts = {}
650 650 tempopts['message'] = "pending changes temporary commit"
651 651 tempopts['date'] = opts.get('date')
652 652 ui.quiet = True
653 653 node = cmdutil.commit(ui, repo, commitfunc, [], tempopts)
654 654 tmpwctx = repo[node]
655 655
656 656 ui.quiet = True
657 657 shelvedfile(repo, basename, 'hg').applybundle()
658 658
659 659 ui.quiet = oldquiet
660 660
661 661 shelvectx = repo['tip']
662 662
663 663 # If the shelve is not immediately on top of the commit
664 664 # we'll be merging with, rebase it to be on top.
665 665 if tmpwctx.node() != shelvectx.parents()[0].node():
666 666 ui.status(_('rebasing shelved changes\n'))
667 667 try:
668 668 rebase.rebase(ui, repo, **{
669 669 'rev' : [shelvectx.rev()],
670 670 'dest' : str(tmpwctx.rev()),
671 671 'keep' : True,
672 672 })
673 673 except error.InterventionRequired:
674 674 tr.close()
675 675
676 676 stripnodes = [repo.changelog.node(rev)
677 677 for rev in xrange(oldtiprev, len(repo))]
678 678 shelvedstate.save(repo, basename, pctx, tmpwctx, stripnodes)
679 679
680 680 util.rename(repo.join('rebasestate'),
681 681 repo.join('unshelverebasestate'))
682 682 raise error.InterventionRequired(
683 683 _("unresolved conflicts (see 'hg resolve', then "
684 684 "'hg unshelve --continue')"))
685 685
686 686 # refresh ctx after rebase completes
687 687 shelvectx = repo['tip']
688 688
689 689 if not shelvectx in tmpwctx.children():
690 690 # rebase was a no-op, so it produced no child commit
691 691 shelvectx = tmpwctx
692 692
693 693 mergefiles(ui, repo, pctx, shelvectx)
694 694 shelvedstate.clear(repo)
695 695
696 696 # The transaction aborting will strip all the commits for us,
697 697 # but it doesn't update the inmemory structures, so addchangegroup
698 698 # hooks still fire and try to operate on the missing commits.
699 699 # Clean up manually to prevent this.
700 700 repo.unfiltered().changelog.strip(oldtiprev, tr)
701 701
702 702 unshelvecleanup(ui, repo, basename, opts)
703 703 finally:
704 704 ui.quiet = oldquiet
705 705 if tr:
706 706 tr.release()
707 707 lockmod.release(lock, wlock)
708 708
709 709 @command('shelve',
710 710 [('A', 'addremove', None,
711 711 _('mark new/missing files as added/removed before shelving')),
712 712 ('', 'cleanup', None,
713 713 _('delete all shelved changes')),
714 714 ('', 'date', '',
715 715 _('shelve with the specified commit date'), _('DATE')),
716 716 ('d', 'delete', None,
717 717 _('delete the named shelved change(s)')),
718 718 ('e', 'edit', False,
719 719 _('invoke editor on commit messages')),
720 720 ('l', 'list', None,
721 721 _('list current shelves')),
722 722 ('m', 'message', '',
723 723 _('use text as shelve message'), _('TEXT')),
724 724 ('n', 'name', '',
725 725 _('use the given name for the shelved commit'), _('NAME')),
726 726 ('p', 'patch', None,
727 727 _('show patch')),
728 728 ('i', 'interactive', None,
729 729 _('interactive mode, only works while creating a shelve')),
730 730 ('', 'stat', None,
731 731 _('output diffstat-style summary of changes'))] + commands.walkopts,
732 732 _('hg shelve [OPTION]... [FILE]...'))
733 733 def shelvecmd(ui, repo, *pats, **opts):
734 734 '''save and set aside changes from the working directory
735 735
736 736 Shelving takes files that "hg status" reports as not clean, saves
737 737 the modifications to a bundle (a shelved change), and reverts the
738 738 files so that their state in the working directory becomes clean.
739 739
740 740 To restore these changes to the working directory, using "hg
741 741 unshelve"; this will work even if you switch to a different
742 742 commit.
743 743
744 744 When no files are specified, "hg shelve" saves all not-clean
745 745 files. If specific files or directories are named, only changes to
746 746 those files are shelved.
747 747
748 748 Each shelved change has a name that makes it easier to find later.
749 749 The name of a shelved change defaults to being based on the active
750 750 bookmark, or if there is no active bookmark, the current named
751 751 branch. To specify a different name, use ``--name``.
752 752
753 753 To see a list of existing shelved changes, use the ``--list``
754 754 option. For each shelved change, this will print its name, age,
755 755 and description; use ``--patch`` or ``--stat`` for more details.
756 756
757 757 To delete specific shelved changes, use ``--delete``. To delete
758 758 all shelved changes, use ``--cleanup``.
759 759 '''
760 760 cmdutil.checkunfinished(repo)
761 761
762 762 allowables = [
763 763 ('addremove', set(['create'])), # 'create' is pseudo action
764 764 ('cleanup', set(['cleanup'])),
765 765 # ('date', set(['create'])), # ignored for passing '--date "0 0"' in tests
766 766 ('delete', set(['delete'])),
767 767 ('edit', set(['create'])),
768 768 ('list', set(['list'])),
769 769 ('message', set(['create'])),
770 770 ('name', set(['create'])),
771 771 ('patch', set(['patch', 'list'])),
772 772 ('stat', set(['stat', 'list'])),
773 773 ]
774 774 def checkopt(opt):
775 775 if opts[opt]:
776 776 for i, allowable in allowables:
777 777 if opts[i] and opt not in allowable:
778 778 raise util.Abort(_("options '--%s' and '--%s' may not be "
779 779 "used together") % (opt, i))
780 780 return True
781 781 if checkopt('cleanup'):
782 782 if pats:
783 783 raise util.Abort(_("cannot specify names when using '--cleanup'"))
784 784 return cleanupcmd(ui, repo)
785 785 elif checkopt('delete'):
786 786 return deletecmd(ui, repo, pats)
787 787 elif checkopt('list'):
788 788 return listcmd(ui, repo, pats, opts)
789 789 elif checkopt('patch'):
790 790 return singlepatchcmds(ui, repo, pats, opts, subcommand='patch')
791 791 elif checkopt('stat'):
792 792 return singlepatchcmds(ui, repo, pats, opts, subcommand='stat')
793 793 else:
794 794 return createcmd(ui, repo, pats, opts)
795 795
796 796 def extsetup(ui):
797 797 cmdutil.unfinishedstates.append(
798 798 [shelvedstate._filename, False, False,
799 799 _('unshelve already in progress'),
800 800 _("use 'hg unshelve --continue' or 'hg unshelve --abort'")])
General Comments 0
You need to be logged in to leave comments. Login now