Make hg rebase --restack smarter

Differential Revision: https://phabricator.intern.facebook.com/D4000348
This commit is contained in:
Arun Kulshreshtha 2016-10-14 11:05:53 -07:00
parent 7f1c4eb449
commit 37cf7bd354
2 changed files with 684 additions and 35 deletions

View File

@ -29,10 +29,11 @@ from mercurial import (
phases,
repair,
)
from mercurial.node import hex
from mercurial.node import hex, nullrev
from mercurial import lock as lockmod
from mercurial.i18n import _
from itertools import chain
from collections import defaultdict, deque
from contextlib import nested
cmdtable = {}
@ -485,15 +486,14 @@ def _nextrebase(orig, ui, repo, **opts):
# from rebasemod.rebase(), so just assume that it's the current
# changeset's only child. (This should always be the case.)
rebasedchild = current.children()[0]
ancestors = [r.node() for r in repo.set('%d %% .', child.rev())]
ancestors = repo.set('%d %% .', child.rev())
# Mark the old child changeset as obsolete, and remove the
# the inhibition markers from it and its ancestors. This
# effectively "strips" all of the obsoleted changesets in the
# stack below the child.
_deinhibit(repo, ancestors)
obsolete.createmarkers(repo, [(child, [rebasedchild])])
if inhibitmod:
inhibitmod._deinhibitmarkers(repo, ancestors)
# Remove any preamend bookmarks on precursors, as these would
# create unnecessary inhibition markers.
@ -533,28 +533,198 @@ def restack(orig, ui, repo, **opts):
cmdutil.checkunfinished(repo)
cmdutil.bailifchanged(repo)
base = repo['.']
precursors = list(repo.set('allprecursors(%d)', base.rev()))
descendants = chain.from_iterable(p.descendants() for p in precursors)
# Identify a base changeset from which to begin stabilizing.
base = _findrestackbase(repo)
targets = _findrestacktargets(repo, base)
# Overwrite source and destination, leave all other options.
opts['rev'] = [d.rev() for d in descendants]
opts['dest'] = base.rev()
ret = None
with repo.transaction('restack') as tr:
# Attempt to stabilize all changesets that are or will be (after
# rebasing) descendants of base.
for rev in targets:
try:
ret = orig(ui, repo, **opts)
_restackonce(ui, repo, rev, opts)
except error.InterventionRequired:
tr.close()
raise
_clearpreamend(repo, precursors)
# If we're currently on one of the precursors of the base, update
# to the latest successor since the old changeset is no longer
# needed. Note that if we're on a descendant of the base or its
# precurosrs, the rebase command will ensure that we end up on a
# non-obsolete changeset, so it is only necessary to explicitly
# update if we're on a precursor of the base.
if not repo.revs('. - allprecursors(%d)', base):
commands.update(ui, repo, rev=base)
def _restackonce(ui, repo, rev, rebaseopts=None):
"""Rebase all descendants of precursors of rev onto rev, thereby
stabilzing any non-obsolete descendants of those precursors.
"""
# Get visible descendants of precusors of rev.
allprecursors = repo.revs('allprecursors(%d)', rev)
descendants = repo.revs('descendants(%ld) - %ld', allprecursors,
allprecursors)
# Nothing to do if there are no descendants.
if not descendants:
return
# Overwrite source and destination, leave all other options.
if rebaseopts is None:
rebaseopts = {}
rebaseopts['rev'] = descendants
rebaseopts['dest'] = rev
rebasemod.rebase(ui, repo, **rebaseopts)
# Remove any preamend bookmarks and any inhibition markers
# on precursors so that they will be correctly labelled as
# obsolete. The rebase command will obsolete the descendants,
# so we only need to do this for the precursors.
contexts = [repo[r] for r in allprecursors]
_clearpreamend(repo, contexts)
_deinhibit(repo, contexts)
def _findrestackbase(repo):
"""Search backwards through history to find a changeset in the current
stack that may have unstable descendants on its precursors, or
may itself need to be stabilized.
"""
# Move down current stack until we find a changeset with visible
# precursors or successors, indicating that we may need to stabilize
# some descendants of this changeset or its precursors.
stack = repo.revs('. %% public()')
stack.reverse()
for rev in stack:
# Is this the latest version of this changeset? If not, we need
# to rebase any unstable descendants onto the latest version.
latest = _latest(repo, rev)
if rev != latest:
return latest
# If we're already on the latest version, check if there are any
# visible precusors. If so, we need to rebase their descendants.
if repo.revs('allprecursors(%d)', rev):
return rev
# If we don't encounter any changesets with precursors or successors
# on the way down, assume the user just wants to recusively fix
# the stack upwards from the current changeset.
return repo['.'].rev()
def _findrestacktargets(repo, base):
"""Starting from the given base revision, do a BFS forwards through
history, looking for changesets with unstable descendants on their
precursors. Returns a list of any such changesets, in a top-down
ordering that will allow all of the descendants of their precursors
to be correctly rebased.
"""
childrenof = _getchildrelationships(repo, base)
# Perform BFS starting from base.
queue = deque([base])
targets = []
processed = set()
while queue:
rev = queue.popleft()
# Merges may result in the same revision being added to the queue
# multiple times. Filter those cases out.
if rev in processed:
continue
processed.add(rev)
queue.extend(childrenof[rev])
# Look for visible precursors (which are probably visible because
# they have unstable descendants) and successors (for which the latest
# non-obsolete version should be visible).
precursors = repo.revs('allprecursors(%d)', rev)
successors = repo.revs('allsuccessors(%d)', rev)
# If this changeset has precursors but no successor, then
# if its precursors have children those children need to be
# rebased onto the changeset.
if precursors and not successors:
children = []
for p in precursors:
children.extend(childrenof[p])
if children:
queue.extend(children)
targets.append(rev)
# We need to perform the rebases in reverse-BFS order so that
# obsolescence information at lower levels is not modified by rebases
# at higher levels.
return reversed(targets)
def _getchildrelationships(repo, base):
"""Build a defaultdict of child relationships between all descendants of
base. This information will prevent us from having to repeatedly
perform children that reconstruct these relationships each time.
"""
cl = repo.changelog
children = defaultdict(list)
for rev in repo.revs('%d:: + allprecursors(%d)::', base, base):
for parent in cl.parentrevs(rev):
if parent != nullrev:
children[parent].append(rev)
return children
def _latest(repo, rev):
"""Find the "latest version" of the given revision -- either the
latest visible successor, or the revision itself if it has no
visible successors. Throws an exception if divergence is
detected.
"""
unfiltered = repo.unfiltered()
def leadstovisible(rev):
"""Return true if the given revision is visble, or if one
of the revisions in its chain of successors is visible.
"""
try:
return repo.revs('allsuccessors(%d) + %d', rev, rev)
except error.FilteredRepoLookupError:
return False
def getsuccessors(rev):
"""Return all successors of the given revision that leads
to a visible successor.
"""
return [
r for r in unfiltered.revs('successors(%d)', rev)
if leadstovisible(r)
]
# Right now this loop runs in O(n^2) due to the allsuccessors
# lookup inside getsuccessors(). This check is neccesary to deal
# with unamended changesets (which create situations where
# the latest successor is acutally obsolete, and we want a
# precursor instead. This logic could probably be made more
# sophisticated for better performance.
successors = getsuccessors(rev)
while successors:
if len(successors) > 1:
raise error.Abort(_("changeset %s has multiple newer versions, "
"cannot automatically determine latest verion")
% unfiltered[rev].hex())
rev = successors[0]
successors = getsuccessors(rev)
return rev
def _clearpreamend(repo, contexts):
"""Remove any preamend bookmarks on the given change contexts."""
for ctx in contexts:
for bookmark in repo.nodebookmarks(ctx.node()):
if bookmark.endswith('.preamend'):
repo._bookmarks.pop(bookmark, None)
def _deinhibit(repo, contexts):
"""Remove any inhibit markers on the given change contexts."""
if inhibitmod:
inhibitmod._deinhibitmarkers(
repo,
(p.node() for p in precursors)
)
return ret
inhibitmod._deinhibitmarkers(repo, (ctx.node() for ctx in contexts))
def _preamendname(repo, node):
suffix = '.preamend'
@ -574,13 +744,6 @@ def _usereducation(ui):
if education:
ui.warn(education + "\n")
def _clearpreamend(repo, contexts):
"""Remove any preamend bookmarks on the given change contexts."""
for ctx in contexts:
for bookmark in repo.nodebookmarks(ctx.node()):
if bookmark.endswith('.preamend'):
repo._bookmarks.pop(bookmark, None)
### bookmarks api compatibility layer ###
def bmactivate(repo, mark):
try:

View File

@ -121,7 +121,6 @@ Test multiple amends of same commit.
|/
o 0 add a
$ hg rebase --restack
rebasing 2:4538525df7e2 "add c"
$ showgraph
@ -191,3 +190,490 @@ Test conflict during rebasing.
|/
o 0 add a
Test finding a stable base commit from within the old stack.
$ reset
$ mkcommit a
$ mkcommit b
$ mkcommit c
$ mkcommit d
$ hg up 1
0 files updated, 0 files merged, 2 files removed, 0 files unresolved
$ echo b >> b
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ hg up 3
3 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ showgraph
o 5 add b
|
| @ 3 add d
| |
| o 2 add c
| |
| o 1 add b
|/
o 0 add a
$ hg rebase --restack
rebasing 2:4538525df7e2 "add c"
rebasing 3:47d2a3944de8 "add d"
$ showgraph
@ 7 add d
|
o 6 add c
|
o 5 add b
|
o 0 add a
Test finding a stable base commit from a new child of the amended commit.
$ reset
$ mkcommit a
$ mkcommit b
$ mkcommit c
$ mkcommit d
$ hg up 1
0 files updated, 0 files merged, 2 files removed, 0 files unresolved
$ echo b >> b
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ mkcommit e
$ showgraph
@ 6 add e
|
o 5 add b
|
| o 3 add d
| |
| o 2 add c
| |
| o 1 add b
|/
o 0 add a
$ hg rebase --restack
rebasing 2:4538525df7e2 "add c"
rebasing 3:47d2a3944de8 "add d"
$ showgraph
o 8 add d
|
o 7 add c
|
| @ 6 add e
|/
o 5 add b
|
o 0 add a
Test finding a stable base commit when there are multiple amends and
a commit on top of one of the obsolete intermediate commits.
$ reset
$ mkcommit a
$ mkcommit b
$ mkcommit c
$ mkcommit d
$ hg up 1
0 files updated, 0 files merged, 2 files removed, 0 files unresolved
$ echo b >> b
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ mkcommit e
$ hg prev
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
[5] add b
$ echo b >> b
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ hg up 6
2 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ showgraph
o 8 add b
|
| @ 6 add e
| |
| o 5 add b
|/
| o 3 add d
| |
| o 2 add c
| |
| o 1 add b
|/
o 0 add a
$ hg rebase --restack
rebasing 2:4538525df7e2 "add c"
rebasing 3:47d2a3944de8 "add d"
rebasing 6:c1992d8998fa "add e"
$ showgraph
@ 11 add e
|
| o 10 add d
| |
| o 9 add c
|/
o 8 add b
|
o 0 add a
Test that we only use the closest stable base commit.
$ reset
$ mkcommit a
$ mkcommit b
$ mkcommit c
$ mkcommit d
$ hg up 1
0 files updated, 0 files merged, 2 files removed, 0 files unresolved
$ echo b >> b
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ hg up 2
2 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ echo c >> c
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ hg up 3
2 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ showgraph
o 7 add c
|
| o 5 add b
| |
| | @ 3 add d
| | |
+---o 2 add c
| |
o | 1 add b
|/
o 0 add a
$ hg rebase --restack
rebasing 3:47d2a3944de8 "add d"
$ showgraph
@ 8 add d
|
o 7 add c
|
| o 5 add b
| |
o | 1 add b
|/
o 0 add a
Test what happens if there is no base commit found. The command should
fix up everything above the current commit, leaving other commits
below the current commit alone.
$ reset
$ mkcommit a
$ mkcommit b
$ mkcommit c
$ mkcommit d
$ mkcommit e
$ hg up 3
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
$ echo d >> d
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ hg up 0
0 files updated, 0 files merged, 3 files removed, 0 files unresolved
$ mkcommit f
created new head
$ hg up 1
1 files updated, 0 files merged, 1 files removed, 0 files unresolved
$ showgraph
o 7 add f
|
| o 6 add d
| |
| | o 4 add e
| | |
| | o 3 add d
| |/
| o 2 add c
| |
| @ 1 add b
|/
o 0 add a
$ hg rebase --restack
rebasing 4:9d206ffc875e "add e"
$ showgraph
o 8 add e
|
| o 7 add f
| |
o | 6 add d
| |
o | 2 add c
| |
@ | 1 add b
|/
o 0 add a
Test having an unamended commit.
$ reset
$ mkcommit a
$ mkcommit b
$ mkcommit c
$ hg prev
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
[1] add b
$ echo b >> b
$ hg amend -m "Amended"
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ echo b >> b
$ hg amend -m "Unamended"
$ hg unamend
$ hg up -C 1
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ showgraph
o 4 Amended
|
| o 2 add c
| |
| @ 1 add b
|/
o 0 add a
$ hg rebase --restack
rebasing 2:4538525df7e2 "add c"
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ showgraph
o 7 add c
|
@ 4 Amended
|
o 0 add a
Test situation with divergence.
$ reset
$ mkcommit a
$ mkcommit b
$ mkcommit c
$ hg prev
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
[1] add b
$ echo b >> b
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ hg up 1
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ echo c >> b
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ hg up 1
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ showgraph
o 6 add b
|
| o 4 add b
|/
| o 2 add c
| |
| @ 1 add b
|/
o 0 add a
$ hg rebase --restack
abort: changeset 7c3bad9141dcb46ff89abf5f61856facd56e476c has multiple newer versions, cannot automatically determine latest verion
[255]
Test situation with divergence due to an unamend. This should actually succeed
since the successor is obsolete.
$ reset
$ mkcommit a
$ mkcommit b
$ mkcommit c
$ hg prev
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
[1] add b
$ echo b >> b
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ hg up 1
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ echo c >> b
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ hg unamend
$ hg up -C 1
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ showgraph
o 4 add b
|
| o 2 add c
| |
| @ 1 add b
|/
o 0 add a
$ hg rebase --restack
rebasing 2:4538525df7e2 "add c"
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ showgraph
o 7 add c
|
@ 4 add b
|
o 0 add a
Test recursive restacking -- basic case.
$ reset
$ mkcommit a
$ mkcommit b
$ mkcommit c
$ mkcommit d
$ hg up 1
0 files updated, 0 files merged, 2 files removed, 0 files unresolved
$ echo b >> b
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ hg up 2
2 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ echo c >> c
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ hg up 1
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
$ showgraph
o 7 add c
|
| o 5 add b
| |
| | o 3 add d
| | |
+---o 2 add c
| |
@ | 1 add b
|/
o 0 add a
$ hg rebase --restack
rebasing 3:47d2a3944de8 "add d"
rebasing 7:a43fcd08f41f "add c" (tip)
rebasing 8:49b119a57122 "add d"
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ showgraph
o 10 add d
|
o 9 add c
|
@ 5 add b
|
o 0 add a
Test recursive restacking -- more complex case. This test is designed to
to check for a bug encountered if rebasing is performed naively from the
bottom-up wherein obsolescence information for commits further up the
stack is lost upon rebasing lower levels.
$ reset
$ mkcommit a
$ mkcommit b
$ mkcommit c
$ mkcommit d
$ hg up 1
0 files updated, 0 files merged, 2 files removed, 0 files unresolved
$ echo b >> b
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ mkcommit e
$ mkcommit f
$ hg prev
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
[6] add e
$ echo e >> e
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ hg up 2
2 files updated, 0 files merged, 1 files removed, 0 files unresolved
$ echo c >> c
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ mkcommit g
$ mkcommit h
$ hg prev
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
[12] add g
$ echo g >> g
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ hg up 1
0 files updated, 0 files merged, 2 files removed, 0 files unresolved
$ showgraph
o 15 add g
|
| o 13 add h
| |
| o 12 add g
|/
o 11 add c
|
| o 9 add e
| |
| | o 7 add f
| | |
| | o 6 add e
| |/
| o 5 add b
| |
| | o 3 add d
| | |
+---o 2 add c
| |
@ | 1 add b
|/
o 0 add a
$ hg rebase --restack
rebasing 13:9f2a7cefd4b4 "add h"
rebasing 3:47d2a3944de8 "add d"
rebasing 7:2a79e3a98cd6 "add f"
rebasing 11:a43fcd08f41f "add c"
rebasing 15:604f34a1983d "add g" (tip)
rebasing 16:e1df23499b99 "add h"
rebasing 17:49b119a57122 "add d"
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ showgraph
o 22 add d
|
| o 21 add h
| |
| o 20 add g
|/
o 19 add c
|
| o 18 add f
| |
| o 9 add e
|/
@ 5 add b
|
o 0 add a