Mercurial > public > mercurial-scm > hg-stable
diff hgext/rebase.py @ 34024:32528419db64
rebase: sort destmap topologically
Previously rebase source and destination could not overlap. But with the
multi-destination support, source and destination could reasonably partially
overlap. That requires another topological sort on `{sourcerev: destrev}`
graph (destmap). This patch implements that.
If a revision's destination is itself, the error message gets changed from
"source is ancestor of destination" to "source and destination form a
cycle". Not marking as BC since automation should depend on exit code, not
error message.
Differential Revision: https://phab.mercurial-scm.org/D470
author | Jun Wu <quark@fb.com> |
---|---|
date | Mon, 21 Aug 2017 20:22:07 -0700 |
parents | 5e83a8fe6bc4 |
children | 06b0cc2588de |
line wrap: on
line diff
--- a/hgext/rebase.py Tue Aug 29 17:27:37 2017 -0700 +++ b/hgext/rebase.py Mon Aug 21 20:22:07 2017 -0700 @@ -361,7 +361,7 @@ self.ui.status(_('reopening closed branch head %s\n') % dest) def _performrebase(self, tr): - repo, ui, opts = self.repo, self.ui, self.opts + repo, ui = self.repo, self.ui if self.keepbranchesf: # insert _savebranch at the start of extrafns so if # there's a user-provided extrafn it can clobber branch if @@ -384,10 +384,17 @@ # if we fail before the transaction closes. self.storestatus() - sortedrevs = repo.revs('sort(%ld, -topo)', self.state) cands = [k for k, v in self.state.iteritems() if v == revtodo] total = len(cands) pos = 0 + for subset in sortsource(self.destmap): + pos = self._performrebasesubset(tr, subset, pos, total) + ui.progress(_('rebasing'), None) + ui.note(_('rebase merging completed\n')) + + def _performrebasesubset(self, tr, subset, pos, total): + repo, ui, opts = self.repo, self.ui, self.opts + sortedrevs = repo.revs('sort(%ld, -topo)', subset) for rev in sortedrevs: dest = self.destmap[rev] ctx = repo[rev] @@ -449,9 +456,7 @@ else: ui.status(_('already rebased %s as %s\n') % (desc, repo[self.state[rev]])) - - ui.progress(_('rebasing'), None) - ui.note(_('rebase merging completed\n')) + return pos def _finishrebase(self): repo, ui, opts = self.repo, self.ui, self.opts @@ -969,6 +974,18 @@ | B | ... |/ |/ A A + + Besides, adjust dest according to existing rebase information. For example, + + B C D B needs to be rebased on top of C, C needs to be rebased on top + \|/ of D. We will rebase C first. + A + + C' After rebasing C, when considering B's destination, use C' + | instead of the original C. + B D + \ / + A """ # pick already rebased revs with same dest from state as interesting source dest = destmap[rev] @@ -981,6 +998,12 @@ candidate = repo.revs('max(%ld and (::%d))', source, prev).first() if candidate is not None: adjusted = state[candidate] + if adjusted == dest and dest in state: + adjusted = state[dest] + if adjusted == revtodo: + # sortsource should produce an order that makes this impossible + raise error.ProgrammingError( + 'rev %d should be rebased already at this time' % dest) result.append(adjusted) return result @@ -1128,6 +1151,12 @@ 'moving at least one of its parents') % (rev, repo[rev])) + # Source should not be ancestor of dest. The check here guarantees it's + # impossible. With multi-dest, the initial check does not cover complex + # cases since we don't have abstractions to dry-run rebase cheaply. + if any(p != nullrev and isancestor(rev, p) for p in newps): + raise error.Abort(_('source is ancestor of destination')) + # "rebasenode" updates to new p1, use the corresponding merge base. if bases[0] != nullrev: base = bases[0] @@ -1354,6 +1383,31 @@ repo.ui.warn(_('rebase aborted\n')) return 0 +def sortsource(destmap): + """yield source revisions in an order that we only rebase things once + + If source and destination overlaps, we should filter out revisions + depending on other revisions which hasn't been rebased yet. + + Yield a sorted list of revisions each time. + + For example, when rebasing A to B, B to C. This function yields [B], then + [A], indicating B needs to be rebased first. + + Raise if there is a cycle so the rebase is impossible. + """ + srcset = set(destmap) + while srcset: + srclist = sorted(srcset) + result = [] + for r in srclist: + if destmap[r] not in srcset: + result.append(r) + if not result: + raise error.Abort(_('source and destination form a cycle')) + srcset -= set(result) + yield result + def buildstate(repo, destmap, collapse, obsoletenotrebased): '''Define which revisions are going to be rebased and where @@ -1372,12 +1426,21 @@ if set(destmap.values()) & mqapplied: raise error.Abort(_('cannot rebase onto an applied mq patch')) - roots = list(repo.set('roots(%ld)', rebaseset)) + # Get "cycle" error early by exhausting the generator. + sortedsrc = list(sortsource(destmap)) # a list of sorted revs + if not sortedsrc: + raise error.Abort(_('no matching revisions')) + + # Only check the first batch of revisions to rebase not depending on other + # rebaseset. This means "source is ancestor of destination" for the second + # (and following) batches of revisions are not checked here. We rely on + # "defineparents" to do that check. + roots = list(repo.set('roots(%ld)', sortedsrc[0])) if not roots: raise error.Abort(_('no matching revisions')) roots.sort() state = dict.fromkeys(rebaseset, revtodo) - emptyrebase = True + emptyrebase = (len(sortedsrc) == 1) for root in roots: dest = repo[destmap[root.rev()]] commonbase = root.ancestor(dest)