summaryrefslogtreecommitdiff
path: root/hgext/rebase.py
diff options
context:
space:
mode:
Diffstat (limited to 'hgext/rebase.py')
-rw-r--r--hgext/rebase.py304
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'))