diff --git a/hgext3rd/fbamend/restack.py b/hgext3rd/fbamend/restack.py --- a/hgext3rd/fbamend/restack.py +++ b/hgext3rd/fbamend/restack.py @@ -7,15 +7,13 @@ from __future__ import absolute_import -from collections import deque - +from mercurial.i18n import _ from mercurial import ( - cmdutil, commands, - error, + revsetlang, ) -from . import common +from hgext import rebase def restack(ui, repo, rebaseopts=None): """Repair a situation in which one or more changesets in a stack @@ -23,91 +21,40 @@ unstable) by finding any such changesets and rebasing their descendants onto the latest version of each respective changeset. """ - if rebaseopts is None: - rebaseopts = {} - - with repo.wlock(), repo.lock(): - cmdutil.checkunfinished(repo) - cmdutil.bailifchanged(repo) - - # Find the latest version of the changeset at the botom of the - # current stack. If the current changeset is public, simply start - # restacking from the current changeset with the assumption - # that there are non-public changesets higher up. - base = repo.revs('::. & draft()').first() - latest = (common.latest(repo, base) if base is not None - else repo['.'].rev()) - targets = _findrestacktargets(repo, latest) + rebaseopts = (rebaseopts or {}).copy() - 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: - common.restackonce(ui, repo, rev, rebaseopts) - except error.InterventionRequired: - tr.close() - raise + # TODO: Remove config override after https://phab.mercurial-scm.org/D1063 + config = {('experimental', 'rebase.multidest'): True} - # Ensure that we always end up on the latest version of the - # current changeset. Usually, this will be taken care of - # by the rebase operation. However, in some cases (such as - # if we are on the precursor of the base changeset) the - # rebase will not update to the latest version, so we need - # to do this manually. - successor = repo.revs('allsuccessors(.)').last() - if successor is not None: - commands.update(ui, repo, rev=successor) + with ui.configoverride(config), repo.wlock(), repo.lock(): + # Find drafts connected to the current stack via either changelog or + # obsolete graph. Note: "draft() & ::." is optimized by D441. -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 = common.getchildrelationships(repo, - repo.revs('%d + allpredecessors(%d)', base, 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) + # 1. Connect drafts via changelog + revs = list(repo.revs('(draft() & ::.)::')) + if not revs: + # "." is probably public. Check its direct children. + revs = repo.revs('draft() & children(.)') + if not revs: + ui.status(_('nothing to restack\n')) + return 1 + # 2. Connect revs via obsolete graph + revs = list(repo.revs('successors(%ld)+allpredecessors(%ld)', + revs, revs)) + # 3. Connect revs via changelog again to cover missing revs + revs = list(repo.revs('(draft() & ::%ld)::', revs)) - # Children need to be added in sorted order so that newer - # children (as determined by rev number) will have their - # descendants of their precursors rebased before older children. - # This ensures that unstable changesets will always be rebased - # onto the latest visible successor of their parent changeset. - queue.extend(sorted(childrenof[rev])) + rebaseopts['rev'] = [revsetlang.formatspec('%ld', revs)] + rebaseopts['dest'] = '_destrestack(SRC)' - # 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('allpredecessors(%d)', rev) - successors = repo.revs('allsuccessors(%d)', rev) + rebase.rebase(ui, repo, **rebaseopts) - # 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) + # Ensure that we always end up on the latest version of the + # current changeset. Usually, this will be taken care of + # by the rebase operation. However, in some cases (such as + # if we are on the precursor of the base changeset) the + # rebase will not update to the latest version, so we need + # to do this manually. + successor = repo.revs('allsuccessors(.)').last() + if successor is not None: + commands.update(ui, repo, rev=successor) diff --git a/hgext3rd/fbamend/revsets.py b/hgext3rd/fbamend/revsets.py --- a/hgext3rd/fbamend/revsets.py +++ b/hgext3rd/fbamend/revsets.py @@ -7,8 +7,10 @@ from __future__ import absolute_import +from mercurial.node import nullrev from mercurial import ( obsutil, + phases, registrar, revset, smartset, @@ -59,3 +61,57 @@ """All changesets which are predecessors for given set, recursively""" f = lambda nodes: obsutil.allpredecessors(repo.obsstore, nodes) return _calculateset(repo, subset, x, f) + +@revsetpredicate('_destrestack(SRC)') +def _destrestack(repo, subset, x): + """restack destination for given single source revision""" + unfi = repo.unfiltered() + obsoleted = unfi.revs('obsolete()') + getparents = unfi.changelog.parentrevs + getphase = unfi._phasecache.phase + nodemap = unfi.changelog.nodemap + + src = revset.getset(repo, subset, x).first() + + # Empty src or already obsoleted - Do not return a destination + if not src or src in obsoleted: + return smartset.baseset() + + # Find the obsoleted "base" by checking source's parent recursively + base = src + while base not in obsoleted: + base = getparents(base)[0] + # When encountering a public revision which cannot be obsoleted, stop + # the search early and return no destination. Do the same for nullrev. + if getphase(repo, base) == phases.public or base == nullrev: + return smartset.baseset() + + # Find successors for given base + # NOTE: Ideally we can use obsutil.successorssets to detect divergence + # case. However it does not support cycles (unamend) well. So we use + # allsuccessors and pick non-obsoleted successors manually as a workaround. + basenode = repo[base].node() + succnodes = [n for n in obsutil.allsuccessors(repo.obsstore, [basenode]) + if (n != basenode and n in nodemap + and nodemap[n] not in obsoleted)] + + # In case of a split, only keep its heads + succrevs = list(unfi.revs('heads(%ln)', succnodes)) + + if len(succrevs) == 0: + # Prune - Find the first non-obsoleted ancestor + while base in obsoleted: + base = getparents(base)[0] + if base == nullrev: + # Root node is pruned. The new base (destination) is the + # virtual nullrev. + return smartset.baseset([nullrev]) + return smartset.baseset([base]) + elif len(succrevs) == 1: + # Unique visible successor case - A valid destination + return smartset.baseset([succrevs[0]]) + else: + # Multiple visible successors - Choose the one with a greater revision + # number. This is to be compatible with restack old behavior. We might + # want to revisit it when we introduce the divergence concept to users. + return smartset.baseset([max(succrevs)]) diff --git a/tests/test-copytrace-amend.t b/tests/test-copytrace-amend.t --- a/tests/test-copytrace-amend.t +++ b/tests/test-copytrace-amend.t @@ -207,12 +207,11 @@ rebasing 2:ad25e018afa9 "mod a" merging b and a to b rebasing 3:ba0395f0e180 "delete a" - transaction abort! - rollback completed abort: a@ba0395f0e180: not found in manifest! [255] $ hg rebase --abort - rebase aborted (no revision is removed, only broken state is cleared) + saved backup bundle to $TESTTMP/repo/.hg/strip-backup/3fd0353a7967-a25c7d46-backup.hg (glob) + rebase aborted $ cd .. $ rm -rf repo diff --git a/tests/test-fbamend-restack.t b/tests/test-fbamend-restack.t --- a/tests/test-fbamend-restack.t +++ b/tests/test-fbamend-restack.t @@ -341,13 +341,12 @@ |/ o 0 add a $ hg rebase --restack + rebasing 5:a43fcd08f41f "add c" (tip) rebasing 3:47d2a3944de8 "add d" - rebasing 5:a43fcd08f41f "add c" (tip) - rebasing 6:49b119a57122 "add d" $ showgraph - @ 8 add d + @ 7 add d | - o 7 add c + o 6 add c | o 4 add b | @@ -521,28 +520,22 @@ |/ o 0 add a $ hg unamend - $ hg up -C 1 + $ hg up -C 3 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ showgraph - o 3 add b + @ 3 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 5 add c - | - @ 3 add b - | | o 1 add b |/ o 0 add a +Revision 2 "add c" is already stable (not orphaned) so restack does nothing: + + $ hg rebase --restack + nothing to rebase - empty destination + Test recursive restacking -- basic case. $ reset $ mkcommit a @@ -576,14 +569,13 @@ |/ o 0 add a $ hg rebase --restack + rebasing 5:a43fcd08f41f "add c" (tip) rebasing 3:47d2a3944de8 "add d" - rebasing 5:a43fcd08f41f "add c" (tip) - rebasing 6:49b119a57122 "add d" 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ showgraph - o 8 add d + o 7 add d | - o 7 add c + o 6 add c | @ 4 add b | @@ -657,24 +649,22 @@ |/ o 0 add a $ hg rebase --restack - rebasing 10:9f2a7cefd4b4 "add h" rebasing 6:2a79e3a98cd6 "add f" - rebasing 3:47d2a3944de8 "add d" rebasing 8:a43fcd08f41f "add c" rebasing 11:604f34a1983d "add g" (tip) - rebasing 12:e1df23499b99 "add h" - rebasing 14:49b119a57122 "add d" + rebasing 3:47d2a3944de8 "add d" + rebasing 10:9f2a7cefd4b4 "add h" 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ showgraph - o 18 add d + o 16 add h | - | o 17 add h + | o 15 add d | | - | o 16 add g + o | 14 add g |/ - o 15 add c + o 13 add c | - | o 13 add f + | o 12 add f | | | o 7 add e |/ @@ -684,7 +674,7 @@ |/ o 0 add a -Suboptimal case: restack rebases "D" twice +Restack does topological sort and only rebases "D" once: $ reset $ hg debugdrawdag<<'EOS' @@ -717,13 +707,12 @@ |/ o 0 A $ hg rebase --restack + rebasing 5:ca53c8ceb284 "C" rebasing 3:f585351a92f8 "D" (D) - rebasing 5:ca53c8ceb284 "C" - rebasing 7:4da953fe10f3 "D" $ showgraph - o 9 D + o 8 D | - o 8 C + o 7 C | @ 6 B3 | @@ -736,3 +725,145 @@ | x 1 B |/ o 0 A + +Restack will only restack the "current" stack and leave other stacks untouched. + + $ reset + $ hg debugdrawdag<<'EOS' + > D H K + > | | | + > B C F G J L # amend: B -> C + > |/ |/ |/ # amend: F -> G + > A E I Z # amend: J -> L + > EOS + + $ hg phase --public -r Z+I+A+E + + $ hg update -q Z + $ hg rebase --restack + nothing to restack + [1] + + $ hg update -q D + $ hg rebase --restack + rebasing 10:be0ef73c17ad "D" (D) + + $ hg update -q G + $ hg rebase --restack + rebasing 11:cc209258a732 "H" (H) + + $ hg update -q I + $ hg rebase --restack + rebasing 12:59760668f0e1 "K" (K) + + $ rm .hg/localtags + $ showgraph + o 15 K + | + | o 14 H + | | + | | o 13 D + | | | + o | | 9 L + | | | + | o | 7 G + | | | + | | o 5 C + | | | + | | | o 3 Z + | | | + @ | | 2 I + / / + o / 1 E + / + o 0 A + +The "prune" cases. + + $ reset + $ hg debugdrawdag<<'EOS' + > D E + > |/ + > C + > | # amend: F -> F2 + > B G H # prune: A, C, F2 + > | |/ + > A F F2 + > EOS + + $ hg update -q B + $ hg rebase --restack + rebasing 3:112478962961 "B" (B) + rebasing 7:f585351a92f8 "D" (D) + rebasing 8:78d2dca436b2 "E" (E tip) + + $ hg update -q H + $ hg rebase --restack + rebasing 4:8fdb2c1feb20 "G" (G) + rebasing 5:02ac06fe83b9 "H" (H) + + $ rm .hg/localtags + $ showgraph + @ 13 H + + o 12 G + + o 11 E + | + | o 10 D + |/ + o 9 B + +Restack could resume after resolving merge conflicts. + + $ reset + $ hg debugdrawdag<<'EOS' + > F G # F/C = F # cause conflict + > | | # G/E = G # cause conflict + > B C D E # amend: B -> C + > |/ |/ # amend: D -> E + > | / + > | / + > | / + > |/ + > A + > EOS + + $ hg update -q F + $ hg rebase --restack + rebasing 5:ed8545a5c22a "F" (F) + merging C + warning: conflicts while merging C! (edit, then use 'hg resolve --mark') + unresolved conflicts (see hg resolve, then hg rebase --continue) + [1] + + $ rm .hg/localtags + + $ echo R > C + $ hg resolve --mark -q + continue: hg rebase --continue + $ hg rebase --continue + rebasing 5:ed8545a5c22a "F" + rebasing 6:4d1ef7d890c5 "G" (tip) + merging E + warning: conflicts while merging E! (edit, then use 'hg resolve --mark') + unresolved conflicts (see hg resolve, then hg rebase --continue) + [1] + + $ echo R > E + $ hg resolve --mark -q + continue: hg rebase --continue + $ hg rebase --continue + already rebased 5:ed8545a5c22a "F" as 2282fe522d5c + rebasing 6:4d1ef7d890c5 "G" + + $ showgraph + o 8 G + | + | @ 7 F + | | + o | 4 E + | | + | o 2 C + |/ + o 0 A