##// END OF EJS Templates
rebase: make sure merge state is cleaned up for no-op rebases (issue5494)...
Jeremy Fitzhardinge -
r32313:a580b2d6 default
parent child Browse files
Show More
@@ -1,1536 +1,1541 b''
1 1 # rebase.py - rebasing feature for mercurial
2 2 #
3 3 # Copyright 2008 Stefano Tortarolo <stefano.tortarolo at gmail dot com>
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 '''command to move sets of revisions to a different ancestor
9 9
10 10 This extension lets you rebase changesets in an existing Mercurial
11 11 repository.
12 12
13 13 For more information:
14 14 https://mercurial-scm.org/wiki/RebaseExtension
15 15 '''
16 16
17 17 from __future__ import absolute_import
18 18
19 19 import errno
20 20 import os
21 21
22 22 from mercurial.i18n import _
23 23 from mercurial.node import (
24 24 hex,
25 25 nullid,
26 26 nullrev,
27 27 short,
28 28 )
29 29 from mercurial import (
30 30 bookmarks,
31 31 cmdutil,
32 32 commands,
33 33 copies,
34 34 destutil,
35 35 dirstateguard,
36 36 error,
37 37 extensions,
38 38 hg,
39 39 lock,
40 40 merge as mergemod,
41 41 mergeutil,
42 42 obsolete,
43 43 patch,
44 44 phases,
45 45 registrar,
46 46 repair,
47 47 repoview,
48 48 revset,
49 49 scmutil,
50 50 smartset,
51 51 util,
52 52 )
53 53
54 54 release = lock.release
55 55 templateopts = commands.templateopts
56 56
57 57 # The following constants are used throughout the rebase module. The ordering of
58 58 # their values must be maintained.
59 59
60 60 # Indicates that a revision needs to be rebased
61 61 revtodo = -1
62 62 nullmerge = -2
63 63 revignored = -3
64 64 # successor in rebase destination
65 65 revprecursor = -4
66 66 # plain prune (no successor)
67 67 revpruned = -5
68 68 revskipped = (revignored, revprecursor, revpruned)
69 69
70 70 cmdtable = {}
71 71 command = cmdutil.command(cmdtable)
72 72 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
73 73 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
74 74 # be specifying the version(s) of Mercurial they are tested with, or
75 75 # leave the attribute unspecified.
76 76 testedwith = 'ships-with-hg-core'
77 77
78 78 def _nothingtorebase():
79 79 return 1
80 80
81 81 def _savegraft(ctx, extra):
82 82 s = ctx.extra().get('source', None)
83 83 if s is not None:
84 84 extra['source'] = s
85 85 s = ctx.extra().get('intermediate-source', None)
86 86 if s is not None:
87 87 extra['intermediate-source'] = s
88 88
89 89 def _savebranch(ctx, extra):
90 90 extra['branch'] = ctx.branch()
91 91
92 92 def _makeextrafn(copiers):
93 93 """make an extrafn out of the given copy-functions.
94 94
95 95 A copy function takes a context and an extra dict, and mutates the
96 96 extra dict as needed based on the given context.
97 97 """
98 98 def extrafn(ctx, extra):
99 99 for c in copiers:
100 100 c(ctx, extra)
101 101 return extrafn
102 102
103 103 def _destrebase(repo, sourceset, destspace=None):
104 104 """small wrapper around destmerge to pass the right extra args
105 105
106 106 Please wrap destutil.destmerge instead."""
107 107 return destutil.destmerge(repo, action='rebase', sourceset=sourceset,
108 108 onheadcheck=False, destspace=destspace)
109 109
110 110 revsetpredicate = registrar.revsetpredicate()
111 111
112 112 @revsetpredicate('_destrebase')
113 113 def _revsetdestrebase(repo, subset, x):
114 114 # ``_rebasedefaultdest()``
115 115
116 116 # default destination for rebase.
117 117 # # XXX: Currently private because I expect the signature to change.
118 118 # # XXX: - bailing out in case of ambiguity vs returning all data.
119 119 # i18n: "_rebasedefaultdest" is a keyword
120 120 sourceset = None
121 121 if x is not None:
122 122 sourceset = revset.getset(repo, smartset.fullreposet(repo), x)
123 123 return subset & smartset.baseset([_destrebase(repo, sourceset)])
124 124
125 125 class rebaseruntime(object):
126 126 """This class is a container for rebase runtime state"""
127 127 def __init__(self, repo, ui, opts=None):
128 128 if opts is None:
129 129 opts = {}
130 130
131 131 self.repo = repo
132 132 self.ui = ui
133 133 self.opts = opts
134 134 self.originalwd = None
135 135 self.external = nullrev
136 136 # Mapping between the old revision id and either what is the new rebased
137 137 # revision or what needs to be done with the old revision. The state
138 138 # dict will be what contains most of the rebase progress state.
139 139 self.state = {}
140 140 self.activebookmark = None
141 141 self.currentbookmarks = None
142 142 self.dest = None
143 143 self.skipped = set()
144 144 self.destancestors = set()
145 145
146 146 self.collapsef = opts.get('collapse', False)
147 147 self.collapsemsg = cmdutil.logmessage(ui, opts)
148 148 self.date = opts.get('date', None)
149 149
150 150 e = opts.get('extrafn') # internal, used by e.g. hgsubversion
151 151 self.extrafns = [_savegraft]
152 152 if e:
153 153 self.extrafns = [e]
154 154
155 155 self.keepf = opts.get('keep', False)
156 156 self.keepbranchesf = opts.get('keepbranches', False)
157 157 # keepopen is not meant for use on the command line, but by
158 158 # other extensions
159 159 self.keepopen = opts.get('keepopen', False)
160 160 self.obsoletenotrebased = {}
161 161
162 162 def storestatus(self, tr=None):
163 163 """Store the current status to allow recovery"""
164 164 if tr:
165 165 tr.addfilegenerator('rebasestate', ('rebasestate',),
166 166 self._writestatus, location='plain')
167 167 else:
168 168 with self.repo.vfs("rebasestate", "w") as f:
169 169 self._writestatus(f)
170 170
171 171 def _writestatus(self, f):
172 172 repo = self.repo.unfiltered()
173 173 f.write(repo[self.originalwd].hex() + '\n')
174 174 f.write(repo[self.dest].hex() + '\n')
175 175 f.write(repo[self.external].hex() + '\n')
176 176 f.write('%d\n' % int(self.collapsef))
177 177 f.write('%d\n' % int(self.keepf))
178 178 f.write('%d\n' % int(self.keepbranchesf))
179 179 f.write('%s\n' % (self.activebookmark or ''))
180 180 for d, v in self.state.iteritems():
181 181 oldrev = repo[d].hex()
182 182 if v >= 0:
183 183 newrev = repo[v].hex()
184 184 elif v == revtodo:
185 185 # To maintain format compatibility, we have to use nullid.
186 186 # Please do remove this special case when upgrading the format.
187 187 newrev = hex(nullid)
188 188 else:
189 189 newrev = v
190 190 f.write("%s:%s\n" % (oldrev, newrev))
191 191 repo.ui.debug('rebase status stored\n')
192 192
193 193 def restorestatus(self):
194 194 """Restore a previously stored status"""
195 195 repo = self.repo
196 196 keepbranches = None
197 197 dest = None
198 198 collapse = False
199 199 external = nullrev
200 200 activebookmark = None
201 201 state = {}
202 202
203 203 try:
204 204 f = repo.vfs("rebasestate")
205 205 for i, l in enumerate(f.read().splitlines()):
206 206 if i == 0:
207 207 originalwd = repo[l].rev()
208 208 elif i == 1:
209 209 dest = repo[l].rev()
210 210 elif i == 2:
211 211 external = repo[l].rev()
212 212 elif i == 3:
213 213 collapse = bool(int(l))
214 214 elif i == 4:
215 215 keep = bool(int(l))
216 216 elif i == 5:
217 217 keepbranches = bool(int(l))
218 218 elif i == 6 and not (len(l) == 81 and ':' in l):
219 219 # line 6 is a recent addition, so for backwards
220 220 # compatibility check that the line doesn't look like the
221 221 # oldrev:newrev lines
222 222 activebookmark = l
223 223 else:
224 224 oldrev, newrev = l.split(':')
225 225 if newrev in (str(nullmerge), str(revignored),
226 226 str(revprecursor), str(revpruned)):
227 227 state[repo[oldrev].rev()] = int(newrev)
228 228 elif newrev == nullid:
229 229 state[repo[oldrev].rev()] = revtodo
230 230 # Legacy compat special case
231 231 else:
232 232 state[repo[oldrev].rev()] = repo[newrev].rev()
233 233
234 234 except IOError as err:
235 235 if err.errno != errno.ENOENT:
236 236 raise
237 237 cmdutil.wrongtooltocontinue(repo, _('rebase'))
238 238
239 239 if keepbranches is None:
240 240 raise error.Abort(_('.hg/rebasestate is incomplete'))
241 241
242 242 skipped = set()
243 243 # recompute the set of skipped revs
244 244 if not collapse:
245 245 seen = {dest}
246 246 for old, new in sorted(state.items()):
247 247 if new != revtodo and new in seen:
248 248 skipped.add(old)
249 249 seen.add(new)
250 250 repo.ui.debug('computed skipped revs: %s\n' %
251 251 (' '.join(str(r) for r in sorted(skipped)) or None))
252 252 repo.ui.debug('rebase status resumed\n')
253 253 _setrebasesetvisibility(repo, set(state.keys()) | {originalwd})
254 254
255 255 self.originalwd = originalwd
256 256 self.dest = dest
257 257 self.state = state
258 258 self.skipped = skipped
259 259 self.collapsef = collapse
260 260 self.keepf = keep
261 261 self.keepbranchesf = keepbranches
262 262 self.external = external
263 263 self.activebookmark = activebookmark
264 264
265 265 def _handleskippingobsolete(self, rebaserevs, obsoleterevs, dest):
266 266 """Compute structures necessary for skipping obsolete revisions
267 267
268 268 rebaserevs: iterable of all revisions that are to be rebased
269 269 obsoleterevs: iterable of all obsolete revisions in rebaseset
270 270 dest: a destination revision for the rebase operation
271 271 """
272 272 self.obsoletenotrebased = {}
273 273 if not self.ui.configbool('experimental', 'rebaseskipobsolete',
274 274 default=True):
275 275 return
276 276 rebaseset = set(rebaserevs)
277 277 obsoleteset = set(obsoleterevs)
278 278 self.obsoletenotrebased = _computeobsoletenotrebased(self.repo,
279 279 obsoleteset, dest)
280 280 skippedset = set(self.obsoletenotrebased)
281 281 _checkobsrebase(self.repo, self.ui, obsoleteset, rebaseset, skippedset)
282 282
283 283 def _prepareabortorcontinue(self, isabort):
284 284 try:
285 285 self.restorestatus()
286 286 self.collapsemsg = restorecollapsemsg(self.repo, isabort)
287 287 except error.RepoLookupError:
288 288 if isabort:
289 289 clearstatus(self.repo)
290 290 clearcollapsemsg(self.repo)
291 291 self.repo.ui.warn(_('rebase aborted (no revision is removed,'
292 292 ' only broken state is cleared)\n'))
293 293 return 0
294 294 else:
295 295 msg = _('cannot continue inconsistent rebase')
296 296 hint = _('use "hg rebase --abort" to clear broken state')
297 297 raise error.Abort(msg, hint=hint)
298 298 if isabort:
299 299 return abort(self.repo, self.originalwd, self.dest,
300 300 self.state, activebookmark=self.activebookmark)
301 301
302 302 obsrevs = (r for r, st in self.state.items() if st == revprecursor)
303 303 self._handleskippingobsolete(self.state.keys(), obsrevs, self.dest)
304 304
305 305 def _preparenewrebase(self, dest, rebaseset):
306 306 if dest is None:
307 307 return _nothingtorebase()
308 308
309 309 allowunstable = obsolete.isenabled(self.repo, obsolete.allowunstableopt)
310 310 if (not (self.keepf or allowunstable)
311 311 and self.repo.revs('first(children(%ld) - %ld)',
312 312 rebaseset, rebaseset)):
313 313 raise error.Abort(
314 314 _("can't remove original changesets with"
315 315 " unrebased descendants"),
316 316 hint=_('use --keep to keep original changesets'))
317 317
318 318 obsrevs = _filterobsoleterevs(self.repo, set(rebaseset))
319 319 self._handleskippingobsolete(rebaseset, obsrevs, dest)
320 320
321 321 result = buildstate(self.repo, dest, rebaseset, self.collapsef,
322 322 self.obsoletenotrebased)
323 323
324 324 if not result:
325 325 # Empty state built, nothing to rebase
326 326 self.ui.status(_('nothing to rebase\n'))
327 327 return _nothingtorebase()
328 328
329 329 for root in self.repo.set('roots(%ld)', rebaseset):
330 330 if not self.keepf and not root.mutable():
331 331 raise error.Abort(_("can't rebase public changeset %s")
332 332 % root,
333 333 hint=_("see 'hg help phases' for details"))
334 334
335 335 (self.originalwd, self.dest, self.state) = result
336 336 if self.collapsef:
337 337 self.destancestors = self.repo.changelog.ancestors(
338 338 [self.dest],
339 339 inclusive=True)
340 340 self.external = externalparent(self.repo, self.state,
341 341 self.destancestors)
342 342
343 343 if dest.closesbranch() and not self.keepbranchesf:
344 344 self.ui.status(_('reopening closed branch head %s\n') % dest)
345 345
346 346 def _performrebase(self, tr):
347 347 repo, ui, opts = self.repo, self.ui, self.opts
348 348 if self.keepbranchesf:
349 349 # insert _savebranch at the start of extrafns so if
350 350 # there's a user-provided extrafn it can clobber branch if
351 351 # desired
352 352 self.extrafns.insert(0, _savebranch)
353 353 if self.collapsef:
354 354 branches = set()
355 355 for rev in self.state:
356 356 branches.add(repo[rev].branch())
357 357 if len(branches) > 1:
358 358 raise error.Abort(_('cannot collapse multiple named '
359 359 'branches'))
360 360
361 361 # Rebase
362 362 if not self.destancestors:
363 363 self.destancestors = repo.changelog.ancestors([self.dest],
364 364 inclusive=True)
365 365
366 366 # Keep track of the current bookmarks in order to reset them later
367 367 self.currentbookmarks = repo._bookmarks.copy()
368 368 self.activebookmark = self.activebookmark or repo._activebookmark
369 369 if self.activebookmark:
370 370 bookmarks.deactivate(repo)
371 371
372 372 # Store the state before we begin so users can run 'hg rebase --abort'
373 373 # if we fail before the transaction closes.
374 374 self.storestatus()
375 375
376 376 sortedrevs = repo.revs('sort(%ld, -topo)', self.state)
377 377 cands = [k for k, v in self.state.iteritems() if v == revtodo]
378 378 total = len(cands)
379 379 pos = 0
380 380 for rev in sortedrevs:
381 381 ctx = repo[rev]
382 382 desc = '%d:%s "%s"' % (ctx.rev(), ctx,
383 383 ctx.description().split('\n', 1)[0])
384 384 names = repo.nodetags(ctx.node()) + repo.nodebookmarks(ctx.node())
385 385 if names:
386 386 desc += ' (%s)' % ' '.join(names)
387 387 if self.state[rev] == rev:
388 388 ui.status(_('already rebased %s\n') % desc)
389 389 elif self.state[rev] == revtodo:
390 390 pos += 1
391 391 ui.status(_('rebasing %s\n') % desc)
392 392 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, ctx)),
393 393 _('changesets'), total)
394 394 p1, p2, base = defineparents(repo, rev, self.dest,
395 395 self.state,
396 396 self.destancestors,
397 397 self.obsoletenotrebased)
398 398 self.storestatus(tr=tr)
399 399 storecollapsemsg(repo, self.collapsemsg)
400 400 if len(repo[None].parents()) == 2:
401 401 repo.ui.debug('resuming interrupted rebase\n')
402 402 else:
403 403 try:
404 404 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
405 405 'rebase')
406 406 stats = rebasenode(repo, rev, p1, base, self.state,
407 407 self.collapsef, self.dest)
408 408 if stats and stats[3] > 0:
409 409 raise error.InterventionRequired(
410 410 _('unresolved conflicts (see hg '
411 411 'resolve, then hg rebase --continue)'))
412 412 finally:
413 413 ui.setconfig('ui', 'forcemerge', '', 'rebase')
414 414 if not self.collapsef:
415 415 merging = p2 != nullrev
416 416 editform = cmdutil.mergeeditform(merging, 'rebase')
417 417 editor = cmdutil.getcommiteditor(editform=editform, **opts)
418 418 newnode = concludenode(repo, rev, p1, p2,
419 419 extrafn=_makeextrafn(self.extrafns),
420 420 editor=editor,
421 421 keepbranches=self.keepbranchesf,
422 422 date=self.date)
423 if newnode is None:
424 # If it ended up being a no-op commit, then the normal
425 # merge state clean-up path doesn't happen, so do it
426 # here. Fix issue5494
427 mergemod.mergestate.clean(repo)
423 428 else:
424 429 # Skip commit if we are collapsing
425 430 repo.dirstate.beginparentchange()
426 431 repo.setparents(repo[p1].node())
427 432 repo.dirstate.endparentchange()
428 433 newnode = None
429 434 # Update the state
430 435 if newnode is not None:
431 436 self.state[rev] = repo[newnode].rev()
432 437 ui.debug('rebased as %s\n' % short(newnode))
433 438 else:
434 439 if not self.collapsef:
435 440 ui.warn(_('note: rebase of %d:%s created no changes '
436 441 'to commit\n') % (rev, ctx))
437 442 self.skipped.add(rev)
438 443 self.state[rev] = p1
439 444 ui.debug('next revision set to %s\n' % p1)
440 445 elif self.state[rev] == nullmerge:
441 446 ui.debug('ignoring null merge rebase of %s\n' % rev)
442 447 elif self.state[rev] == revignored:
443 448 ui.status(_('not rebasing ignored %s\n') % desc)
444 449 elif self.state[rev] == revprecursor:
445 450 destctx = repo[self.obsoletenotrebased[rev]]
446 451 descdest = '%d:%s "%s"' % (destctx.rev(), destctx,
447 452 destctx.description().split('\n', 1)[0])
448 453 msg = _('note: not rebasing %s, already in destination as %s\n')
449 454 ui.status(msg % (desc, descdest))
450 455 elif self.state[rev] == revpruned:
451 456 msg = _('note: not rebasing %s, it has no successor\n')
452 457 ui.status(msg % desc)
453 458 else:
454 459 ui.status(_('already rebased %s as %s\n') %
455 460 (desc, repo[self.state[rev]]))
456 461
457 462 ui.progress(_('rebasing'), None)
458 463 ui.note(_('rebase merging completed\n'))
459 464
460 465 def _finishrebase(self):
461 466 repo, ui, opts = self.repo, self.ui, self.opts
462 467 if self.collapsef and not self.keepopen:
463 468 p1, p2, _base = defineparents(repo, min(self.state),
464 469 self.dest, self.state,
465 470 self.destancestors,
466 471 self.obsoletenotrebased)
467 472 editopt = opts.get('edit')
468 473 editform = 'rebase.collapse'
469 474 if self.collapsemsg:
470 475 commitmsg = self.collapsemsg
471 476 else:
472 477 commitmsg = 'Collapsed revision'
473 478 for rebased in self.state:
474 479 if rebased not in self.skipped and\
475 480 self.state[rebased] > nullmerge:
476 481 commitmsg += '\n* %s' % repo[rebased].description()
477 482 editopt = True
478 483 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
479 484 revtoreuse = max(self.state)
480 485 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
481 486 try:
482 487 newnode = concludenode(repo, revtoreuse, p1, self.external,
483 488 commitmsg=commitmsg,
484 489 extrafn=_makeextrafn(self.extrafns),
485 490 editor=editor,
486 491 keepbranches=self.keepbranchesf,
487 492 date=self.date)
488 493 dsguard.close()
489 494 release(dsguard)
490 495 except error.InterventionRequired:
491 496 dsguard.close()
492 497 release(dsguard)
493 498 raise
494 499 except Exception:
495 500 release(dsguard)
496 501 raise
497 502
498 503 if newnode is None:
499 504 newrev = self.dest
500 505 else:
501 506 newrev = repo[newnode].rev()
502 507 for oldrev in self.state.iterkeys():
503 508 if self.state[oldrev] > nullmerge:
504 509 self.state[oldrev] = newrev
505 510
506 511 if 'qtip' in repo.tags():
507 512 updatemq(repo, self.state, self.skipped, **opts)
508 513
509 514 if self.currentbookmarks:
510 515 # Nodeids are needed to reset bookmarks
511 516 nstate = {}
512 517 for k, v in self.state.iteritems():
513 518 if v > nullmerge and v != k:
514 519 nstate[repo[k].node()] = repo[v].node()
515 520 elif v == revprecursor:
516 521 succ = self.obsoletenotrebased[k]
517 522 nstate[repo[k].node()] = repo[succ].node()
518 523 # XXX this is the same as dest.node() for the non-continue path --
519 524 # this should probably be cleaned up
520 525 destnode = repo[self.dest].node()
521 526
522 527 # restore original working directory
523 528 # (we do this before stripping)
524 529 newwd = self.state.get(self.originalwd, self.originalwd)
525 530 if newwd == revprecursor:
526 531 newwd = self.obsoletenotrebased[self.originalwd]
527 532 elif newwd < 0:
528 533 # original directory is a parent of rebase set root or ignored
529 534 newwd = self.originalwd
530 535 if newwd not in [c.rev() for c in repo[None].parents()]:
531 536 ui.note(_("update back to initial working directory parent\n"))
532 537 hg.updaterepo(repo, newwd, False)
533 538
534 539 if self.currentbookmarks:
535 540 with repo.transaction('bookmark') as tr:
536 541 updatebookmarks(repo, destnode, nstate,
537 542 self.currentbookmarks, tr)
538 543 if self.activebookmark not in repo._bookmarks:
539 544 # active bookmark was divergent one and has been deleted
540 545 self.activebookmark = None
541 546
542 547 if not self.keepf:
543 548 collapsedas = None
544 549 if self.collapsef:
545 550 collapsedas = newnode
546 551 clearrebased(ui, repo, self.state, self.skipped, collapsedas)
547 552
548 553 clearstatus(repo)
549 554 clearcollapsemsg(repo)
550 555
551 556 ui.note(_("rebase completed\n"))
552 557 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
553 558 if self.skipped:
554 559 skippedlen = len(self.skipped)
555 560 ui.note(_("%d revisions have been skipped\n") % skippedlen)
556 561
557 562 if (self.activebookmark and
558 563 repo['.'].node() == repo._bookmarks[self.activebookmark]):
559 564 bookmarks.activate(repo, self.activebookmark)
560 565
561 566 @command('rebase',
562 567 [('s', 'source', '',
563 568 _('rebase the specified changeset and descendants'), _('REV')),
564 569 ('b', 'base', '',
565 570 _('rebase everything from branching point of specified changeset'),
566 571 _('REV')),
567 572 ('r', 'rev', [],
568 573 _('rebase these revisions'),
569 574 _('REV')),
570 575 ('d', 'dest', '',
571 576 _('rebase onto the specified changeset'), _('REV')),
572 577 ('', 'collapse', False, _('collapse the rebased changesets')),
573 578 ('m', 'message', '',
574 579 _('use text as collapse commit message'), _('TEXT')),
575 580 ('e', 'edit', False, _('invoke editor on commit messages')),
576 581 ('l', 'logfile', '',
577 582 _('read collapse commit message from file'), _('FILE')),
578 583 ('k', 'keep', False, _('keep original changesets')),
579 584 ('', 'keepbranches', False, _('keep original branch names')),
580 585 ('D', 'detach', False, _('(DEPRECATED)')),
581 586 ('i', 'interactive', False, _('(DEPRECATED)')),
582 587 ('t', 'tool', '', _('specify merge tool')),
583 588 ('c', 'continue', False, _('continue an interrupted rebase')),
584 589 ('a', 'abort', False, _('abort an interrupted rebase'))] +
585 590 templateopts,
586 591 _('[-s REV | -b REV] [-d REV] [OPTION]'))
587 592 def rebase(ui, repo, **opts):
588 593 """move changeset (and descendants) to a different branch
589 594
590 595 Rebase uses repeated merging to graft changesets from one part of
591 596 history (the source) onto another (the destination). This can be
592 597 useful for linearizing *local* changes relative to a master
593 598 development tree.
594 599
595 600 Published commits cannot be rebased (see :hg:`help phases`).
596 601 To copy commits, see :hg:`help graft`.
597 602
598 603 If you don't specify a destination changeset (``-d/--dest``), rebase
599 604 will use the same logic as :hg:`merge` to pick a destination. if
600 605 the current branch contains exactly one other head, the other head
601 606 is merged with by default. Otherwise, an explicit revision with
602 607 which to merge with must be provided. (destination changeset is not
603 608 modified by rebasing, but new changesets are added as its
604 609 descendants.)
605 610
606 611 Here are the ways to select changesets:
607 612
608 613 1. Explicitly select them using ``--rev``.
609 614
610 615 2. Use ``--source`` to select a root changeset and include all of its
611 616 descendants.
612 617
613 618 3. Use ``--base`` to select a changeset; rebase will find ancestors
614 619 and their descendants which are not also ancestors of the destination.
615 620
616 621 4. If you do not specify any of ``--rev``, ``source``, or ``--base``,
617 622 rebase will use ``--base .`` as above.
618 623
619 624 Rebase will destroy original changesets unless you use ``--keep``.
620 625 It will also move your bookmarks (even if you do).
621 626
622 627 Some changesets may be dropped if they do not contribute changes
623 628 (e.g. merges from the destination branch).
624 629
625 630 Unlike ``merge``, rebase will do nothing if you are at the branch tip of
626 631 a named branch with two heads. You will need to explicitly specify source
627 632 and/or destination.
628 633
629 634 If you need to use a tool to automate merge/conflict decisions, you
630 635 can specify one with ``--tool``, see :hg:`help merge-tools`.
631 636 As a caveat: the tool will not be used to mediate when a file was
632 637 deleted, there is no hook presently available for this.
633 638
634 639 If a rebase is interrupted to manually resolve a conflict, it can be
635 640 continued with --continue/-c or aborted with --abort/-a.
636 641
637 642 .. container:: verbose
638 643
639 644 Examples:
640 645
641 646 - move "local changes" (current commit back to branching point)
642 647 to the current branch tip after a pull::
643 648
644 649 hg rebase
645 650
646 651 - move a single changeset to the stable branch::
647 652
648 653 hg rebase -r 5f493448 -d stable
649 654
650 655 - splice a commit and all its descendants onto another part of history::
651 656
652 657 hg rebase --source c0c3 --dest 4cf9
653 658
654 659 - rebase everything on a branch marked by a bookmark onto the
655 660 default branch::
656 661
657 662 hg rebase --base myfeature --dest default
658 663
659 664 - collapse a sequence of changes into a single commit::
660 665
661 666 hg rebase --collapse -r 1520:1525 -d .
662 667
663 668 - move a named branch while preserving its name::
664 669
665 670 hg rebase -r "branch(featureX)" -d 1.3 --keepbranches
666 671
667 672 Configuration Options:
668 673
669 674 You can make rebase require a destination if you set the following config
670 675 option::
671 676
672 677 [commands]
673 678 rebase.requiredest = True
674 679
675 680 Return Values:
676 681
677 682 Returns 0 on success, 1 if nothing to rebase or there are
678 683 unresolved conflicts.
679 684
680 685 """
681 686 rbsrt = rebaseruntime(repo, ui, opts)
682 687
683 688 lock = wlock = None
684 689 try:
685 690 wlock = repo.wlock()
686 691 lock = repo.lock()
687 692
688 693 # Validate input and define rebasing points
689 694 destf = opts.get('dest', None)
690 695 srcf = opts.get('source', None)
691 696 basef = opts.get('base', None)
692 697 revf = opts.get('rev', [])
693 698 # search default destination in this space
694 699 # used in the 'hg pull --rebase' case, see issue 5214.
695 700 destspace = opts.get('_destspace')
696 701 contf = opts.get('continue')
697 702 abortf = opts.get('abort')
698 703 if opts.get('interactive'):
699 704 try:
700 705 if extensions.find('histedit'):
701 706 enablehistedit = ''
702 707 except KeyError:
703 708 enablehistedit = " --config extensions.histedit="
704 709 help = "hg%s help -e histedit" % enablehistedit
705 710 msg = _("interactive history editing is supported by the "
706 711 "'histedit' extension (see \"%s\")") % help
707 712 raise error.Abort(msg)
708 713
709 714 if rbsrt.collapsemsg and not rbsrt.collapsef:
710 715 raise error.Abort(
711 716 _('message can only be specified with collapse'))
712 717
713 718 if contf or abortf:
714 719 if contf and abortf:
715 720 raise error.Abort(_('cannot use both abort and continue'))
716 721 if rbsrt.collapsef:
717 722 raise error.Abort(
718 723 _('cannot use collapse with continue or abort'))
719 724 if srcf or basef or destf:
720 725 raise error.Abort(
721 726 _('abort and continue do not allow specifying revisions'))
722 727 if abortf and opts.get('tool', False):
723 728 ui.warn(_('tool option will be ignored\n'))
724 729 if contf:
725 730 ms = mergemod.mergestate.read(repo)
726 731 mergeutil.checkunresolved(ms)
727 732
728 733 retcode = rbsrt._prepareabortorcontinue(abortf)
729 734 if retcode is not None:
730 735 return retcode
731 736 else:
732 737 dest, rebaseset = _definesets(ui, repo, destf, srcf, basef, revf,
733 738 destspace=destspace)
734 739 retcode = rbsrt._preparenewrebase(dest, rebaseset)
735 740 if retcode is not None:
736 741 return retcode
737 742
738 743 with repo.transaction('rebase') as tr:
739 744 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
740 745 try:
741 746 rbsrt._performrebase(tr)
742 747 dsguard.close()
743 748 release(dsguard)
744 749 except error.InterventionRequired:
745 750 dsguard.close()
746 751 release(dsguard)
747 752 tr.close()
748 753 raise
749 754 except Exception:
750 755 release(dsguard)
751 756 raise
752 757 rbsrt._finishrebase()
753 758 finally:
754 759 release(lock, wlock)
755 760
756 761 def _definesets(ui, repo, destf=None, srcf=None, basef=None, revf=None,
757 762 destspace=None):
758 763 """use revisions argument to define destination and rebase set
759 764 """
760 765 if revf is None:
761 766 revf = []
762 767
763 768 # destspace is here to work around issues with `hg pull --rebase` see
764 769 # issue5214 for details
765 770 if srcf and basef:
766 771 raise error.Abort(_('cannot specify both a source and a base'))
767 772 if revf and basef:
768 773 raise error.Abort(_('cannot specify both a revision and a base'))
769 774 if revf and srcf:
770 775 raise error.Abort(_('cannot specify both a revision and a source'))
771 776
772 777 cmdutil.checkunfinished(repo)
773 778 cmdutil.bailifchanged(repo)
774 779
775 780 if ui.configbool('commands', 'rebase.requiredest') and not destf:
776 781 raise error.Abort(_('you must specify a destination'),
777 782 hint=_('use: hg rebase -d REV'))
778 783
779 784 if destf:
780 785 dest = scmutil.revsingle(repo, destf)
781 786
782 787 if revf:
783 788 rebaseset = scmutil.revrange(repo, revf)
784 789 if not rebaseset:
785 790 ui.status(_('empty "rev" revision set - nothing to rebase\n'))
786 791 return None, None
787 792 elif srcf:
788 793 src = scmutil.revrange(repo, [srcf])
789 794 if not src:
790 795 ui.status(_('empty "source" revision set - nothing to rebase\n'))
791 796 return None, None
792 797 rebaseset = repo.revs('(%ld)::', src)
793 798 assert rebaseset
794 799 else:
795 800 base = scmutil.revrange(repo, [basef or '.'])
796 801 if not base:
797 802 ui.status(_('empty "base" revision set - '
798 803 "can't compute rebase set\n"))
799 804 return None, None
800 805 if not destf:
801 806 dest = repo[_destrebase(repo, base, destspace=destspace)]
802 807 destf = str(dest)
803 808
804 809 roots = [] # selected children of branching points
805 810 bpbase = {} # {branchingpoint: [origbase]}
806 811 for b in base: # group bases by branching points
807 812 bp = repo.revs('ancestor(%d, %d)', b, dest).first()
808 813 bpbase[bp] = bpbase.get(bp, []) + [b]
809 814 if None in bpbase:
810 815 # emulate the old behavior, showing "nothing to rebase" (a better
811 816 # behavior may be abort with "cannot find branching point" error)
812 817 bpbase.clear()
813 818 for bp, bs in bpbase.iteritems(): # calculate roots
814 819 roots += list(repo.revs('children(%d) & ancestors(%ld)', bp, bs))
815 820
816 821 rebaseset = repo.revs('%ld::', roots)
817 822
818 823 if not rebaseset:
819 824 # transform to list because smartsets are not comparable to
820 825 # lists. This should be improved to honor laziness of
821 826 # smartset.
822 827 if list(base) == [dest.rev()]:
823 828 if basef:
824 829 ui.status(_('nothing to rebase - %s is both "base"'
825 830 ' and destination\n') % dest)
826 831 else:
827 832 ui.status(_('nothing to rebase - working directory '
828 833 'parent is also destination\n'))
829 834 elif not repo.revs('%ld - ::%d', base, dest):
830 835 if basef:
831 836 ui.status(_('nothing to rebase - "base" %s is '
832 837 'already an ancestor of destination '
833 838 '%s\n') %
834 839 ('+'.join(str(repo[r]) for r in base),
835 840 dest))
836 841 else:
837 842 ui.status(_('nothing to rebase - working '
838 843 'directory parent is already an '
839 844 'ancestor of destination %s\n') % dest)
840 845 else: # can it happen?
841 846 ui.status(_('nothing to rebase from %s to %s\n') %
842 847 ('+'.join(str(repo[r]) for r in base), dest))
843 848 return None, None
844 849
845 850 if not destf:
846 851 dest = repo[_destrebase(repo, rebaseset, destspace=destspace)]
847 852 destf = str(dest)
848 853
849 854 return dest, rebaseset
850 855
851 856 def externalparent(repo, state, destancestors):
852 857 """Return the revision that should be used as the second parent
853 858 when the revisions in state is collapsed on top of destancestors.
854 859 Abort if there is more than one parent.
855 860 """
856 861 parents = set()
857 862 source = min(state)
858 863 for rev in state:
859 864 if rev == source:
860 865 continue
861 866 for p in repo[rev].parents():
862 867 if (p.rev() not in state
863 868 and p.rev() not in destancestors):
864 869 parents.add(p.rev())
865 870 if not parents:
866 871 return nullrev
867 872 if len(parents) == 1:
868 873 return parents.pop()
869 874 raise error.Abort(_('unable to collapse on top of %s, there is more '
870 875 'than one external parent: %s') %
871 876 (max(destancestors),
872 877 ', '.join(str(p) for p in sorted(parents))))
873 878
874 879 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None,
875 880 keepbranches=False, date=None):
876 881 '''Commit the wd changes with parents p1 and p2. Reuse commit info from rev
877 882 but also store useful information in extra.
878 883 Return node of committed revision.'''
879 884 repo.setparents(repo[p1].node(), repo[p2].node())
880 885 ctx = repo[rev]
881 886 if commitmsg is None:
882 887 commitmsg = ctx.description()
883 888 keepbranch = keepbranches and repo[p1].branch() != ctx.branch()
884 889 extra = {'rebase_source': ctx.hex()}
885 890 if extrafn:
886 891 extrafn(ctx, extra)
887 892
888 893 destphase = max(ctx.phase(), phases.draft)
889 894 overrides = {('phases', 'new-commit'): destphase}
890 895 with repo.ui.configoverride(overrides, 'rebase'):
891 896 if keepbranch:
892 897 repo.ui.setconfig('ui', 'allowemptycommit', True)
893 898 # Commit might fail if unresolved files exist
894 899 if date is None:
895 900 date = ctx.date()
896 901 newnode = repo.commit(text=commitmsg, user=ctx.user(),
897 902 date=date, extra=extra, editor=editor)
898 903
899 904 repo.dirstate.setbranch(repo[newnode].branch())
900 905 return newnode
901 906
902 907 def rebasenode(repo, rev, p1, base, state, collapse, dest):
903 908 'Rebase a single revision rev on top of p1 using base as merge ancestor'
904 909 # Merge phase
905 910 # Update to destination and merge it with local
906 911 if repo['.'].rev() != p1:
907 912 repo.ui.debug(" update to %d:%s\n" % (p1, repo[p1]))
908 913 mergemod.update(repo, p1, False, True)
909 914 else:
910 915 repo.ui.debug(" already in destination\n")
911 916 repo.dirstate.write(repo.currenttransaction())
912 917 repo.ui.debug(" merge against %d:%s\n" % (rev, repo[rev]))
913 918 if base is not None:
914 919 repo.ui.debug(" detach base %d:%s\n" % (base, repo[base]))
915 920 # When collapsing in-place, the parent is the common ancestor, we
916 921 # have to allow merging with it.
917 922 stats = mergemod.update(repo, rev, True, True, base, collapse,
918 923 labels=['dest', 'source'])
919 924 if collapse:
920 925 copies.duplicatecopies(repo, rev, dest)
921 926 else:
922 927 # If we're not using --collapse, we need to
923 928 # duplicate copies between the revision we're
924 929 # rebasing and its first parent, but *not*
925 930 # duplicate any copies that have already been
926 931 # performed in the destination.
927 932 p1rev = repo[rev].p1().rev()
928 933 copies.duplicatecopies(repo, rev, p1rev, skiprev=dest)
929 934 return stats
930 935
931 936 def nearestrebased(repo, rev, state):
932 937 """return the nearest ancestors of rev in the rebase result"""
933 938 rebased = [r for r in state if state[r] > nullmerge]
934 939 candidates = repo.revs('max(%ld and (::%d))', rebased, rev)
935 940 if candidates:
936 941 return state[candidates.first()]
937 942 else:
938 943 return None
939 944
940 945 def _checkobsrebase(repo, ui, rebaseobsrevs, rebasesetrevs, rebaseobsskipped):
941 946 """
942 947 Abort if rebase will create divergence or rebase is noop because of markers
943 948
944 949 `rebaseobsrevs`: set of obsolete revision in source
945 950 `rebasesetrevs`: set of revisions to be rebased from source
946 951 `rebaseobsskipped`: set of revisions from source skipped because they have
947 952 successors in destination
948 953 """
949 954 # Obsolete node with successors not in dest leads to divergence
950 955 divergenceok = ui.configbool('experimental',
951 956 'allowdivergence')
952 957 divergencebasecandidates = rebaseobsrevs - rebaseobsskipped
953 958
954 959 if divergencebasecandidates and not divergenceok:
955 960 divhashes = (str(repo[r])
956 961 for r in divergencebasecandidates)
957 962 msg = _("this rebase will cause "
958 963 "divergences from: %s")
959 964 h = _("to force the rebase please set "
960 965 "experimental.allowdivergence=True")
961 966 raise error.Abort(msg % (",".join(divhashes),), hint=h)
962 967
963 968 def defineparents(repo, rev, dest, state, destancestors,
964 969 obsoletenotrebased):
965 970 'Return the new parent relationship of the revision that will be rebased'
966 971 parents = repo[rev].parents()
967 972 p1 = p2 = nullrev
968 973 rp1 = None
969 974
970 975 p1n = parents[0].rev()
971 976 if p1n in destancestors:
972 977 p1 = dest
973 978 elif p1n in state:
974 979 if state[p1n] == nullmerge:
975 980 p1 = dest
976 981 elif state[p1n] in revskipped:
977 982 p1 = nearestrebased(repo, p1n, state)
978 983 if p1 is None:
979 984 p1 = dest
980 985 else:
981 986 p1 = state[p1n]
982 987 else: # p1n external
983 988 p1 = dest
984 989 p2 = p1n
985 990
986 991 if len(parents) == 2 and parents[1].rev() not in destancestors:
987 992 p2n = parents[1].rev()
988 993 # interesting second parent
989 994 if p2n in state:
990 995 if p1 == dest: # p1n in destancestors or external
991 996 p1 = state[p2n]
992 997 if p1 == revprecursor:
993 998 rp1 = obsoletenotrebased[p2n]
994 999 elif state[p2n] in revskipped:
995 1000 p2 = nearestrebased(repo, p2n, state)
996 1001 if p2 is None:
997 1002 # no ancestors rebased yet, detach
998 1003 p2 = dest
999 1004 else:
1000 1005 p2 = state[p2n]
1001 1006 else: # p2n external
1002 1007 if p2 != nullrev: # p1n external too => rev is a merged revision
1003 1008 raise error.Abort(_('cannot use revision %d as base, result '
1004 1009 'would have 3 parents') % rev)
1005 1010 p2 = p2n
1006 1011 repo.ui.debug(" future parents are %d and %d\n" %
1007 1012 (repo[rp1 or p1].rev(), repo[p2].rev()))
1008 1013
1009 1014 if not any(p.rev() in state for p in parents):
1010 1015 # Case (1) root changeset of a non-detaching rebase set.
1011 1016 # Let the merge mechanism find the base itself.
1012 1017 base = None
1013 1018 elif not repo[rev].p2():
1014 1019 # Case (2) detaching the node with a single parent, use this parent
1015 1020 base = repo[rev].p1().rev()
1016 1021 else:
1017 1022 # Assuming there is a p1, this is the case where there also is a p2.
1018 1023 # We are thus rebasing a merge and need to pick the right merge base.
1019 1024 #
1020 1025 # Imagine we have:
1021 1026 # - M: current rebase revision in this step
1022 1027 # - A: one parent of M
1023 1028 # - B: other parent of M
1024 1029 # - D: destination of this merge step (p1 var)
1025 1030 #
1026 1031 # Consider the case where D is a descendant of A or B and the other is
1027 1032 # 'outside'. In this case, the right merge base is the D ancestor.
1028 1033 #
1029 1034 # An informal proof, assuming A is 'outside' and B is the D ancestor:
1030 1035 #
1031 1036 # If we pick B as the base, the merge involves:
1032 1037 # - changes from B to M (actual changeset payload)
1033 1038 # - changes from B to D (induced by rebase) as D is a rebased
1034 1039 # version of B)
1035 1040 # Which exactly represent the rebase operation.
1036 1041 #
1037 1042 # If we pick A as the base, the merge involves:
1038 1043 # - changes from A to M (actual changeset payload)
1039 1044 # - changes from A to D (with include changes between unrelated A and B
1040 1045 # plus changes induced by rebase)
1041 1046 # Which does not represent anything sensible and creates a lot of
1042 1047 # conflicts. A is thus not the right choice - B is.
1043 1048 #
1044 1049 # Note: The base found in this 'proof' is only correct in the specified
1045 1050 # case. This base does not make sense if is not D a descendant of A or B
1046 1051 # or if the other is not parent 'outside' (especially not if the other
1047 1052 # parent has been rebased). The current implementation does not
1048 1053 # make it feasible to consider different cases separately. In these
1049 1054 # other cases we currently just leave it to the user to correctly
1050 1055 # resolve an impossible merge using a wrong ancestor.
1051 1056 #
1052 1057 # xx, p1 could be -4, and both parents could probably be -4...
1053 1058 for p in repo[rev].parents():
1054 1059 if state.get(p.rev()) == p1:
1055 1060 base = p.rev()
1056 1061 break
1057 1062 else: # fallback when base not found
1058 1063 base = None
1059 1064
1060 1065 # Raise because this function is called wrong (see issue 4106)
1061 1066 raise AssertionError('no base found to rebase on '
1062 1067 '(defineparents called wrong)')
1063 1068 return rp1 or p1, p2, base
1064 1069
1065 1070 def isagitpatch(repo, patchname):
1066 1071 'Return true if the given patch is in git format'
1067 1072 mqpatch = os.path.join(repo.mq.path, patchname)
1068 1073 for line in patch.linereader(file(mqpatch, 'rb')):
1069 1074 if line.startswith('diff --git'):
1070 1075 return True
1071 1076 return False
1072 1077
1073 1078 def updatemq(repo, state, skipped, **opts):
1074 1079 'Update rebased mq patches - finalize and then import them'
1075 1080 mqrebase = {}
1076 1081 mq = repo.mq
1077 1082 original_series = mq.fullseries[:]
1078 1083 skippedpatches = set()
1079 1084
1080 1085 for p in mq.applied:
1081 1086 rev = repo[p.node].rev()
1082 1087 if rev in state:
1083 1088 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
1084 1089 (rev, p.name))
1085 1090 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
1086 1091 else:
1087 1092 # Applied but not rebased, not sure this should happen
1088 1093 skippedpatches.add(p.name)
1089 1094
1090 1095 if mqrebase:
1091 1096 mq.finish(repo, mqrebase.keys())
1092 1097
1093 1098 # We must start import from the newest revision
1094 1099 for rev in sorted(mqrebase, reverse=True):
1095 1100 if rev not in skipped:
1096 1101 name, isgit = mqrebase[rev]
1097 1102 repo.ui.note(_('updating mq patch %s to %s:%s\n') %
1098 1103 (name, state[rev], repo[state[rev]]))
1099 1104 mq.qimport(repo, (), patchname=name, git=isgit,
1100 1105 rev=[str(state[rev])])
1101 1106 else:
1102 1107 # Rebased and skipped
1103 1108 skippedpatches.add(mqrebase[rev][0])
1104 1109
1105 1110 # Patches were either applied and rebased and imported in
1106 1111 # order, applied and removed or unapplied. Discard the removed
1107 1112 # ones while preserving the original series order and guards.
1108 1113 newseries = [s for s in original_series
1109 1114 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
1110 1115 mq.fullseries[:] = newseries
1111 1116 mq.seriesdirty = True
1112 1117 mq.savedirty()
1113 1118
1114 1119 def updatebookmarks(repo, destnode, nstate, originalbookmarks, tr):
1115 1120 'Move bookmarks to their correct changesets, and delete divergent ones'
1116 1121 marks = repo._bookmarks
1117 1122 for k, v in originalbookmarks.iteritems():
1118 1123 if v in nstate:
1119 1124 # update the bookmarks for revs that have moved
1120 1125 marks[k] = nstate[v]
1121 1126 bookmarks.deletedivergent(repo, [destnode], k)
1122 1127 marks.recordchange(tr)
1123 1128
1124 1129 def storecollapsemsg(repo, collapsemsg):
1125 1130 'Store the collapse message to allow recovery'
1126 1131 collapsemsg = collapsemsg or ''
1127 1132 f = repo.vfs("last-message.txt", "w")
1128 1133 f.write("%s\n" % collapsemsg)
1129 1134 f.close()
1130 1135
1131 1136 def clearcollapsemsg(repo):
1132 1137 'Remove collapse message file'
1133 1138 repo.vfs.unlinkpath("last-message.txt", ignoremissing=True)
1134 1139
1135 1140 def restorecollapsemsg(repo, isabort):
1136 1141 'Restore previously stored collapse message'
1137 1142 try:
1138 1143 f = repo.vfs("last-message.txt")
1139 1144 collapsemsg = f.readline().strip()
1140 1145 f.close()
1141 1146 except IOError as err:
1142 1147 if err.errno != errno.ENOENT:
1143 1148 raise
1144 1149 if isabort:
1145 1150 # Oh well, just abort like normal
1146 1151 collapsemsg = ''
1147 1152 else:
1148 1153 raise error.Abort(_('missing .hg/last-message.txt for rebase'))
1149 1154 return collapsemsg
1150 1155
1151 1156 def clearstatus(repo):
1152 1157 'Remove the status files'
1153 1158 _clearrebasesetvisibiliy(repo)
1154 1159 repo.vfs.unlinkpath("rebasestate", ignoremissing=True)
1155 1160
1156 1161 def needupdate(repo, state):
1157 1162 '''check whether we should `update --clean` away from a merge, or if
1158 1163 somehow the working dir got forcibly updated, e.g. by older hg'''
1159 1164 parents = [p.rev() for p in repo[None].parents()]
1160 1165
1161 1166 # Are we in a merge state at all?
1162 1167 if len(parents) < 2:
1163 1168 return False
1164 1169
1165 1170 # We should be standing on the first as-of-yet unrebased commit.
1166 1171 firstunrebased = min([old for old, new in state.iteritems()
1167 1172 if new == nullrev])
1168 1173 if firstunrebased in parents:
1169 1174 return True
1170 1175
1171 1176 return False
1172 1177
1173 1178 def abort(repo, originalwd, dest, state, activebookmark=None):
1174 1179 '''Restore the repository to its original state. Additional args:
1175 1180
1176 1181 activebookmark: the name of the bookmark that should be active after the
1177 1182 restore'''
1178 1183
1179 1184 try:
1180 1185 # If the first commits in the rebased set get skipped during the rebase,
1181 1186 # their values within the state mapping will be the dest rev id. The
1182 1187 # dstates list must must not contain the dest rev (issue4896)
1183 1188 dstates = [s for s in state.values() if s >= 0 and s != dest]
1184 1189 immutable = [d for d in dstates if not repo[d].mutable()]
1185 1190 cleanup = True
1186 1191 if immutable:
1187 1192 repo.ui.warn(_("warning: can't clean up public changesets %s\n")
1188 1193 % ', '.join(str(repo[r]) for r in immutable),
1189 1194 hint=_("see 'hg help phases' for details"))
1190 1195 cleanup = False
1191 1196
1192 1197 descendants = set()
1193 1198 if dstates:
1194 1199 descendants = set(repo.changelog.descendants(dstates))
1195 1200 if descendants - set(dstates):
1196 1201 repo.ui.warn(_("warning: new changesets detected on destination "
1197 1202 "branch, can't strip\n"))
1198 1203 cleanup = False
1199 1204
1200 1205 if cleanup:
1201 1206 shouldupdate = False
1202 1207 rebased = filter(lambda x: x >= 0 and x != dest, state.values())
1203 1208 if rebased:
1204 1209 strippoints = [
1205 1210 c.node() for c in repo.set('roots(%ld)', rebased)]
1206 1211
1207 1212 updateifonnodes = set(rebased)
1208 1213 updateifonnodes.add(dest)
1209 1214 updateifonnodes.add(originalwd)
1210 1215 shouldupdate = repo['.'].rev() in updateifonnodes
1211 1216
1212 1217 # Update away from the rebase if necessary
1213 1218 if shouldupdate or needupdate(repo, state):
1214 1219 mergemod.update(repo, originalwd, False, True)
1215 1220
1216 1221 # Strip from the first rebased revision
1217 1222 if rebased:
1218 1223 # no backup of rebased cset versions needed
1219 1224 repair.strip(repo.ui, repo, strippoints)
1220 1225
1221 1226 if activebookmark and activebookmark in repo._bookmarks:
1222 1227 bookmarks.activate(repo, activebookmark)
1223 1228
1224 1229 finally:
1225 1230 clearstatus(repo)
1226 1231 clearcollapsemsg(repo)
1227 1232 repo.ui.warn(_('rebase aborted\n'))
1228 1233 return 0
1229 1234
1230 1235 def buildstate(repo, dest, rebaseset, collapse, obsoletenotrebased):
1231 1236 '''Define which revisions are going to be rebased and where
1232 1237
1233 1238 repo: repo
1234 1239 dest: context
1235 1240 rebaseset: set of rev
1236 1241 '''
1237 1242 originalwd = repo['.'].rev()
1238 1243 _setrebasesetvisibility(repo, set(rebaseset) | {originalwd})
1239 1244
1240 1245 # This check isn't strictly necessary, since mq detects commits over an
1241 1246 # applied patch. But it prevents messing up the working directory when
1242 1247 # a partially completed rebase is blocked by mq.
1243 1248 if 'qtip' in repo.tags() and (dest.node() in
1244 1249 [s.node for s in repo.mq.applied]):
1245 1250 raise error.Abort(_('cannot rebase onto an applied mq patch'))
1246 1251
1247 1252 roots = list(repo.set('roots(%ld)', rebaseset))
1248 1253 if not roots:
1249 1254 raise error.Abort(_('no matching revisions'))
1250 1255 roots.sort()
1251 1256 state = dict.fromkeys(rebaseset, revtodo)
1252 1257 detachset = set()
1253 1258 emptyrebase = True
1254 1259 for root in roots:
1255 1260 commonbase = root.ancestor(dest)
1256 1261 if commonbase == root:
1257 1262 raise error.Abort(_('source is ancestor of destination'))
1258 1263 if commonbase == dest:
1259 1264 wctx = repo[None]
1260 1265 if dest == wctx.p1():
1261 1266 # when rebasing to '.', it will use the current wd branch name
1262 1267 samebranch = root.branch() == wctx.branch()
1263 1268 else:
1264 1269 samebranch = root.branch() == dest.branch()
1265 1270 if not collapse and samebranch and root in dest.children():
1266 1271 # mark the revision as done by setting its new revision
1267 1272 # equal to its old (current) revisions
1268 1273 state[root.rev()] = root.rev()
1269 1274 repo.ui.debug('source is a child of destination\n')
1270 1275 continue
1271 1276
1272 1277 emptyrebase = False
1273 1278 repo.ui.debug('rebase onto %s starting from %s\n' % (dest, root))
1274 1279 # Rebase tries to turn <dest> into a parent of <root> while
1275 1280 # preserving the number of parents of rebased changesets:
1276 1281 #
1277 1282 # - A changeset with a single parent will always be rebased as a
1278 1283 # changeset with a single parent.
1279 1284 #
1280 1285 # - A merge will be rebased as merge unless its parents are both
1281 1286 # ancestors of <dest> or are themselves in the rebased set and
1282 1287 # pruned while rebased.
1283 1288 #
1284 1289 # If one parent of <root> is an ancestor of <dest>, the rebased
1285 1290 # version of this parent will be <dest>. This is always true with
1286 1291 # --base option.
1287 1292 #
1288 1293 # Otherwise, we need to *replace* the original parents with
1289 1294 # <dest>. This "detaches" the rebased set from its former location
1290 1295 # and rebases it onto <dest>. Changes introduced by ancestors of
1291 1296 # <root> not common with <dest> (the detachset, marked as
1292 1297 # nullmerge) are "removed" from the rebased changesets.
1293 1298 #
1294 1299 # - If <root> has a single parent, set it to <dest>.
1295 1300 #
1296 1301 # - If <root> is a merge, we cannot decide which parent to
1297 1302 # replace, the rebase operation is not clearly defined.
1298 1303 #
1299 1304 # The table below sums up this behavior:
1300 1305 #
1301 1306 # +------------------+----------------------+-------------------------+
1302 1307 # | | one parent | merge |
1303 1308 # +------------------+----------------------+-------------------------+
1304 1309 # | parent in | new parent is <dest> | parents in ::<dest> are |
1305 1310 # | ::<dest> | | remapped to <dest> |
1306 1311 # +------------------+----------------------+-------------------------+
1307 1312 # | unrelated source | new parent is <dest> | ambiguous, abort |
1308 1313 # +------------------+----------------------+-------------------------+
1309 1314 #
1310 1315 # The actual abort is handled by `defineparents`
1311 1316 if len(root.parents()) <= 1:
1312 1317 # ancestors of <root> not ancestors of <dest>
1313 1318 detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
1314 1319 [root.rev()]))
1315 1320 if emptyrebase:
1316 1321 return None
1317 1322 for rev in sorted(state):
1318 1323 parents = [p for p in repo.changelog.parentrevs(rev) if p != nullrev]
1319 1324 # if all parents of this revision are done, then so is this revision
1320 1325 if parents and all((state.get(p) == p for p in parents)):
1321 1326 state[rev] = rev
1322 1327 for r in detachset:
1323 1328 if r not in state:
1324 1329 state[r] = nullmerge
1325 1330 if len(roots) > 1:
1326 1331 # If we have multiple roots, we may have "hole" in the rebase set.
1327 1332 # Rebase roots that descend from those "hole" should not be detached as
1328 1333 # other root are. We use the special `revignored` to inform rebase that
1329 1334 # the revision should be ignored but that `defineparents` should search
1330 1335 # a rebase destination that make sense regarding rebased topology.
1331 1336 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset))
1332 1337 for ignored in set(rebasedomain) - set(rebaseset):
1333 1338 state[ignored] = revignored
1334 1339 for r in obsoletenotrebased:
1335 1340 if obsoletenotrebased[r] is None:
1336 1341 state[r] = revpruned
1337 1342 else:
1338 1343 state[r] = revprecursor
1339 1344 return originalwd, dest.rev(), state
1340 1345
1341 1346 def clearrebased(ui, repo, state, skipped, collapsedas=None):
1342 1347 """dispose of rebased revision at the end of the rebase
1343 1348
1344 1349 If `collapsedas` is not None, the rebase was a collapse whose result if the
1345 1350 `collapsedas` node."""
1346 1351 if obsolete.isenabled(repo, obsolete.createmarkersopt):
1347 1352 markers = []
1348 1353 for rev, newrev in sorted(state.items()):
1349 1354 if newrev >= 0 and newrev != rev:
1350 1355 if rev in skipped:
1351 1356 succs = ()
1352 1357 elif collapsedas is not None:
1353 1358 succs = (repo[collapsedas],)
1354 1359 else:
1355 1360 succs = (repo[newrev],)
1356 1361 markers.append((repo[rev], succs))
1357 1362 if markers:
1358 1363 obsolete.createmarkers(repo, markers)
1359 1364 else:
1360 1365 rebased = [rev for rev in state
1361 1366 if state[rev] > nullmerge and state[rev] != rev]
1362 1367 if rebased:
1363 1368 stripped = []
1364 1369 for root in repo.set('roots(%ld)', rebased):
1365 1370 if set(repo.changelog.descendants([root.rev()])) - set(state):
1366 1371 ui.warn(_("warning: new changesets detected "
1367 1372 "on source branch, not stripping\n"))
1368 1373 else:
1369 1374 stripped.append(root.node())
1370 1375 if stripped:
1371 1376 # backup the old csets by default
1372 1377 repair.strip(ui, repo, stripped, "all")
1373 1378
1374 1379
1375 1380 def pullrebase(orig, ui, repo, *args, **opts):
1376 1381 'Call rebase after pull if the latter has been invoked with --rebase'
1377 1382 ret = None
1378 1383 if opts.get('rebase'):
1379 1384 if ui.configbool('commands', 'rebase.requiredest'):
1380 1385 msg = _('rebase destination required by configuration')
1381 1386 hint = _('use hg pull followed by hg rebase -d DEST')
1382 1387 raise error.Abort(msg, hint=hint)
1383 1388
1384 1389 wlock = lock = None
1385 1390 try:
1386 1391 wlock = repo.wlock()
1387 1392 lock = repo.lock()
1388 1393 if opts.get('update'):
1389 1394 del opts['update']
1390 1395 ui.debug('--update and --rebase are not compatible, ignoring '
1391 1396 'the update flag\n')
1392 1397
1393 1398 cmdutil.checkunfinished(repo)
1394 1399 cmdutil.bailifchanged(repo, hint=_('cannot pull with rebase: '
1395 1400 'please commit or shelve your changes first'))
1396 1401
1397 1402 revsprepull = len(repo)
1398 1403 origpostincoming = commands.postincoming
1399 1404 def _dummy(*args, **kwargs):
1400 1405 pass
1401 1406 commands.postincoming = _dummy
1402 1407 try:
1403 1408 ret = orig(ui, repo, *args, **opts)
1404 1409 finally:
1405 1410 commands.postincoming = origpostincoming
1406 1411 revspostpull = len(repo)
1407 1412 if revspostpull > revsprepull:
1408 1413 # --rev option from pull conflict with rebase own --rev
1409 1414 # dropping it
1410 1415 if 'rev' in opts:
1411 1416 del opts['rev']
1412 1417 # positional argument from pull conflicts with rebase's own
1413 1418 # --source.
1414 1419 if 'source' in opts:
1415 1420 del opts['source']
1416 1421 # revsprepull is the len of the repo, not revnum of tip.
1417 1422 destspace = list(repo.changelog.revs(start=revsprepull))
1418 1423 opts['_destspace'] = destspace
1419 1424 try:
1420 1425 rebase(ui, repo, **opts)
1421 1426 except error.NoMergeDestAbort:
1422 1427 # we can maybe update instead
1423 1428 rev, _a, _b = destutil.destupdate(repo)
1424 1429 if rev == repo['.'].rev():
1425 1430 ui.status(_('nothing to rebase\n'))
1426 1431 else:
1427 1432 ui.status(_('nothing to rebase - updating instead\n'))
1428 1433 # not passing argument to get the bare update behavior
1429 1434 # with warning and trumpets
1430 1435 commands.update(ui, repo)
1431 1436 finally:
1432 1437 release(lock, wlock)
1433 1438 else:
1434 1439 if opts.get('tool'):
1435 1440 raise error.Abort(_('--tool can only be used with --rebase'))
1436 1441 ret = orig(ui, repo, *args, **opts)
1437 1442
1438 1443 return ret
1439 1444
1440 1445 def _setrebasesetvisibility(repo, revs):
1441 1446 """store the currently rebased set on the repo object
1442 1447
1443 1448 This is used by another function to prevent rebased revision to because
1444 1449 hidden (see issue4504)"""
1445 1450 repo = repo.unfiltered()
1446 1451 repo._rebaseset = revs
1447 1452 # invalidate cache if visibility changes
1448 1453 hiddens = repo.filteredrevcache.get('visible', set())
1449 1454 if revs & hiddens:
1450 1455 repo.invalidatevolatilesets()
1451 1456
1452 1457 def _clearrebasesetvisibiliy(repo):
1453 1458 """remove rebaseset data from the repo"""
1454 1459 repo = repo.unfiltered()
1455 1460 if '_rebaseset' in vars(repo):
1456 1461 del repo._rebaseset
1457 1462
1458 1463 def _rebasedvisible(orig, repo):
1459 1464 """ensure rebased revs stay visible (see issue4504)"""
1460 1465 blockers = orig(repo)
1461 1466 blockers.update(getattr(repo, '_rebaseset', ()))
1462 1467 return blockers
1463 1468
1464 1469 def _filterobsoleterevs(repo, revs):
1465 1470 """returns a set of the obsolete revisions in revs"""
1466 1471 return set(r for r in revs if repo[r].obsolete())
1467 1472
1468 1473 def _computeobsoletenotrebased(repo, rebaseobsrevs, dest):
1469 1474 """return a mapping obsolete => successor for all obsolete nodes to be
1470 1475 rebased that have a successors in the destination
1471 1476
1472 1477 obsolete => None entries in the mapping indicate nodes with no successor"""
1473 1478 obsoletenotrebased = {}
1474 1479
1475 1480 # Build a mapping successor => obsolete nodes for the obsolete
1476 1481 # nodes to be rebased
1477 1482 allsuccessors = {}
1478 1483 cl = repo.changelog
1479 1484 for r in rebaseobsrevs:
1480 1485 node = cl.node(r)
1481 1486 for s in obsolete.allsuccessors(repo.obsstore, [node]):
1482 1487 try:
1483 1488 allsuccessors[cl.rev(s)] = cl.rev(node)
1484 1489 except LookupError:
1485 1490 pass
1486 1491
1487 1492 if allsuccessors:
1488 1493 # Look for successors of obsolete nodes to be rebased among
1489 1494 # the ancestors of dest
1490 1495 ancs = cl.ancestors([repo[dest].rev()],
1491 1496 stoprev=min(allsuccessors),
1492 1497 inclusive=True)
1493 1498 for s in allsuccessors:
1494 1499 if s in ancs:
1495 1500 obsoletenotrebased[allsuccessors[s]] = s
1496 1501 elif (s == allsuccessors[s] and
1497 1502 allsuccessors.values().count(s) == 1):
1498 1503 # plain prune
1499 1504 obsoletenotrebased[s] = None
1500 1505
1501 1506 return obsoletenotrebased
1502 1507
1503 1508 def summaryhook(ui, repo):
1504 1509 if not repo.vfs.exists('rebasestate'):
1505 1510 return
1506 1511 try:
1507 1512 rbsrt = rebaseruntime(repo, ui, {})
1508 1513 rbsrt.restorestatus()
1509 1514 state = rbsrt.state
1510 1515 except error.RepoLookupError:
1511 1516 # i18n: column positioning for "hg summary"
1512 1517 msg = _('rebase: (use "hg rebase --abort" to clear broken state)\n')
1513 1518 ui.write(msg)
1514 1519 return
1515 1520 numrebased = len([i for i in state.itervalues() if i >= 0])
1516 1521 # i18n: column positioning for "hg summary"
1517 1522 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
1518 1523 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
1519 1524 ui.label(_('%d remaining'), 'rebase.remaining') %
1520 1525 (len(state) - numrebased)))
1521 1526
1522 1527 def uisetup(ui):
1523 1528 #Replace pull with a decorator to provide --rebase option
1524 1529 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
1525 1530 entry[1].append(('', 'rebase', None,
1526 1531 _("rebase working directory to branch head")))
1527 1532 entry[1].append(('t', 'tool', '',
1528 1533 _("specify merge tool for rebase")))
1529 1534 cmdutil.summaryhooks.add('rebase', summaryhook)
1530 1535 cmdutil.unfinishedstates.append(
1531 1536 ['rebasestate', False, False, _('rebase in progress'),
1532 1537 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
1533 1538 cmdutil.afterresolvedstates.append(
1534 1539 ['rebasestate', _('hg rebase --continue')])
1535 1540 # ensure rebased rev are not hidden
1536 1541 extensions.wrapfunction(repoview, '_getdynamicblockers', _rebasedvisible)
@@ -1,274 +1,305 b''
1 1 $ cat >> $HGRCPATH <<EOF
2 2 > [extensions]
3 3 > rebase=
4 4 >
5 5 > [phases]
6 6 > publish=False
7 7 >
8 8 > [alias]
9 9 > tglog = log -G --template "{rev}: '{desc}' {branches}\n"
10 10 > tglogp = log -G --template "{rev}:{phase} '{desc}' {branches}\n"
11 11 > EOF
12 12
13 13
14 14 $ hg init a
15 15 $ cd a
16 16
17 17 $ echo A > A
18 18 $ hg ci -Am A
19 19 adding A
20 20
21 21 $ echo B > B
22 22 $ hg ci -Am B
23 23 adding B
24 24
25 25 $ echo C >> A
26 26 $ hg ci -m C
27 27
28 28 $ hg up -q -C 0
29 29
30 30 $ echo D >> A
31 31 $ hg ci -m D
32 32 created new head
33 33
34 34 $ echo E > E
35 35 $ hg ci -Am E
36 36 adding E
37 37
38 38 $ cd ..
39 39
40 40
41 41 Changes during an interruption - continue:
42 42
43 43 $ hg clone -q -u . a a1
44 44 $ cd a1
45 45
46 46 $ hg tglog
47 47 @ 4: 'E'
48 48 |
49 49 o 3: 'D'
50 50 |
51 51 | o 2: 'C'
52 52 | |
53 53 | o 1: 'B'
54 54 |/
55 55 o 0: 'A'
56 56
57 57 Rebasing B onto E:
58 58
59 59 $ hg rebase -s 1 -d 4
60 60 rebasing 1:27547f69f254 "B"
61 61 rebasing 2:965c486023db "C"
62 62 merging A
63 63 warning: conflicts while merging A! (edit, then use 'hg resolve --mark')
64 64 unresolved conflicts (see hg resolve, then hg rebase --continue)
65 65 [1]
66 66
67 67 Force a commit on C during the interruption:
68 68
69 69 $ hg up -q -C 2 --config 'extensions.rebase=!'
70 70
71 71 $ echo 'Extra' > Extra
72 72 $ hg add Extra
73 73 $ hg ci -m 'Extra' --config 'extensions.rebase=!'
74 74
75 75 Force this commit onto secret phase
76 76
77 77 $ hg phase --force --secret 6
78 78
79 79 $ hg tglogp
80 80 @ 6:secret 'Extra'
81 81 |
82 82 | o 5:draft 'B'
83 83 | |
84 84 | o 4:draft 'E'
85 85 | |
86 86 | o 3:draft 'D'
87 87 | |
88 88 o | 2:draft 'C'
89 89 | |
90 90 o | 1:draft 'B'
91 91 |/
92 92 o 0:draft 'A'
93 93
94 94 Resume the rebasing:
95 95
96 96 $ hg rebase --continue
97 97 already rebased 1:27547f69f254 "B" as 45396c49d53b
98 98 rebasing 2:965c486023db "C"
99 99 merging A
100 100 warning: conflicts while merging A! (edit, then use 'hg resolve --mark')
101 101 unresolved conflicts (see hg resolve, then hg rebase --continue)
102 102 [1]
103 103
104 104 Solve the conflict and go on:
105 105
106 106 $ echo 'conflict solved' > A
107 107 $ rm A.orig
108 108 $ hg resolve -m A
109 109 (no more unresolved files)
110 110 continue: hg rebase --continue
111 111
112 112 $ hg rebase --continue
113 113 already rebased 1:27547f69f254 "B" as 45396c49d53b
114 114 rebasing 2:965c486023db "C"
115 115 warning: new changesets detected on source branch, not stripping
116 116
117 117 $ hg tglogp
118 118 o 7:draft 'C'
119 119 |
120 120 | o 6:secret 'Extra'
121 121 | |
122 122 o | 5:draft 'B'
123 123 | |
124 124 @ | 4:draft 'E'
125 125 | |
126 126 o | 3:draft 'D'
127 127 | |
128 128 | o 2:draft 'C'
129 129 | |
130 130 | o 1:draft 'B'
131 131 |/
132 132 o 0:draft 'A'
133 133
134 134 $ cd ..
135 135
136 136
137 137 Changes during an interruption - abort:
138 138
139 139 $ hg clone -q -u . a a2
140 140 $ cd a2
141 141
142 142 $ hg tglog
143 143 @ 4: 'E'
144 144 |
145 145 o 3: 'D'
146 146 |
147 147 | o 2: 'C'
148 148 | |
149 149 | o 1: 'B'
150 150 |/
151 151 o 0: 'A'
152 152
153 153 Rebasing B onto E:
154 154
155 155 $ hg rebase -s 1 -d 4
156 156 rebasing 1:27547f69f254 "B"
157 157 rebasing 2:965c486023db "C"
158 158 merging A
159 159 warning: conflicts while merging A! (edit, then use 'hg resolve --mark')
160 160 unresolved conflicts (see hg resolve, then hg rebase --continue)
161 161 [1]
162 162
163 163 Force a commit on B' during the interruption:
164 164
165 165 $ hg up -q -C 5 --config 'extensions.rebase=!'
166 166
167 167 $ echo 'Extra' > Extra
168 168 $ hg add Extra
169 169 $ hg ci -m 'Extra' --config 'extensions.rebase=!'
170 170
171 171 $ hg tglog
172 172 @ 6: 'Extra'
173 173 |
174 174 o 5: 'B'
175 175 |
176 176 o 4: 'E'
177 177 |
178 178 o 3: 'D'
179 179 |
180 180 | o 2: 'C'
181 181 | |
182 182 | o 1: 'B'
183 183 |/
184 184 o 0: 'A'
185 185
186 186 Abort the rebasing:
187 187
188 188 $ hg rebase --abort
189 189 warning: new changesets detected on destination branch, can't strip
190 190 rebase aborted
191 191
192 192 $ hg tglog
193 193 @ 6: 'Extra'
194 194 |
195 195 o 5: 'B'
196 196 |
197 197 o 4: 'E'
198 198 |
199 199 o 3: 'D'
200 200 |
201 201 | o 2: 'C'
202 202 | |
203 203 | o 1: 'B'
204 204 |/
205 205 o 0: 'A'
206 206
207 207 $ cd ..
208 208
209 209 Changes during an interruption - abort (again):
210 210
211 211 $ hg clone -q -u . a a3
212 212 $ cd a3
213 213
214 214 $ hg tglogp
215 215 @ 4:draft 'E'
216 216 |
217 217 o 3:draft 'D'
218 218 |
219 219 | o 2:draft 'C'
220 220 | |
221 221 | o 1:draft 'B'
222 222 |/
223 223 o 0:draft 'A'
224 224
225 225 Rebasing B onto E:
226 226
227 227 $ hg rebase -s 1 -d 4
228 228 rebasing 1:27547f69f254 "B"
229 229 rebasing 2:965c486023db "C"
230 230 merging A
231 231 warning: conflicts while merging A! (edit, then use 'hg resolve --mark')
232 232 unresolved conflicts (see hg resolve, then hg rebase --continue)
233 233 [1]
234 234
235 235 Change phase on B and B'
236 236
237 237 $ hg up -q -C 5 --config 'extensions.rebase=!'
238 238 $ hg phase --public 1
239 239 $ hg phase --public 5
240 240 $ hg phase --secret -f 2
241 241
242 242 $ hg tglogp
243 243 @ 5:public 'B'
244 244 |
245 245 o 4:public 'E'
246 246 |
247 247 o 3:public 'D'
248 248 |
249 249 | o 2:secret 'C'
250 250 | |
251 251 | o 1:public 'B'
252 252 |/
253 253 o 0:public 'A'
254 254
255 255 Abort the rebasing:
256 256
257 257 $ hg rebase --abort
258 258 warning: can't clean up public changesets 45396c49d53b
259 259 rebase aborted
260 260
261 261 $ hg tglogp
262 262 @ 5:public 'B'
263 263 |
264 264 o 4:public 'E'
265 265 |
266 266 o 3:public 'D'
267 267 |
268 268 | o 2:secret 'C'
269 269 | |
270 270 | o 1:public 'B'
271 271 |/
272 272 o 0:public 'A'
273 273
274 274 $ cd ..
275
276 Make sure merge state is cleaned up after a no-op rebase merge (issue5494)
277 $ hg init repo
278 $ cd repo
279 $ echo a > a
280 $ hg commit -qAm base
281 $ echo b >> a
282 $ hg commit -qm b
283 $ hg up '.^'
284 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
285 $ echo c >> a
286 $ hg commit -qm c
287 $ hg rebase -s 1 -d 2 --noninteractive
288 rebasing 1:fdaca8533b86 "b"
289 merging a
290 warning: conflicts while merging a! (edit, then use 'hg resolve --mark')
291 unresolved conflicts (see hg resolve, then hg rebase --continue)
292 [1]
293 $ echo a > a
294 $ echo c >> a
295 $ hg resolve --mark a
296 (no more unresolved files)
297 continue: hg rebase --continue
298 $ hg rebase --continue
299 rebasing 1:fdaca8533b86 "b"
300 note: rebase of 1:fdaca8533b86 created no changes to commit
301 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/fdaca8533b86-7fd70513-backup.hg (glob)
302 $ hg resolve --list
303 $ test -f .hg/merge
304 [1]
305
General Comments 0
You need to be logged in to leave comments. Login now