diff options
Diffstat (limited to 'hgext/rebase.py')
-rw-r--r-- | hgext/rebase.py | 304 |
1 files changed, 107 insertions, 197 deletions
diff --git a/hgext/rebase.py b/hgext/rebase.py index f276fcf..ad62f8a 100644 --- a/hgext/rebase.py +++ b/hgext/rebase.py @@ -15,7 +15,7 @@ http://mercurial.selenic.com/wiki/RebaseExtension ''' from mercurial import hg, util, repair, merge, cmdutil, commands, bookmarks -from mercurial import extensions, patch, scmutil, phases +from mercurial import extensions, copies, patch from mercurial.commands import templateopts from mercurial.node import nullrev from mercurial.lock import release @@ -26,7 +26,6 @@ nullmerge = -2 cmdtable = {} command = cmdutil.command(cmdtable) -testedwith = 'internal' @command('rebase', [('s', 'source', '', @@ -35,25 +34,23 @@ testedwith = 'internal' _('rebase from the base of the specified changeset ' '(up to greatest common ancestor of base and dest)'), _('REV')), - ('r', 'rev', [], - _('rebase these revisions'), - _('REV')), ('d', 'dest', '', _('rebase onto the specified changeset'), _('REV')), ('', 'collapse', False, _('collapse the rebased changesets')), ('m', 'message', '', _('use text as collapse commit message'), _('TEXT')), - ('e', 'edit', False, _('invoke editor on commit messages')), ('l', 'logfile', '', _('read collapse commit message from file'), _('FILE')), ('', 'keep', False, _('keep original changesets')), ('', 'keepbranches', False, _('keep original branch names')), - ('D', 'detach', False, _('(DEPRECATED)')), + ('', 'detach', False, _('force detaching of source from its original ' + 'branch')), ('t', 'tool', '', _('specify merge tool')), ('c', 'continue', False, _('continue an interrupted rebase')), ('a', 'abort', False, _('abort an interrupted rebase'))] + templateopts, - _('[-s REV | -b REV] [-d REV] [OPTION]')) + _('hg rebase [-s REV | -b REV] [-d REV] [options]\n' + 'hg rebase {-a|-c}')) def rebase(ui, repo, **opts): """move changeset (and descendants) to a different branch @@ -108,20 +105,15 @@ def rebase(ui, repo, **opts): skipped = set() targetancestors = set() - editor = None - if opts.get('edit'): - editor = cmdutil.commitforceeditor - lock = wlock = None try: - wlock = repo.wlock() lock = repo.lock() + wlock = repo.wlock() # Validate input and define rebasing points destf = opts.get('dest', None) srcf = opts.get('source', None) basef = opts.get('base', None) - revf = opts.get('rev', []) contf = opts.get('continue') abortf = opts.get('abort') collapsef = opts.get('collapse', False) @@ -129,6 +121,7 @@ def rebase(ui, repo, **opts): extrafn = opts.get('extrafn') # internal, used by e.g. hgsubversion keepf = opts.get('keep', False) keepbranchesf = opts.get('keepbranches', False) + detachf = opts.get('detach', False) # keepopen is not meant for use on the command line, but by # other extensions keepopen = opts.get('keepopen', False) @@ -143,6 +136,8 @@ def rebase(ui, repo, **opts): if collapsef: raise util.Abort( _('cannot use collapse with continue or abort')) + if detachf: + raise util.Abort(_('cannot use detach with continue or abort')) if srcf or basef or destf: raise util.Abort( _('abort and continue do not allow specifying revisions')) @@ -156,56 +151,16 @@ def rebase(ui, repo, **opts): else: if srcf and basef: raise util.Abort(_('cannot specify both a ' - 'source and a base')) - if revf and basef: - raise util.Abort(_('cannot specify both a ' 'revision and a base')) - if revf and srcf: - raise util.Abort(_('cannot specify both a ' - 'revision and a source')) + if detachf: + if not srcf: + raise util.Abort( + _('detach requires a revision to be specified')) + if basef: + raise util.Abort(_('cannot specify a base with detach')) cmdutil.bailifchanged(repo) - - if not destf: - # Destination defaults to the latest revision in the - # current branch - branch = repo[None].branch() - dest = repo[branch] - else: - dest = scmutil.revsingle(repo, destf) - - if revf: - rebaseset = repo.revs('%lr', revf) - elif srcf: - src = scmutil.revrange(repo, [srcf]) - rebaseset = repo.revs('(%ld)::', src) - else: - base = scmutil.revrange(repo, [basef or '.']) - rebaseset = repo.revs( - '(children(ancestor(%ld, %d)) and ::(%ld))::', - base, dest, base) - - if rebaseset: - root = min(rebaseset) - else: - root = None - - if not rebaseset: - repo.ui.debug('base is ancestor of destination\n') - result = None - elif not keepf and list(repo.revs('first(children(%ld) - %ld)', - rebaseset, rebaseset)): - raise util.Abort( - _("can't remove original changesets with" - " unrebased descendants"), - hint=_('use --keep to keep original changesets')) - elif not keepf and not repo[root].mutable(): - raise util.Abort(_("can't rebase immutable changeset %s") - % repo[root], - hint=_('see hg help phases for details')) - else: - result = buildstate(repo, dest, rebaseset, collapsef) - + result = buildstate(repo, destf, srcf, basef, detachf) if not result: # Empty state built, nothing to rebase ui.status(_('nothing to rebase\n')) @@ -213,8 +168,7 @@ def rebase(ui, repo, **opts): else: originalwd, target, state = result if collapsef: - targetancestors = set(repo.changelog.ancestors([target])) - targetancestors.add(target) + targetancestors = set(repo.changelog.ancestors(target)) external = checkexternal(repo, state, targetancestors) if keepbranchesf: @@ -232,14 +186,11 @@ def rebase(ui, repo, **opts): # Rebase if not targetancestors: - targetancestors = set(repo.changelog.ancestors([target])) + targetancestors = set(repo.changelog.ancestors(target)) targetancestors.add(target) # Keep track of the current bookmarks in order to reset them later currentbookmarks = repo._bookmarks.copy() - activebookmark = repo._bookmarkcurrent - if activebookmark: - bookmarks.unsetcurrent(repo) sortedstate = sorted(state) total = len(sortedstate) @@ -258,19 +209,18 @@ def rebase(ui, repo, **opts): else: try: ui.setconfig('ui', 'forcemerge', opts.get('tool', '')) - stats = rebasenode(repo, rev, p1, state, collapsef) + stats = rebasenode(repo, rev, p1, state) if stats and stats[3] > 0: raise util.Abort(_('unresolved conflicts (see hg ' 'resolve, then hg rebase --continue)')) finally: ui.setconfig('ui', 'forcemerge', '') - cmdutil.duplicatecopies(repo, rev, target) + updatedirstate(repo, rev, target, p2) if not collapsef: - newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn, - editor=editor) + newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn) else: # Skip commit if we are collapsing - repo.setparents(repo[p1].node()) + repo.dirstate.setparents(repo[p1].node()) newrev = None # Update the state if newrev is not None: @@ -297,7 +247,7 @@ def rebase(ui, repo, **opts): commitmsg += '\n* %s' % repo[rebased].description() commitmsg = ui.edit(commitmsg, repo.ui.username()) newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg, - extrafn=extrafn, editor=editor) + extrafn=extrafn) if 'qtip' in repo.tags(): updatemq(repo, state, skipped, **opts) @@ -313,7 +263,7 @@ def rebase(ui, repo, **opts): # Remove no more useful revisions rebased = [rev for rev in state if state[rev] != nullmerge] if rebased: - if set(repo.changelog.descendants([min(rebased)])) - set(state): + if set(repo.changelog.descendants(min(rebased))) - set(state): ui.warn(_("warning: new changesets detected " "on source branch, not stripping\n")) else: @@ -329,11 +279,6 @@ def rebase(ui, repo, **opts): util.unlinkpath(repo.sjoin('undo')) if skipped: ui.note(_("%d revisions have been skipped\n") % len(skipped)) - - if (activebookmark and - repo['tip'].node() == repo._bookmarks[activebookmark]): - bookmarks.setcurrent(repo, activebookmark) - finally: release(lock, wlock) @@ -356,10 +301,24 @@ def checkexternal(repo, state, targetancestors): external = p.rev() return external -def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None): +def updatedirstate(repo, rev, p1, p2): + """Keep track of renamed files in the revision that is going to be rebased + """ + # Here we simulate the copies and renames in the source changeset + cop, diver = copies.copies(repo, repo[rev], repo[p1], repo[p2], True) + m1 = repo[rev].manifest() + m2 = repo[p1].manifest() + for k, v in cop.iteritems(): + if k in m1: + if v in m1 or v in m2: + repo.dirstate.copy(v, k) + if v in m2 and v not in m1 and k in m2: + repo.dirstate.remove(v) + +def concludenode(repo, rev, p1, p2, commitmsg=None, extrafn=None): 'Commit the changes and store useful information in extra' try: - repo.setparents(repo[p1].node(), repo[p2].node()) + repo.dirstate.setparents(repo[p1].node(), repo[p2].node()) ctx = repo[rev] if commitmsg is None: commitmsg = ctx.description() @@ -368,20 +327,15 @@ def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None): extrafn(ctx, extra) # Commit might fail if unresolved files exist newrev = repo.commit(text=commitmsg, user=ctx.user(), - date=ctx.date(), extra=extra, editor=editor) + date=ctx.date(), extra=extra) repo.dirstate.setbranch(repo[newrev].branch()) - targetphase = max(ctx.phase(), phases.draft) - # retractboundary doesn't overwrite upper phase inherited from parent - newnode = repo[newrev].node() - if newnode: - phases.retractboundary(repo, targetphase, [newnode]) return newrev except util.Abort: # Invalidate the previous setparents repo.dirstate.invalidate() raise -def rebasenode(repo, rev, p1, state, collapse): +def rebasenode(repo, rev, p1, state): 'Rebase a single revision' # Merge phase # Update to target and merge it with local @@ -395,9 +349,7 @@ def rebasenode(repo, rev, p1, state, collapse): base = None if repo[rev].rev() != repo[min(state)].rev(): base = repo[rev].p1().node() - # When collapsing in-place, the parent is the common ancestor, we - # have to allow merging with it. - return merge.update(repo, rev, True, True, False, base, collapse) + return merge.update(repo, rev, True, True, False, base) def defineparents(repo, rev, target, state, targetancestors): 'Return the new parent relationship of the revision that will be rebased' @@ -446,7 +398,6 @@ def updatemq(repo, state, skipped, **opts): mqrebase = {} mq = repo.mq original_series = mq.fullseries[:] - skippedpatches = set() for p in mq.applied: rev = repo[p.node].rev() @@ -454,9 +405,6 @@ def updatemq(repo, state, skipped, **opts): repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' % (rev, p.name)) mqrebase[rev] = (p.name, isagitpatch(repo, p.name)) - else: - # Applied but not rebased, not sure this should happen - skippedpatches.add(p.name) if mqrebase: mq.finish(repo, mqrebase.keys()) @@ -468,26 +416,21 @@ def updatemq(repo, state, skipped, **opts): repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name)) mq.qimport(repo, (), patchname=name, git=isgit, rev=[str(state[rev])]) - else: - # Rebased and skipped - skippedpatches.add(mqrebase[rev][0]) - - # Patches were either applied and rebased and imported in - # order, applied and removed or unapplied. Discard the removed - # ones while preserving the original series order and guards. - newseries = [s for s in original_series - if mq.guard_re.split(s, 1)[0] not in skippedpatches] - mq.fullseries[:] = newseries - mq.seriesdirty = True + + # restore old series to preserve guards + mq.fullseries = original_series + mq.series_dirty = True mq.savedirty() def updatebookmarks(repo, nstate, originalbookmarks, **opts): 'Move bookmarks to their correct changesets' + current = repo._bookmarkcurrent for k, v in originalbookmarks.iteritems(): if v in nstate: if nstate[v] != nullmerge: - # update the bookmarks for revs that have moved - repo._bookmarks[k] = nstate[v] + # reset the pointer if the bookmark was moved incorrectly + if k != current: + repo._bookmarks[k] = nstate[v] bookmarks.write(repo) @@ -503,10 +446,7 @@ def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches, f.write('%d\n' % int(keepbranches)) for d, v in state.iteritems(): oldrev = repo[d].hex() - if v != nullmerge: - newrev = repo[v].hex() - else: - newrev = v + newrev = repo[v].hex() f.write("%s:%s\n" % (oldrev, newrev)) f.close() repo.ui.debug('rebase status stored\n') @@ -539,10 +479,7 @@ def restorestatus(repo): keepbranches = bool(int(l)) else: oldrev, newrev = l.split(':') - if newrev != str(nullmerge): - state[repo[oldrev].rev()] = repo[newrev].rev() - else: - state[repo[oldrev].rev()] = int(newrev) + state[repo[oldrev].rev()] = repo[newrev].rev() skipped = set() # recompute the set of skipped revs if not collapse: @@ -562,19 +499,9 @@ def restorestatus(repo): def abort(repo, originalwd, target, state): 'Restore the repository to its original state' - dstates = [s for s in state.values() if s != nullrev] - immutable = [d for d in dstates if not repo[d].mutable()] - if immutable: - raise util.Abort(_("can't abort rebase due to immutable changesets %s") - % ', '.join(str(repo[r]) for r in immutable), - hint=_('see hg help phases for details')) - - descendants = set() - if dstates: - descendants = set(repo.changelog.descendants(dstates)) - if descendants - set(dstates): + if set(repo.changelog.descendants(target)) - set(state.values()): repo.ui.warn(_("warning: new changesets detected on target branch, " - "can't abort\n")) + "can't abort\n")) return -1 else: # Strip from the first rebased revision @@ -588,81 +515,68 @@ def abort(repo, originalwd, target, state): repo.ui.warn(_('rebase aborted\n')) return 0 -def buildstate(repo, dest, rebaseset, collapse): - '''Define which revisions are going to be rebased and where +def buildstate(repo, dest, src, base, detach): + 'Define which revisions are going to be rebased and where' + targetancestors = set() + detachset = set() - repo: repo - dest: context - rebaseset: set of rev - ''' + if not dest: + # Destination defaults to the latest revision in the current branch + branch = repo[None].branch() + dest = repo[branch].rev() + else: + dest = repo[dest].rev() # This check isn't strictly necessary, since mq detects commits over an # applied patch. But it prevents messing up the working directory when # a partially completed rebase is blocked by mq. - if 'qtip' in repo.tags() and (dest.node() in + if 'qtip' in repo.tags() and (repo[dest].node() in [s.node for s in repo.mq.applied]): raise util.Abort(_('cannot rebase onto an applied mq patch')) - 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') + if src: + commonbase = repo[src].ancestor(repo[dest]) + samebranch = repo[src].branch() == repo[dest].branch() + if commonbase == repo[src]: + raise util.Abort(_('source is ancestor of destination')) + if samebranch and commonbase == repo[dest]: + raise util.Abort(_('source is descendant of destination')) + source = repo[src].rev() + if detach: + # We need to keep track of source's ancestors up to the common base + srcancestors = set(repo.changelog.ancestors(source)) + baseancestors = set(repo.changelog.ancestors(commonbase.rev())) + detachset = srcancestors - baseancestors + detachset.discard(commonbase.rev()) + else: + if base: + cwd = repo[base].rev() + else: + cwd = repo['.'].rev() + + if cwd == dest: + repo.ui.debug('source and destination are the same\n') + return None + + targetancestors = set(repo.changelog.ancestors(dest)) + if cwd in targetancestors: + repo.ui.debug('source is ancestor of destination\n') + return None + + cwdancestors = set(repo.changelog.ancestors(cwd)) + if dest in cwdancestors: + repo.ui.debug('source is descendant 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: - # (strict) ancestors of <root> not ancestors of <dest> - detachset = repo.revs('::%d - ::%d - %d', root, commonbase, root) - state.update(dict.fromkeys(detachset, nullmerge)) - return repo['.'].rev(), dest.rev(), state + cwdancestors.add(cwd) + rebasingbranch = cwdancestors - targetancestors + source = min(rebasingbranch) + + repo.ui.debug('rebase onto %d starting from %d\n' % (dest, source)) + state = dict.fromkeys(repo.changelog.descendants(source), nullrev) + state.update(dict.fromkeys(detachset, nullmerge)) + state[source] = nullrev + return repo['.'].rev(), repo[dest].rev(), state def pullrebase(orig, ui, repo, *args, **opts): 'Call rebase after pull if the latter has been invoked with --rebase' @@ -672,7 +586,6 @@ def pullrebase(orig, ui, repo, *args, **opts): ui.debug('--update and --rebase are not compatible, ignoring ' 'the update flag\n') - movemarkfrom = repo['.'].node() cmdutil.bailifchanged(repo) revsprepull = len(repo) origpostincoming = commands.postincoming @@ -691,9 +604,6 @@ def pullrebase(orig, ui, repo, *args, **opts): if dest != repo['.'].rev(): # there was nothing to rebase we force an update hg.update(repo, dest) - if bookmarks.update(repo, [movemarkfrom], repo['.'].node()): - ui.status(_("updating bookmark %s\n") - % repo._bookmarkcurrent) else: if opts.get('tool'): raise util.Abort(_('--tool can only be used with --rebase')) |