Mercurial > hg
changeset 18424:100fdc84670f
rebase: support multiple roots for rebaseset
We have all the necessary mechanism to rebase a set with multiple roots, we only
needed a proper handling of this case we preparing and concluding the rebase.
This changeset des that.
Rebase set with multiple root allows some awesome usage of rebase like:
- rebase all your draft on lastest upstream
hg rebase --dest @ --rev 'draft()'
- exclusion of specific changeset during rebase
hg rebase --rev '42:: - author(Babar)'
- rebase a set of revision were multiple roots are later merged
hg rebase --rev '(18+42)::'
author | Pierre-Yves David <pierre-yves.david@ens-lyon.org> |
---|---|
date | Thu, 17 Jan 2013 00:35:01 +0100 |
parents | 5d6ee2494f63 |
children | 6da1e979340a |
files | hgext/rebase.py tests/test-rebase-obsolete.t tests/test-rebase-scenario-global.t |
diffstat | 3 files changed, 196 insertions(+), 67 deletions(-) [+] |
line wrap: on
line diff
--- a/hgext/rebase.py Wed Jan 16 05:21:11 2013 +0100 +++ b/hgext/rebase.py Thu Jan 17 00:35:01 2013 +0100 @@ -574,9 +574,9 @@ merge.update(repo, repo[originalwd].rev(), False, True, False) rebased = filter(lambda x: x > -1 and x != target, state.values()) if rebased: - strippoint = min(rebased) + strippoints = [c.node() for c in repo.set('roots(%ld)', rebased)] # no backup of rebased cset versions needed - repair.strip(repo.ui, repo, repo[strippoint].node()) + repair.strip(repo.ui, repo, strippoints) clearstatus(repo) repo.ui.warn(_('rebase aborted\n')) return 0 @@ -599,65 +599,65 @@ roots = list(repo.set('roots(%ld)', rebaseset)) if not roots: raise util.Abort(_('no matching revisions')) - if len(roots) > 1: - raise util.Abort(_("can't rebase multiple roots")) - root = roots[0] - - commonbase = root.ancestor(dest) - if commonbase == root: - raise util.Abort(_('source is ancestor of destination')) - if commonbase == dest: - samebranch = root.branch() == dest.branch() - if not collapse and samebranch and root in dest.children(): - repo.ui.debug('source is a child of destination\n') - return None + roots.sort() + state = {} + detachset = set() + for root in roots: + commonbase = root.ancestor(dest) + if commonbase == root: + raise util.Abort(_('source is ancestor of destination')) + if commonbase == dest: + samebranch = root.branch() == dest.branch() + if not collapse and samebranch and root in dest.children(): + repo.ui.debug('source is a child of destination\n') + return None - repo.ui.debug('rebase onto %d starting from %d\n' % (dest, root)) - state = dict.fromkeys(rebaseset, nullrev) - # Rebase tries to turn <dest> into a parent of <root> while - # preserving the number of parents of rebased changesets: - # - # - A changeset with a single parent will always be rebased as a - # changeset with a single parent. - # - # - A merge will be rebased as merge unless its parents are both - # ancestors of <dest> or are themselves in the rebased set and - # pruned while rebased. - # - # If one parent of <root> is an ancestor of <dest>, the rebased - # version of this parent will be <dest>. This is always true with - # --base option. - # - # Otherwise, we need to *replace* the original parents with - # <dest>. This "detaches" the rebased set from its former location - # and rebases it onto <dest>. Changes introduced by ancestors of - # <root> not common with <dest> (the detachset, marked as - # nullmerge) are "removed" from the rebased changesets. - # - # - If <root> has a single parent, set it to <dest>. - # - # - If <root> is a merge, we cannot decide which parent to - # replace, the rebase operation is not clearly defined. - # - # The table below sums up this behavior: - # - # +--------------------+----------------------+-------------------------+ - # | | one parent | merge | - # +--------------------+----------------------+-------------------------+ - # | parent in ::<dest> | new parent is <dest> | parents in ::<dest> are | - # | | | remapped to <dest> | - # +--------------------+----------------------+-------------------------+ - # | unrelated source | new parent is <dest> | ambiguous, abort | - # +--------------------+----------------------+-------------------------+ - # - # The actual abort is handled by `defineparents` - if len(root.parents()) <= 1: - # ancestors of <root> not ancestors of <dest> - detachset = repo.changelog.findmissingrevs([commonbase.rev()], - [root.rev()]) - state.update(dict.fromkeys(detachset, nullmerge)) - # detachset can have root, and we definitely want to rebase that - state[root.rev()] = nullrev + repo.ui.debug('rebase onto %d starting from %s\n' % (dest, roots)) + state.update(dict.fromkeys(rebaseset, nullrev)) + # Rebase tries to turn <dest> into a parent of <root> while + # preserving the number of parents of rebased changesets: + # + # - A changeset with a single parent will always be rebased as a + # changeset with a single parent. + # + # - A merge will be rebased as merge unless its parents are both + # ancestors of <dest> or are themselves in the rebased set and + # pruned while rebased. + # + # If one parent of <root> is an ancestor of <dest>, the rebased + # version of this parent will be <dest>. This is always true with + # --base option. + # + # Otherwise, we need to *replace* the original parents with + # <dest>. This "detaches" the rebased set from its former location + # and rebases it onto <dest>. Changes introduced by ancestors of + # <root> not common with <dest> (the detachset, marked as + # nullmerge) are "removed" from the rebased changesets. + # + # - If <root> has a single parent, set it to <dest>. + # + # - If <root> is a merge, we cannot decide which parent to + # replace, the rebase operation is not clearly defined. + # + # The table below sums up this behavior: + # + # +------------------+----------------------+-------------------------+ + # | | one parent | merge | + # +------------------+----------------------+-------------------------+ + # | parent in | new parent is <dest> | parents in ::<dest> are | + # | ::<dest> | | remapped to <dest> | + # +------------------+----------------------+-------------------------+ + # | unrelated source | new parent is <dest> | ambiguous, abort | + # +------------------+----------------------+-------------------------+ + # + # The actual abort is handled by `defineparents` + if len(root.parents()) <= 1: + # ancestors of <root> not ancestors of <dest> + detachset.update(repo.changelog.findmissingrevs([commonbase.rev()], + [root.rev()])) + for r in detachset: + if r not in state: + state[r] = nullmerge return repo['.'].rev(), dest.rev(), state def clearrebased(ui, repo, state, collapsedas=None): @@ -677,12 +677,16 @@ else: rebased = [rev for rev in state if state[rev] != nullmerge] if rebased: - if set(repo.changelog.descendants([min(rebased)])) - set(state): - ui.warn(_("warning: new changesets detected " - "on source branch, not stripping\n")) - else: + stripped = [] + for root in repo.set('roots(%ld)', rebased): + if set(repo.changelog.descendants([root.rev()])) - set(state): + ui.warn(_("warning: new changesets detected " + "on source branch, not stripping\n")) + else: + stripped.append(root.node()) + if stripped: # backup the old csets by default - repair.strip(ui, repo, repo[min(rebased)].node(), "all") + repair.strip(ui, repo, stripped, "all") def pullrebase(orig, ui, repo, *args, **opts):
--- a/tests/test-rebase-obsolete.t Wed Jan 16 05:21:11 2013 +0100 +++ b/tests/test-rebase-obsolete.t Thu Jan 17 00:35:01 2013 +0100 @@ -306,3 +306,26 @@ +Test multiple root handling +------------------------------------ + + $ hg rebase --dest 4 --rev '7+11+9' + $ hg log -G + @ 14:00891d85fcfc C + | + | o 13:102b4c1d889b D + |/ + | o 12:bfe264faf697 H + |/ + | o 10:7c6027df6a99 B + | | + | x 7:02de42196ebe H + | | + +---o 6:eea13746799a G + | |/ + | o 5:24b6387c8c8c F + | | + o | 4:9520eea781bc E + |/ + o 0:cd010b8cd998 A +
--- a/tests/test-rebase-scenario-global.t Wed Jan 16 05:21:11 2013 +0100 +++ b/tests/test-rebase-scenario-global.t Thu Jan 17 00:35:01 2013 +0100 @@ -542,6 +542,108 @@ $ hg clone -q -u . ah ah6 $ cd ah6 $ hg rebase -r '(4+6)::' -d 1 - abort: can't rebase multiple roots - [255] + saved backup bundle to $TESTTMP/ah6/.hg/strip-backup/3d8a618087a7-backup.hg (glob) + $ hg tglog + @ 8: 'I' + | + o 7: 'H' + | + o 6: 'G' + | + | o 5: 'F' + | | + | o 4: 'E' + |/ + | o 3: 'D' + | | + | o 2: 'C' + | | + o | 1: 'B' + |/ + o 0: 'A' + $ cd .. + +More complexe rebase with multiple roots +each root have a different common ancestor with the destination and this is a detach + +(setup) + + $ hg clone -q -u . a a8 + $ cd a8 + $ echo I > I + $ hg add I + $ hg commit -m I + $ hg up 4 + 1 files updated, 0 files merged, 3 files removed, 0 files unresolved + $ echo I > J + $ hg add J + $ hg commit -m J + created new head + $ echo I > K + $ hg add K + $ hg commit -m K + $ hg tglog + @ 10: 'K' + | + o 9: 'J' + | + | o 8: 'I' + | | + | o 7: 'H' + | | + +---o 6: 'G' + | |/ + | o 5: 'F' + | | + o | 4: 'E' + |/ + | o 3: 'D' + | | + | o 2: 'C' + | | + | o 1: 'B' + |/ + o 0: 'A' + +(actual test) + + $ hg rebase --dest 'desc(G)' --rev 'desc(K) + desc(I)' + saved backup bundle to $TESTTMP/a8/.hg/strip-backup/23a4ace37988-backup.hg (glob) + $ hg log --rev 'children(desc(G))' + changeset: 9:adb617877056 + parent: 6:eea13746799a + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: I + + changeset: 10:882431a34a0e + tag: tip + parent: 6:eea13746799a + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: K + + $ hg tglog + @ 10: 'K' + | + | o 9: 'I' + |/ + | o 8: 'J' + | | + | | o 7: 'H' + | | | + o---+ 6: 'G' + |/ / + | o 5: 'F' + | | + o | 4: 'E' + |/ + | o 3: 'D' + | | + | o 2: 'C' + | | + | o 1: 'B' + |/ + o 0: 'A' +