Mercurial > hg
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] |