comparison hgext/rebase.py @ 26349:92409f8dff5d

rebase: don't rebase obsolete commit whose successor is already rebased This patch avoids unnecessary conflicts to resolve during rebase for the users of changeset evolution. This patch modifies rebase to skip obsolete commits if they are being rebased on their successors. It introduces a new rebase state 'revprecursor' for these revisions that are being skipped and a new message to inform the user of what is happening. This feature is gated behind the config flag experimental.rebaseskipobsolete When an obsolete commit is skipped, the output is: not rebasing 14:9ad579b4a5de "I", already in destination as 17:fc37a630c901 "K"
author Laurent Charignon <lcharignon@fb.com>
date Mon, 14 Sep 2015 17:31:48 -0700
parents 3f8c5c284c86
children b2415e94b2f5
comparison
equal deleted inserted replaced
26348:b80b2ee71a08 26349:92409f8dff5d
24 import os, errno 24 import os, errno
25 25
26 revtodo = -1 26 revtodo = -1
27 nullmerge = -2 27 nullmerge = -2
28 revignored = -3 28 revignored = -3
29 revprecursor = -4
29 30
30 cmdtable = {} 31 cmdtable = {}
31 command = cmdutil.command(cmdtable) 32 command = cmdutil.command(cmdtable)
32 # Note for extension authors: ONLY specify testedwith = 'internal' for 33 # Note for extension authors: ONLY specify testedwith = 'internal' for
33 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should 34 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
331 raise util.Abort( 332 raise util.Abort(
332 _("can't remove original changesets with" 333 _("can't remove original changesets with"
333 " unrebased descendants"), 334 " unrebased descendants"),
334 hint=_('use --keep to keep original changesets')) 335 hint=_('use --keep to keep original changesets'))
335 336
336 result = buildstate(repo, dest, rebaseset, collapsef) 337 obsoletenotrebased = {}
338 if ui.configbool('experimental', 'rebaseskipobsolete'):
339 rebasesetrevs = set(rebaseset)
340 obsoletenotrebased = _computeobsoletenotrebased(repo,
341 rebasesetrevs,
342 dest)
343
344 # - plain prune (no successor) changesets are rebased
345 # - split changesets are not rebased if at least one of the
346 # changeset resulting from the split is an ancestor of dest
347 rebaseset = rebasesetrevs - set(obsoletenotrebased)
348 result = buildstate(repo, dest, rebaseset, collapsef,
349 obsoletenotrebased)
350
337 if not result: 351 if not result:
338 # Empty state built, nothing to rebase 352 # Empty state built, nothing to rebase
339 ui.status(_('nothing to rebase\n')) 353 ui.status(_('nothing to rebase\n'))
340 return 1 354 return 1
341 355
437 ui.debug('next revision set to %s\n' % p1) 451 ui.debug('next revision set to %s\n' % p1)
438 elif state[rev] == nullmerge: 452 elif state[rev] == nullmerge:
439 ui.debug('ignoring null merge rebase of %s\n' % rev) 453 ui.debug('ignoring null merge rebase of %s\n' % rev)
440 elif state[rev] == revignored: 454 elif state[rev] == revignored:
441 ui.status(_('not rebasing ignored %s\n') % desc) 455 ui.status(_('not rebasing ignored %s\n') % desc)
456 elif state[rev] == revprecursor:
457 targetctx = repo[obsoletenotrebased[rev]]
458 desctarget = '%d:%s "%s"' % (targetctx.rev(), targetctx,
459 targetctx.description().split('\n', 1)[0])
460 msg = _('note: not rebasing %s, already in destination as %s\n')
461 ui.status(msg % (desc, desctarget))
442 else: 462 else:
443 ui.status(_('already rebased %s as %s\n') % 463 ui.status(_('already rebased %s as %s\n') %
444 (desc, repo[state[rev]])) 464 (desc, repo[state[rev]]))
445 465
446 ui.progress(_('rebasing'), None) 466 ui.progress(_('rebasing'), None)
618 if p1n in targetancestors: 638 if p1n in targetancestors:
619 p1 = target 639 p1 = target
620 elif p1n in state: 640 elif p1n in state:
621 if state[p1n] == nullmerge: 641 if state[p1n] == nullmerge:
622 p1 = target 642 p1 = target
623 elif state[p1n] == revignored: 643 elif state[p1n] in (revignored, revprecursor):
624 p1 = nearestrebased(repo, p1n, state) 644 p1 = nearestrebased(repo, p1n, state)
625 if p1 is None: 645 if p1 is None:
626 p1 = target 646 p1 = target
627 else: 647 else:
628 p1 = state[p1n] 648 p1 = state[p1n]
634 p2n = parents[1].rev() 654 p2n = parents[1].rev()
635 # interesting second parent 655 # interesting second parent
636 if p2n in state: 656 if p2n in state:
637 if p1 == target: # p1n in targetancestors or external 657 if p1 == target: # p1n in targetancestors or external
638 p1 = state[p2n] 658 p1 = state[p2n]
639 elif state[p2n] == revignored: 659 elif state[p2n] in (revignored, revprecursor):
640 p2 = nearestrebased(repo, p2n, state) 660 p2 = nearestrebased(repo, p2n, state)
641 if p2 is None: 661 if p2 is None:
642 # no ancestors rebased yet, detach 662 # no ancestors rebased yet, detach
643 p2 = target 663 p2 = target
644 else: 664 else:
822 # line 6 is a recent addition, so for backwards compatibility 842 # line 6 is a recent addition, so for backwards compatibility
823 # check that the line doesn't look like the oldrev:newrev lines 843 # check that the line doesn't look like the oldrev:newrev lines
824 activebookmark = l 844 activebookmark = l
825 else: 845 else:
826 oldrev, newrev = l.split(':') 846 oldrev, newrev = l.split(':')
827 if newrev in (str(nullmerge), str(revignored)): 847 if newrev in (str(nullmerge), str(revignored),
848 str(revprecursor)):
828 state[repo[oldrev].rev()] = int(newrev) 849 state[repo[oldrev].rev()] = int(newrev)
829 elif newrev == nullid: 850 elif newrev == nullid:
830 state[repo[oldrev].rev()] = revtodo 851 state[repo[oldrev].rev()] = revtodo
831 # Legacy compat special case 852 # Legacy compat special case
832 else: 853 else:
910 931
911 clearstatus(repo) 932 clearstatus(repo)
912 repo.ui.warn(_('rebase aborted\n')) 933 repo.ui.warn(_('rebase aborted\n'))
913 return 0 934 return 0
914 935
915 def buildstate(repo, dest, rebaseset, collapse): 936 def buildstate(repo, dest, rebaseset, collapse, obsoletenotrebased):
916 '''Define which revisions are going to be rebased and where 937 '''Define which revisions are going to be rebased and where
917 938
918 repo: repo 939 repo: repo
919 dest: context 940 dest: context
920 rebaseset: set of rev 941 rebaseset: set of rev
997 # the revision should be ignored but that `defineparents` should search 1018 # the revision should be ignored but that `defineparents` should search
998 # a rebase destination that make sense regarding rebased topology. 1019 # a rebase destination that make sense regarding rebased topology.
999 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset)) 1020 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset))
1000 for ignored in set(rebasedomain) - set(rebaseset): 1021 for ignored in set(rebasedomain) - set(rebaseset):
1001 state[ignored] = revignored 1022 state[ignored] = revignored
1023 for r in obsoletenotrebased:
1024 state[r] = revprecursor
1002 return repo['.'].rev(), dest.rev(), state 1025 return repo['.'].rev(), dest.rev(), state
1003 1026
1004 def clearrebased(ui, repo, state, skipped, collapsedas=None): 1027 def clearrebased(ui, repo, state, skipped, collapsedas=None):
1005 """dispose of rebased revision at the end of the rebase 1028 """dispose of rebased revision at the end of the rebase
1006 1029
1105 """ensure rebased revs stay visible (see issue4505)""" 1128 """ensure rebased revs stay visible (see issue4505)"""
1106 blockers = orig(repo) 1129 blockers = orig(repo)
1107 blockers.update(getattr(repo, '_rebaseset', ())) 1130 blockers.update(getattr(repo, '_rebaseset', ()))
1108 return blockers 1131 return blockers
1109 1132
1133 def _computeobsoletenotrebased(repo, rebasesetrevs, dest):
1134 """return a mapping obsolete => successor for all obsolete nodes to be
1135 rebased that have a successors in the destination"""
1136 obsoletenotrebased = {}
1137
1138 # Build a mapping succesor => obsolete nodes for the obsolete
1139 # nodes to be rebased
1140 allsuccessors = {}
1141 for r in rebasesetrevs:
1142 n = repo[r]
1143 if n.obsolete():
1144 node = repo.changelog.node(r)
1145 for s in obsolete.allsuccessors(repo.obsstore, [node]):
1146 allsuccessors[repo.changelog.rev(s)] = repo.changelog.rev(node)
1147
1148 if allsuccessors:
1149 # Look for successors of obsolete nodes to be rebased among
1150 # the ancestors of dest
1151 ancs = repo.changelog.ancestors([repo[dest].rev()],
1152 stoprev=min(allsuccessors),
1153 inclusive=True)
1154 for s in allsuccessors:
1155 if s in ancs:
1156 obsoletenotrebased[allsuccessors[s]] = s
1157 return obsoletenotrebased
1158
1110 def summaryhook(ui, repo): 1159 def summaryhook(ui, repo):
1111 if not os.path.exists(repo.join('rebasestate')): 1160 if not os.path.exists(repo.join('rebasestate')):
1112 return 1161 return
1113 try: 1162 try:
1114 state = restorestatus(repo)[2] 1163 state = restorestatus(repo)[2]