rebase.py
469 lines
| 17.6 KiB
| text/x-python
|
PythonLexer
/ hgext / rebase.py
|
r6906 | # rebase.py - rebasing feature for mercurial | ||
# | ||||
# Copyright 2008 Stefano Tortarolo <stefano.tortarolo at gmail dot com> | ||||
# | ||||
|
r8225 | # This software may be used and distributed according to the terms of the | ||
# GNU General Public License version 2, incorporated herein by reference. | ||||
|
r6906 | |||
|
r7127 | '''move sets of revisions to a different ancestor | ||
|
r6906 | |||
|
r7999 | This extension lets you rebase changesets in an existing Mercurial | ||
repository. | ||||
|
r6906 | |||
For more information: | ||||
http://www.selenic.com/mercurial/wiki/index.cgi/RebaseProject | ||||
''' | ||||
|
r7636 | from mercurial import util, repair, merge, cmdutil, commands, error | ||
|
r7955 | from mercurial import extensions, ancestor, copies, patch | ||
|
r6906 | from mercurial.commands import templateopts | ||
from mercurial.node import nullrev | ||||
|
r8112 | from mercurial.lock import release | ||
|
r6906 | from mercurial.i18n import _ | ||
import os, errno | ||||
|
r7278 | def rebasemerge(repo, rev, first=False): | ||
'return the correct ancestor' | ||||
oldancestor = ancestor.ancestor | ||||
|
r7298 | |||
|
r7278 | def newancestor(a, b, pfunc): | ||
ancestor.ancestor = oldancestor | ||||
if b == rev: | ||||
return repo[rev].parents()[0].rev() | ||||
return ancestor.ancestor(a, b, pfunc) | ||||
if not first: | ||||
ancestor.ancestor = newancestor | ||||
else: | ||||
|
r7599 | repo.ui.debug(_("first revision, do not change ancestor\n")) | ||
|
r7278 | stats = merge.update(repo, rev, True, True, False) | ||
return stats | ||||
|
r6906 | def rebase(ui, repo, **opts): | ||
"""move changeset (and descendants) to a different branch | ||||
|
r7999 | Rebase uses repeated merging to graft changesets from one part of | ||
history onto another. This can be useful for linearizing local | ||||
changes relative to a master development tree. | ||||
|
r6906 | |||
|
r7999 | If a rebase is interrupted to manually resolve a merge, it can be | ||
|
r8076 | continued with --continue/-c or aborted with --abort/-a. | ||
|
r6906 | """ | ||
|
r7280 | originalwd = target = None | ||
|
r6906 | external = nullrev | ||
state = skipped = {} | ||||
lock = wlock = None | ||||
try: | ||||
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) | ||||
contf = opts.get('continue') | ||||
abortf = opts.get('abort') | ||||
collapsef = opts.get('collapse', False) | ||||
|
r7468 | extrafn = opts.get('extrafn') | ||
|
r7952 | keepf = opts.get('keep', False) | ||
keepbranchesf = opts.get('keepbranches', False) | ||||
|
r7468 | |||
|
r6906 | if contf or abortf: | ||
if contf and abortf: | ||||
|
r7636 | raise error.ParseError('rebase', | ||
_('cannot use both abort and continue')) | ||||
|
r6906 | if collapsef: | ||
|
r7636 | raise error.ParseError( | ||
'rebase', _('cannot use collapse with continue or abort')) | ||||
|
r6906 | |||
|
r8117 | if srcf or basef or destf: | ||
|
r7636 | raise error.ParseError('rebase', | ||
|
r6906 | _('abort and continue do not allow specifying revisions')) | ||
|
r7952 | (originalwd, target, state, collapsef, keepf, | ||
keepbranchesf, external) = restorestatus(repo) | ||||
|
r6906 | if abortf: | ||
abort(repo, originalwd, target, state) | ||||
return | ||||
else: | ||||
if srcf and basef: | ||||
|
r7636 | raise error.ParseError('rebase', _('cannot specify both a ' | ||
'revision and a base')) | ||||
|
r6906 | cmdutil.bail_if_changed(repo) | ||
result = buildstate(repo, destf, srcf, basef, collapsef) | ||||
if result: | ||||
originalwd, target, state, external = result | ||||
else: # Empty state built, nothing to rebase | ||||
repo.ui.status(_('nothing to rebase\n')) | ||||
return | ||||
|
r7952 | if keepbranchesf: | ||
if extrafn: | ||||
raise error.ParseError( | ||||
'rebase', _('cannot use both keepbranches and extrafn')) | ||||
def extrafn(ctx, extra): | ||||
extra['branch'] = ctx.branch() | ||||
|
r6906 | # Rebase | ||
targetancestors = list(repo.changelog.ancestors(target)) | ||||
targetancestors.append(target) | ||||
|
r8209 | for rev in sorted(state): | ||
|
r6906 | if state[rev] == -1: | ||
|
r7952 | storestatus(repo, originalwd, target, state, collapsef, keepf, | ||
keepbranchesf, external) | ||||
|
r6906 | rebasenode(repo, rev, target, state, skipped, targetancestors, | ||
|
r7468 | collapsef, extrafn) | ||
|
r6906 | ui.note(_('rebase merging completed\n')) | ||
if collapsef: | ||||
|
r6923 | p1, p2 = defineparents(repo, min(state), target, | ||
|
r6906 | state, targetancestors) | ||
concludenode(repo, rev, p1, external, state, collapsef, | ||||
|
r7468 | last=True, skipped=skipped, extrafn=extrafn) | ||
|
r6906 | |||
if 'qtip' in repo.tags(): | ||||
updatemq(repo, state, skipped, **opts) | ||||
|
r7952 | if not keepf: | ||
|
r6906 | # Remove no more useful revisions | ||
|
r8163 | if set(repo.changelog.descendants(min(state))) - set(state): | ||
|
r6906 | ui.warn(_("warning: new changesets detected on source branch, " | ||
"not stripping\n")) | ||||
else: | ||||
repair.strip(repo.ui, repo, repo[min(state)].node(), "strip") | ||||
clearstatus(repo) | ||||
ui.status(_("rebase completed\n")) | ||||
|
r7130 | if os.path.exists(repo.sjoin('undo')): | ||
util.unlink(repo.sjoin('undo')) | ||||
|
r6906 | if skipped: | ||
ui.note(_("%d revisions have been skipped\n") % len(skipped)) | ||||
finally: | ||||
|
r8112 | release(lock, wlock) | ||
|
r6906 | |||
|
r7468 | def concludenode(repo, rev, p1, p2, state, collapse, last=False, skipped={}, | ||
extrafn=None): | ||||
|
r6906 | """Skip commit if collapsing has been required and rev is not the last | ||
revision, commit otherwise | ||||
""" | ||||
|
r7278 | repo.ui.debug(_(" set parents\n")) | ||
if collapse and not last: | ||||
repo.dirstate.setparents(repo[p1].node()) | ||||
return None | ||||
|
r6906 | |||
|
r7278 | repo.dirstate.setparents(repo[p1].node(), repo[p2].node()) | ||
|
r6906 | |||
# Commit, record the old nodeid | ||||
m, a, r = repo.status()[:3] | ||||
newrev = nullrev | ||||
try: | ||||
if last: | ||||
commitmsg = 'Collapsed revision' | ||||
for rebased in state: | ||||
if rebased not in skipped: | ||||
commitmsg += '\n* %s' % repo[rebased].description() | ||||
commitmsg = repo.ui.edit(commitmsg, repo.ui.username()) | ||||
else: | ||||
commitmsg = repo[rev].description() | ||||
# Commit might fail if unresolved files exist | ||||
|
r7468 | extra = {'rebase_source': repo[rev].hex()} | ||
if extrafn: | ||||
extrafn(repo[rev], extra) | ||||
|
r6906 | newrev = repo.commit(m+a+r, | ||
text=commitmsg, | ||||
user=repo[rev].user(), | ||||
date=repo[rev].date(), | ||||
|
r7468 | extra=extra) | ||
|
r6906 | return newrev | ||
except util.Abort: | ||||
# Invalidate the previous setparents | ||||
repo.dirstate.invalidate() | ||||
raise | ||||
|
r7468 | def rebasenode(repo, rev, target, state, skipped, targetancestors, collapse, | ||
extrafn): | ||||
|
r6906 | 'Rebase a single revision' | ||
|
r7278 | repo.ui.debug(_("rebasing %d:%s\n") % (rev, repo[rev])) | ||
|
r6906 | |||
p1, p2 = defineparents(repo, rev, target, state, targetancestors) | ||||
|
r7298 | repo.ui.debug(_(" future parents are %d and %d\n") % (repo[p1].rev(), | ||
|
r7278 | repo[p2].rev())) | ||
|
r7298 | |||
|
r6906 | # Merge phase | ||
if len(repo.parents()) != 2: | ||||
# Update to target and merge it with local | ||||
|
r7278 | if repo['.'].rev() != repo[p1].rev(): | ||
repo.ui.debug(_(" update to %d:%s\n") % (repo[p1].rev(), repo[p1])) | ||||
merge.update(repo, p1, False, True, False) | ||||
else: | ||||
repo.ui.debug(_(" already in target\n")) | ||||
|
r6906 | repo.dirstate.write() | ||
|
r7278 | repo.ui.debug(_(" merge against %d:%s\n") % (repo[rev].rev(), repo[rev])) | ||
first = repo[rev].rev() == repo[min(state)].rev() | ||||
stats = rebasemerge(repo, rev, first) | ||||
|
r6906 | |||
if stats[3] > 0: | ||||
raise util.Abort(_('fix unresolved conflicts with hg resolve then ' | ||||
'run hg rebase --continue')) | ||||
else: # we have an interrupted rebase | ||||
repo.ui.debug(_('resuming interrupted rebase\n')) | ||||
|
r7954 | # 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[target], repo[p2], True) | ||||
m1 = repo[rev].manifest() | ||||
m2 = repo[target].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: | ||||
repo.dirstate.remove(v) | ||||
|
r6906 | |||
|
r7468 | newrev = concludenode(repo, rev, p1, p2, state, collapse, | ||
extrafn=extrafn) | ||||
|
r6906 | |||
# Update the state | ||||
if newrev is not None: | ||||
state[rev] = repo[newrev].rev() | ||||
else: | ||||
if not collapse: | ||||
|
r6964 | repo.ui.note(_('no changes, revision %d skipped\n') % rev) | ||
repo.ui.debug(_('next revision set to %s\n') % p1) | ||||
|
r6906 | skipped[rev] = True | ||
state[rev] = p1 | ||||
|
r6923 | |||
|
r6906 | def defineparents(repo, rev, target, state, targetancestors): | ||
'Return the new parent relationship of the revision that will be rebased' | ||||
parents = repo[rev].parents() | ||||
p1 = p2 = nullrev | ||||
P1n = parents[0].rev() | ||||
if P1n in targetancestors: | ||||
p1 = target | ||||
elif P1n in state: | ||||
p1 = state[P1n] | ||||
else: # P1n external | ||||
p1 = target | ||||
p2 = P1n | ||||
if len(parents) == 2 and parents[1].rev() not in targetancestors: | ||||
P2n = parents[1].rev() | ||||
# interesting second parent | ||||
if P2n in state: | ||||
if p1 == target: # P1n in targetancestors or external | ||||
p1 = state[P2n] | ||||
else: | ||||
p2 = state[P2n] | ||||
else: # P2n external | ||||
if p2 != nullrev: # P1n external too => rev is a merged revision | ||||
raise util.Abort(_('cannot use revision %d as base, result ' | ||||
'would have 3 parents') % rev) | ||||
p2 = P2n | ||||
return p1, p2 | ||||
|
r7955 | def isagitpatch(repo, patchname): | ||
'Return true if the given patch is in git format' | ||||
mqpatch = os.path.join(repo.mq.path, patchname) | ||||
for line in patch.linereader(file(mqpatch, 'rb')): | ||||
if line.startswith('diff --git'): | ||||
return True | ||||
return False | ||||
|
r6906 | def updatemq(repo, state, skipped, **opts): | ||
'Update rebased mq patches - finalize and then import them' | ||||
mqrebase = {} | ||||
for p in repo.mq.applied: | ||||
if repo[p.rev].rev() in state: | ||||
|
r6964 | repo.ui.debug(_('revision %d is an mq patch (%s), finalize it.\n') % | ||
|
r6906 | (repo[p.rev].rev(), p.name)) | ||
|
r7955 | mqrebase[repo[p.rev].rev()] = (p.name, isagitpatch(repo, p.name)) | ||
|
r6906 | |||
if mqrebase: | ||||
repo.mq.finish(repo, mqrebase.keys()) | ||||
# We must start import from the newest revision | ||||
|
r8210 | for rev in sorted(mqrebase, reverse=True): | ||
|
r6906 | if rev not in skipped: | ||
|
r6964 | repo.ui.debug(_('import mq patch %d (%s)\n') | ||
|
r7955 | % (state[rev], mqrebase[rev][0])) | ||
repo.mq.qimport(repo, (), patchname=mqrebase[rev][0], | ||||
git=mqrebase[rev][1],rev=[str(state[rev])]) | ||||
|
r6906 | repo.mq.save_dirty() | ||
|
r7952 | def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches, | ||
external): | ||||
|
r6906 | 'Store the current status to allow recovery' | ||
f = repo.opener("rebasestate", "w") | ||||
f.write(repo[originalwd].hex() + '\n') | ||||
f.write(repo[target].hex() + '\n') | ||||
f.write(repo[external].hex() + '\n') | ||||
f.write('%d\n' % int(collapse)) | ||||
|
r7952 | f.write('%d\n' % int(keep)) | ||
f.write('%d\n' % int(keepbranches)) | ||||
|
r7622 | for d, v in state.iteritems(): | ||
|
r6906 | oldrev = repo[d].hex() | ||
newrev = repo[v].hex() | ||||
f.write("%s:%s\n" % (oldrev, newrev)) | ||||
f.close() | ||||
repo.ui.debug(_('rebase status stored\n')) | ||||
def clearstatus(repo): | ||||
'Remove the status files' | ||||
if os.path.exists(repo.join("rebasestate")): | ||||
util.unlink(repo.join("rebasestate")) | ||||
def restorestatus(repo): | ||||
'Restore a previously stored status' | ||||
try: | ||||
target = None | ||||
collapse = False | ||||
external = nullrev | ||||
state = {} | ||||
f = repo.opener("rebasestate") | ||||
for i, l in enumerate(f.read().splitlines()): | ||||
if i == 0: | ||||
originalwd = repo[l].rev() | ||||
elif i == 1: | ||||
target = repo[l].rev() | ||||
elif i == 2: | ||||
external = repo[l].rev() | ||||
elif i == 3: | ||||
collapse = bool(int(l)) | ||||
|
r7952 | elif i == 4: | ||
keep = bool(int(l)) | ||||
elif i == 5: | ||||
keepbranches = bool(int(l)) | ||||
|
r6906 | else: | ||
oldrev, newrev = l.split(':') | ||||
state[repo[oldrev].rev()] = repo[newrev].rev() | ||||
repo.ui.debug(_('rebase status resumed\n')) | ||||
|
r7952 | return originalwd, target, state, collapse, keep, keepbranches, external | ||
|
r6906 | except IOError, err: | ||
if err.errno != errno.ENOENT: | ||||
raise | ||||
raise util.Abort(_('no rebase in progress')) | ||||
def abort(repo, originalwd, target, state): | ||||
'Restore the repository to its original state' | ||||
|
r8150 | if set(repo.changelog.descendants(target)) - set(state.values()): | ||
|
r6906 | repo.ui.warn(_("warning: new changesets detected on target branch, " | ||
"not stripping\n")) | ||||
else: | ||||
# Strip from the first rebased revision | ||||
merge.update(repo, repo[originalwd].rev(), False, True, False) | ||||
rebased = filter(lambda x: x > -1, state.values()) | ||||
if rebased: | ||||
strippoint = min(rebased) | ||||
repair.strip(repo.ui, repo, repo[strippoint].node(), "strip") | ||||
clearstatus(repo) | ||||
repo.ui.status(_('rebase aborted\n')) | ||||
def buildstate(repo, dest, src, base, collapse): | ||||
'Define which revisions are going to be rebased and where' | ||||
|
r8150 | targetancestors = set() | ||
|
r6906 | |||
if not dest: | ||||
|
r7877 | # Destination defaults to the latest revision in the current branch | ||
|
r6906 | branch = repo[None].branch() | ||
dest = repo[branch].rev() | ||||
else: | ||||
if 'qtip' in repo.tags() and (repo[dest].hex() in | ||||
[s.rev for s in repo.mq.applied]): | ||||
raise util.Abort(_('cannot rebase onto an applied mq patch')) | ||||
dest = repo[dest].rev() | ||||
if src: | ||||
commonbase = repo[src].ancestor(repo[dest]) | ||||
if commonbase == repo[src]: | ||||
raise util.Abort(_('cannot rebase an ancestor')) | ||||
if commonbase == repo[dest]: | ||||
raise util.Abort(_('cannot rebase a descendant')) | ||||
source = repo[src].rev() | ||||
else: | ||||
if base: | ||||
cwd = repo[base].rev() | ||||
else: | ||||
cwd = repo['.'].rev() | ||||
if cwd == dest: | ||||
repo.ui.debug(_('already working on current\n')) | ||||
return None | ||||
|
r8150 | targetancestors = set(repo.changelog.ancestors(dest)) | ||
|
r6906 | if cwd in targetancestors: | ||
repo.ui.debug(_('already working on the current branch\n')) | ||||
return None | ||||
|
r8150 | cwdancestors = set(repo.changelog.ancestors(cwd)) | ||
|
r6906 | cwdancestors.add(cwd) | ||
rebasingbranch = cwdancestors - targetancestors | ||||
source = min(rebasingbranch) | ||||
|
r6923 | |||
|
r6906 | repo.ui.debug(_('rebase onto %d starting from %d\n') % (dest, source)) | ||
state = dict.fromkeys(repo.changelog.descendants(source), nullrev) | ||||
external = nullrev | ||||
if collapse: | ||||
if not targetancestors: | ||||
|
r8150 | targetancestors = set(repo.changelog.ancestors(dest)) | ||
|
r6906 | for rev in state: | ||
# Check externals and fail if there are more than one | ||||
for p in repo[rev].parents(): | ||||
if (p.rev() not in state and p.rev() != source | ||||
and p.rev() not in targetancestors): | ||||
if external != nullrev: | ||||
raise util.Abort(_('unable to collapse, there is more ' | ||||
'than one external parent')) | ||||
external = p.rev() | ||||
state[source] = nullrev | ||||
return repo['.'].rev(), repo[dest].rev(), state, external | ||||
|
r7216 | def pullrebase(orig, ui, repo, *args, **opts): | ||
|
r6906 | 'Call rebase after pull if the latter has been invoked with --rebase' | ||
if opts.get('rebase'): | ||||
if opts.get('update'): | ||||
|
r8242 | del opts['update'] | ||
|
r7786 | ui.debug(_('--update and --rebase are not compatible, ignoring ' | ||
'the update flag\n')) | ||||
|
r6906 | |||
cmdutil.bail_if_changed(repo) | ||||
revsprepull = len(repo) | ||||
|
r7216 | orig(ui, repo, *args, **opts) | ||
|
r6906 | revspostpull = len(repo) | ||
if revspostpull > revsprepull: | ||||
|
r7216 | rebase(ui, repo, **opts) | ||
|
r7786 | branch = repo[None].branch() | ||
dest = repo[branch].rev() | ||||
if dest != repo['.'].rev(): | ||||
# there was nothing to rebase we force an update | ||||
merge.update(repo, dest, False, False, False) | ||||
|
r6906 | else: | ||
|
r7216 | orig(ui, repo, *args, **opts) | ||
|
r6906 | |||
def uisetup(ui): | ||||
'Replace pull with a decorator to provide --rebase option' | ||||
|
r7216 | entry = extensions.wrapcommand(commands.table, 'pull', pullrebase) | ||
entry[1].append(('', 'rebase', None, | ||||
_("rebase working directory to branch head")) | ||||
) | ||||
|
r6906 | |||
cmdtable = { | ||||
"rebase": | ||||
(rebase, | ||||
[ | ||||
('s', 'source', '', _('rebase from a given revision')), | ||||
('b', 'base', '', _('rebase from the base of a given revision')), | ||||
('d', 'dest', '', _('rebase onto a given revision')), | ||||
('', 'collapse', False, _('collapse the rebased revisions')), | ||||
|
r7951 | ('', 'keep', False, _('keep original revisions')), | ||
('', 'keepbranches', False, _('keep original branches')), | ||||
|
r6906 | ('c', 'continue', False, _('continue an interrupted rebase')), | ||
('a', 'abort', False, _('abort an interrupted rebase')),] + | ||||
templateopts, | ||||
|
r8031 | _('hg rebase [-s REV | -b REV] [-d REV] [--collapse] [--keep] ' | ||
|
r7951 | '[--keepbranches] | [-c] | [-a]')), | ||
|
r6906 | } | ||