histedit.py
716 lines
| 26.4 KiB
| text/x-python
|
PythonLexer
/ hgext / histedit.py
Augie Fackler
|
r17064 | # histedit.py - interactive history editing for mercurial | ||
# | ||||
# Copyright 2009 Augie Fackler <raf@durin42.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
Augie Fackler
|
r17131 | """interactive history editing | ||
With this extension installed, Mercurial gains one new command: histedit. Usage | ||||
is as follows, assuming the following history:: | ||||
@ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42 | ||||
| Add delta | ||||
| | ||||
o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42 | ||||
| Add gamma | ||||
| | ||||
o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42 | ||||
| Add beta | ||||
| | ||||
o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42 | ||||
Add alpha | ||||
If you were to run ``hg histedit c561b4e977df``, you would see the following | ||||
file open in your editor:: | ||||
pick c561b4e977df Add beta | ||||
pick 030b686bedc4 Add gamma | ||||
pick 7c2fd3b9020c Add delta | ||||
# Edit history between 633536316234 and 7c2fd3b9020c | ||||
# | ||||
# Commands: | ||||
# p, pick = use commit | ||||
# e, edit = use commit, but stop for amending | ||||
Wagner Bruna
|
r17335 | # f, fold = use commit, but fold into previous commit (combines N and N-1) | ||
Augie Fackler
|
r17131 | # d, drop = remove commit from history | ||
# m, mess = edit message without changing commit content | ||||
# | ||||
In this file, lines beginning with ``#`` are ignored. You must specify a rule | ||||
for each revision in your history. For example, if you had meant to add gamma | ||||
before beta, and then wanted to add delta in the same revision as beta, you | ||||
would reorganize the file to look like this:: | ||||
pick 030b686bedc4 Add gamma | ||||
pick c561b4e977df Add beta | ||||
fold 7c2fd3b9020c Add delta | ||||
# Edit history between 633536316234 and 7c2fd3b9020c | ||||
# | ||||
# Commands: | ||||
# p, pick = use commit | ||||
# e, edit = use commit, but stop for amending | ||||
Wagner Bruna
|
r17335 | # f, fold = use commit, but fold into previous commit (combines N and N-1) | ||
Augie Fackler
|
r17131 | # d, drop = remove commit from history | ||
# m, mess = edit message without changing commit content | ||||
# | ||||
At which point you close the editor and ``histedit`` starts working. When you | ||||
specify a ``fold`` operation, ``histedit`` will open an editor when it folds | ||||
those revisions together, offering you a chance to clean up the commit message:: | ||||
Add beta | ||||
*** | ||||
Add delta | ||||
Augie Fackler
|
r17064 | |||
Augie Fackler
|
r17131 | Edit the commit message to your liking, then close the editor. For | ||
this example, let's assume that the commit message was changed to | ||||
``Add beta and delta.`` After histedit has run and had a chance to | ||||
remove any old or temporary revisions it needed, the history looks | ||||
like this:: | ||||
@ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42 | ||||
| Add beta and delta. | ||||
| | ||||
o 1 081603921c3f 2009-04-27 18:04 -0500 durin42 | ||||
| Add gamma | ||||
| | ||||
o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42 | ||||
Add alpha | ||||
Note that ``histedit`` does *not* remove any revisions (even its own temporary | ||||
ones) until after it has completed all the editing operations, so it will | ||||
probably perform several strip operations when it's done. For the above example, | ||||
it had to run strip twice. Strip can be slow depending on a variety of factors, | ||||
so you might need to be a little patient. You can choose to keep the original | ||||
revisions by passing the ``--keep`` flag. | ||||
The ``edit`` operation will drop you back to a command prompt, | ||||
allowing you to edit files freely, or even use ``hg record`` to commit | ||||
some changes as a separate commit. When you're done, any remaining | ||||
uncommitted changes will be committed as well. When done, run ``hg | ||||
histedit --continue`` to finish this step. You'll be prompted for a | ||||
new commit message, but the default commit message will be the | ||||
original message for the ``edit`` ed revision. | ||||
The ``message`` operation will give you a chance to revise a commit | ||||
message without changing the contents. It's a shortcut for doing | ||||
``edit`` immediately followed by `hg histedit --continue``. | ||||
If ``histedit`` encounters a conflict when moving a revision (while | ||||
handling ``pick`` or ``fold``), it'll stop in a similar manner to | ||||
``edit`` with the difference that it won't prompt you for a commit | ||||
message when done. If you decide at this point that you don't like how | ||||
much work it will be to rearrange history, or that you made a mistake, | ||||
you can use ``hg histedit --abort`` to abandon the new changes you | ||||
have made and return to the state before you attempted to edit your | ||||
history. | ||||
If we clone the example repository above and add three more changes, such that | ||||
we have the following history:: | ||||
@ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan | ||||
| Add theta | ||||
| | ||||
o 5 140988835471 2009-04-27 18:04 -0500 stefan | ||||
| Add eta | ||||
| | ||||
o 4 122930637314 2009-04-27 18:04 -0500 stefan | ||||
| Add zeta | ||||
| | ||||
o 3 836302820282 2009-04-27 18:04 -0500 stefan | ||||
| Add epsilon | ||||
| | ||||
o 2 989b4d060121 2009-04-27 18:04 -0500 durin42 | ||||
| Add beta and delta. | ||||
| | ||||
o 1 081603921c3f 2009-04-27 18:04 -0500 durin42 | ||||
| Add gamma | ||||
| | ||||
o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42 | ||||
Add alpha | ||||
If you run ``hg histedit --outgoing`` on the clone then it is the same | ||||
as running ``hg histedit 836302820282``. If you need plan to push to a | ||||
repository that Mercurial does not detect to be related to the source | ||||
repo, you can add a ``--force`` option. | ||||
Augie Fackler
|
r17064 | """ | ||
Augie Fackler
|
r17131 | |||
Augie Fackler
|
r17064 | try: | ||
import cPickle as pickle | ||||
except ImportError: | ||||
import pickle | ||||
import tempfile | ||||
import os | ||||
from mercurial import bookmarks | ||||
from mercurial import cmdutil | ||||
from mercurial import discovery | ||||
from mercurial import error | ||||
from mercurial import hg | ||||
Augie Fackler
|
r17326 | from mercurial import lock as lockmod | ||
Augie Fackler
|
r17064 | from mercurial import node | ||
from mercurial import patch | ||||
from mercurial import repair | ||||
from mercurial import scmutil | ||||
from mercurial import util | ||||
from mercurial.i18n import _ | ||||
Adrian Buehlmann
|
r17147 | cmdtable = {} | ||
command = cmdutil.command(cmdtable) | ||||
Augie Fackler
|
r17069 | testedwith = 'internal' | ||
Augie Fackler
|
r17064 | |||
Wagner Bruna
|
r17337 | # i18n: command names and abbreviations must remain untranslated | ||
FUJIWARA Katsunori
|
r17315 | editcomment = _("""# Edit history between %s and %s | ||
Augie Fackler
|
r17064 | # | ||
# Commands: | ||||
# p, pick = use commit | ||||
# e, edit = use commit, but stop for amending | ||||
# f, fold = use commit, but fold into previous commit (combines N and N-1) | ||||
# d, drop = remove commit from history | ||||
# m, mess = edit message without changing commit content | ||||
# | ||||
FUJIWARA Katsunori
|
r17315 | """) | ||
Augie Fackler
|
r17064 | |||
def between(repo, old, new, keep): | ||||
Augie Fackler
|
r17066 | revs = [old] | ||
Augie Fackler
|
r17064 | current = old | ||
while current != new: | ||||
ctx = repo[current] | ||||
if not keep and len(ctx.children()) > 1: | ||||
raise util.Abort(_('cannot edit history that would orphan nodes')) | ||||
if len(ctx.parents()) != 1 and ctx.parents()[1] != node.nullid: | ||||
raise util.Abort(_("can't edit history with merges")) | ||||
if not ctx.children(): | ||||
current = new | ||||
else: | ||||
current = ctx.children()[0].node() | ||||
revs.append(current) | ||||
if len(repo[current].children()) and not keep: | ||||
raise util.Abort(_('cannot edit history that would orphan nodes')) | ||||
return revs | ||||
def pick(ui, repo, ctx, ha, opts): | ||||
oldctx = repo[ha] | ||||
if oldctx.parents()[0] == ctx: | ||||
ui.debug('node %s unchanged\n' % ha) | ||||
return oldctx, [], [], [] | ||||
hg.update(repo, ctx.node()) | ||||
fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-') | ||||
fp = os.fdopen(fd, 'w') | ||||
diffopts = patch.diffopts(ui, opts) | ||||
diffopts.git = True | ||||
diffopts.ignorews = False | ||||
diffopts.ignorewsamount = False | ||||
diffopts.ignoreblanklines = False | ||||
gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts) | ||||
for chunk in gen: | ||||
fp.write(chunk) | ||||
fp.close() | ||||
try: | ||||
files = set() | ||||
try: | ||||
patch.patch(ui, repo, patchfile, files=files, eolmode=None) | ||||
if not files: | ||||
ui.warn(_('%s: empty changeset') | ||||
% node.hex(ha)) | ||||
return ctx, [], [], [] | ||||
finally: | ||||
os.unlink(patchfile) | ||||
Augie Fackler
|
r17066 | except Exception: | ||
Augie Fackler
|
r17064 | raise util.Abort(_('Fix up the change and run ' | ||
'hg histedit --continue')) | ||||
Augie Fackler
|
r17066 | n = repo.commit(text=oldctx.description(), user=oldctx.user(), | ||
date=oldctx.date(), extra=oldctx.extra()) | ||||
return repo[n], [n], [oldctx.node()], [] | ||||
Augie Fackler
|
r17064 | |||
def edit(ui, repo, ctx, ha, opts): | ||||
oldctx = repo[ha] | ||||
hg.update(repo, ctx.node()) | ||||
fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-') | ||||
fp = os.fdopen(fd, 'w') | ||||
diffopts = patch.diffopts(ui, opts) | ||||
diffopts.git = True | ||||
diffopts.ignorews = False | ||||
diffopts.ignorewsamount = False | ||||
diffopts.ignoreblanklines = False | ||||
gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts) | ||||
for chunk in gen: | ||||
fp.write(chunk) | ||||
fp.close() | ||||
try: | ||||
files = set() | ||||
try: | ||||
patch.patch(ui, repo, patchfile, files=files, eolmode=None) | ||||
finally: | ||||
os.unlink(patchfile) | ||||
Augie Fackler
|
r17066 | except Exception: | ||
Augie Fackler
|
r17064 | pass | ||
raise util.Abort(_('Make changes as needed, you may commit or record as ' | ||||
'needed now.\nWhen you are finished, run hg' | ||||
' histedit --continue to resume.')) | ||||
def fold(ui, repo, ctx, ha, opts): | ||||
oldctx = repo[ha] | ||||
hg.update(repo, ctx.node()) | ||||
fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-') | ||||
fp = os.fdopen(fd, 'w') | ||||
diffopts = patch.diffopts(ui, opts) | ||||
diffopts.git = True | ||||
diffopts.ignorews = False | ||||
diffopts.ignorewsamount = False | ||||
diffopts.ignoreblanklines = False | ||||
gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts) | ||||
for chunk in gen: | ||||
fp.write(chunk) | ||||
fp.close() | ||||
try: | ||||
files = set() | ||||
try: | ||||
patch.patch(ui, repo, patchfile, files=files, eolmode=None) | ||||
if not files: | ||||
ui.warn(_('%s: empty changeset') | ||||
% node.hex(ha)) | ||||
return ctx, [], [], [] | ||||
finally: | ||||
os.unlink(patchfile) | ||||
Augie Fackler
|
r17066 | except Exception: | ||
Augie Fackler
|
r17064 | raise util.Abort(_('Fix up the change and run ' | ||
'hg histedit --continue')) | ||||
Augie Fackler
|
r17066 | n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(), | ||
date=oldctx.date(), extra=oldctx.extra()) | ||||
Augie Fackler
|
r17064 | return finishfold(ui, repo, ctx, oldctx, n, opts, []) | ||
def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges): | ||||
parent = ctx.parents()[0].node() | ||||
hg.update(repo, parent) | ||||
fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-') | ||||
fp = os.fdopen(fd, 'w') | ||||
diffopts = patch.diffopts(ui, opts) | ||||
diffopts.git = True | ||||
diffopts.ignorews = False | ||||
diffopts.ignorewsamount = False | ||||
diffopts.ignoreblanklines = False | ||||
gen = patch.diff(repo, parent, newnode, opts=diffopts) | ||||
for chunk in gen: | ||||
fp.write(chunk) | ||||
fp.close() | ||||
files = set() | ||||
try: | ||||
patch.patch(ui, repo, patchfile, files=files, eolmode=None) | ||||
finally: | ||||
os.unlink(patchfile) | ||||
newmessage = '\n***\n'.join( | ||||
Augie Fackler
|
r17066 | [ctx.description()] + | ||
Augie Fackler
|
r17064 | [repo[r].description() for r in internalchanges] + | ||
Patrick Mezard
|
r17241 | [oldctx.description()]) + '\n' | ||
Augie Fackler
|
r17064 | # If the changesets are from the same author, keep it. | ||
if ctx.user() == oldctx.user(): | ||||
username = ctx.user() | ||||
else: | ||||
username = ui.username() | ||||
newmessage = ui.edit(newmessage, username) | ||||
Augie Fackler
|
r17066 | n = repo.commit(text=newmessage, user=username, | ||
date=max(ctx.date(), oldctx.date()), extra=oldctx.extra()) | ||||
return repo[n], [n], [oldctx.node(), ctx.node()], [newnode] | ||||
Augie Fackler
|
r17064 | |||
def drop(ui, repo, ctx, ha, opts): | ||||
Augie Fackler
|
r17066 | return ctx, [], [repo[ha].node()], [] | ||
Augie Fackler
|
r17064 | |||
def message(ui, repo, ctx, ha, opts): | ||||
oldctx = repo[ha] | ||||
hg.update(repo, ctx.node()) | ||||
fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-') | ||||
fp = os.fdopen(fd, 'w') | ||||
diffopts = patch.diffopts(ui, opts) | ||||
diffopts.git = True | ||||
diffopts.ignorews = False | ||||
diffopts.ignorewsamount = False | ||||
diffopts.ignoreblanklines = False | ||||
gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts) | ||||
for chunk in gen: | ||||
fp.write(chunk) | ||||
fp.close() | ||||
try: | ||||
files = set() | ||||
try: | ||||
patch.patch(ui, repo, patchfile, files=files, eolmode=None) | ||||
finally: | ||||
os.unlink(patchfile) | ||||
Augie Fackler
|
r17066 | except Exception: | ||
Augie Fackler
|
r17064 | raise util.Abort(_('Fix up the change and run ' | ||
'hg histedit --continue')) | ||||
Mads Kiilerich
|
r17285 | message = oldctx.description() + '\n' | ||
Augie Fackler
|
r17064 | message = ui.edit(message, ui.username()) | ||
new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(), | ||||
extra=oldctx.extra()) | ||||
newctx = repo[new] | ||||
if oldctx.node() != newctx.node(): | ||||
return newctx, [new], [oldctx.node()], [] | ||||
# We didn't make an edit, so just indicate no replaced nodes | ||||
return newctx, [new], [], [] | ||||
def makedesc(c): | ||||
summary = '' | ||||
if c.description(): | ||||
summary = c.description().splitlines()[0] | ||||
line = 'pick %s %d %s' % (c.hex()[:12], c.rev(), summary) | ||||
return line[:80] # trim to 80 chars so it's not stupidly wide in my editor | ||||
actiontable = {'p': pick, | ||||
'pick': pick, | ||||
'e': edit, | ||||
'edit': edit, | ||||
'f': fold, | ||||
'fold': fold, | ||||
'd': drop, | ||||
'drop': drop, | ||||
'm': message, | ||||
'mess': message, | ||||
} | ||||
Adrian Buehlmann
|
r17147 | |||
@command('histedit', | ||||
[('', 'commands', '', | ||||
_('Read history edits from the specified file.')), | ||||
('c', 'continue', False, _('continue an edit already in progress')), | ||||
('k', 'keep', False, | ||||
_("don't strip old nodes after edit is complete")), | ||||
('', 'abort', False, _('abort an edit in progress')), | ||||
('o', 'outgoing', False, _('changesets not found in destination')), | ||||
('f', 'force', False, | ||||
_('force outgoing even for unrelated repositories')), | ||||
('r', 'rev', [], _('first revision to be edited'))], | ||||
_("[PARENT]")) | ||||
Augie Fackler
|
r17064 | def histedit(ui, repo, *parent, **opts): | ||
Augie Fackler
|
r17131 | """interactively edit changeset history | ||
Augie Fackler
|
r17064 | """ | ||
# TODO only abort if we try and histedit mq patches, not just | ||||
# blanket if mq patches are applied somewhere | ||||
mq = getattr(repo, 'mq', None) | ||||
if mq and mq.applied: | ||||
raise util.Abort(_('source has mq patches applied')) | ||||
parent = list(parent) + opts.get('rev', []) | ||||
if opts.get('outgoing'): | ||||
if len(parent) > 1: | ||||
Augie Fackler
|
r17066 | raise util.Abort( | ||
_('only one repo argument allowed with --outgoing')) | ||||
Augie Fackler
|
r17064 | elif parent: | ||
parent = parent[0] | ||||
dest = ui.expandpath(parent or 'default-push', parent or 'default') | ||||
dest, revs = hg.parseurl(dest, None)[:2] | ||||
ui.status(_('comparing with %s\n') % util.hidepassword(dest)) | ||||
revs, checkout = hg.addbranchrevs(repo, repo, revs, None) | ||||
Sune Foldager
|
r17191 | other = hg.peer(repo, opts, dest) | ||
Augie Fackler
|
r17064 | |||
if revs: | ||||
revs = [repo.lookup(rev) for rev in revs] | ||||
parent = discovery.findcommonoutgoing( | ||||
repo, other, [], force=opts.get('force')).missing[0:1] | ||||
else: | ||||
if opts.get('force'): | ||||
raise util.Abort(_('--force only allowed with --outgoing')) | ||||
if opts.get('continue', False): | ||||
if len(parent) != 0: | ||||
raise util.Abort(_('no arguments allowed with --continue')) | ||||
(parentctxnode, created, replaced, | ||||
Augie Fackler
|
r17066 | tmpnodes, existing, rules, keep, tip, replacemap) = readstate(repo) | ||
Augie Fackler
|
r17064 | currentparent, wantnull = repo.dirstate.parents() | ||
parentctx = repo[parentctxnode] | ||||
Patrick Mezard
|
r17242 | # existing is the list of revisions initially considered by | ||
# histedit. Here we use it to list new changesets, descendants | ||||
# of parentctx without an 'existing' changeset in-between. We | ||||
# also have to exclude 'existing' changesets which were | ||||
# previously dropped. | ||||
descendants = set(c.node() for c in | ||||
repo.set('(%n::) - %n', parentctxnode, parentctxnode)) | ||||
existing = set(existing) | ||||
notdropped = set(n for n in existing if n in descendants and | ||||
(n not in replacemap or replacemap[n] in descendants)) | ||||
# Discover any nodes the user has added in the interim. We can | ||||
# miss changesets which were dropped and recreated the same. | ||||
newchildren = list(c.node() for c in repo.set( | ||||
'sort(%ln - (%ln or %ln::))', descendants, existing, notdropped)) | ||||
Augie Fackler
|
r17064 | action, currentnode = rules.pop(0) | ||
Patrick Mezard
|
r17242 | if action in ('f', 'fold'): | ||
tmpnodes.extend(newchildren) | ||||
else: | ||||
created.extend(newchildren) | ||||
Augie Fackler
|
r17064 | m, a, r, d = repo.status()[:4] | ||
oldctx = repo[currentnode] | ||||
Mads Kiilerich
|
r17285 | message = oldctx.description() + '\n' | ||
Augie Fackler
|
r17064 | if action in ('e', 'edit', 'm', 'mess'): | ||
message = ui.edit(message, ui.username()) | ||||
Augie Fackler
|
r17066 | elif action in ('f', 'fold'): | ||
Augie Fackler
|
r17064 | message = 'fold-temp-revision %s' % currentnode | ||
new = None | ||||
if m or a or r or d: | ||||
Augie Fackler
|
r17066 | new = repo.commit(text=message, user=oldctx.user(), | ||
date=oldctx.date(), extra=oldctx.extra()) | ||||
Augie Fackler
|
r17064 | |||
Augie Fackler
|
r17130 | # If we're resuming a fold and we have new changes, mark the | ||
# replacements and finish the fold. If not, it's more like a | ||||
# drop of the changesets that disappeared, and we can skip | ||||
# this step. | ||||
if action in ('f', 'fold') and (new or newchildren): | ||||
Augie Fackler
|
r17064 | if new: | ||
tmpnodes.append(new) | ||||
else: | ||||
new = newchildren[-1] | ||||
Augie Fackler
|
r17066 | (parentctx, created_, replaced_, tmpnodes_) = finishfold( | ||
ui, repo, parentctx, oldctx, new, opts, newchildren) | ||||
Augie Fackler
|
r17064 | replaced.extend(replaced_) | ||
created.extend(created_) | ||||
tmpnodes.extend(tmpnodes_) | ||||
elif action not in ('d', 'drop'): | ||||
if new != oldctx.node(): | ||||
replaced.append(oldctx.node()) | ||||
if new: | ||||
if new != oldctx.node(): | ||||
created.append(new) | ||||
parentctx = repo[new] | ||||
elif opts.get('abort', False): | ||||
if len(parent) != 0: | ||||
raise util.Abort(_('no arguments allowed with --abort')) | ||||
(parentctxnode, created, replaced, tmpnodes, | ||||
existing, rules, keep, tip, replacemap) = readstate(repo) | ||||
ui.debug('restore wc to old tip %s\n' % node.hex(tip)) | ||||
hg.clean(repo, tip) | ||||
ui.debug('should strip created nodes %s\n' % | ||||
', '.join([node.hex(n)[:12] for n in created])) | ||||
ui.debug('should strip temp nodes %s\n' % | ||||
', '.join([node.hex(n)[:12] for n in tmpnodes])) | ||||
Augie Fackler
|
r17066 | for nodes in (created, tmpnodes): | ||
Augie Fackler
|
r17326 | lock = None | ||
try: | ||||
lock = repo.lock() | ||||
for n in reversed(nodes): | ||||
try: | ||||
repair.strip(ui, repo, n) | ||||
except error.LookupError: | ||||
pass | ||||
finally: | ||||
lockmod.release(lock) | ||||
Augie Fackler
|
r17064 | os.unlink(os.path.join(repo.path, 'histedit-state')) | ||
return | ||||
else: | ||||
cmdutil.bailifchanged(repo) | ||||
if os.path.exists(os.path.join(repo.path, 'histedit-state')): | ||||
raise util.Abort(_('history edit already in progress, try ' | ||||
'--continue or --abort')) | ||||
tip, empty = repo.dirstate.parents() | ||||
if len(parent) != 1: | ||||
raise util.Abort(_('histedit requires exactly one parent revision')) | ||||
parent = scmutil.revsingle(repo, parent[0]).node() | ||||
keep = opts.get('keep', False) | ||||
revs = between(repo, parent, tip, keep) | ||||
ctxs = [repo[r] for r in revs] | ||||
existing = [r.node() for r in ctxs] | ||||
rules = opts.get('commands', '') | ||||
if not rules: | ||||
rules = '\n'.join([makedesc(c) for c in ctxs]) | ||||
FUJIWARA Katsunori
|
r17315 | rules += '\n\n' | ||
Augie Fackler
|
r17066 | rules += editcomment % (node.hex(parent)[:12], node.hex(tip)[:12]) | ||
Augie Fackler
|
r17064 | rules = ui.edit(rules, ui.username()) | ||
# Save edit rules in .hg/histedit-last-edit.txt in case | ||||
# the user needs to ask for help after something | ||||
# surprising happens. | ||||
f = open(repo.join('histedit-last-edit.txt'), 'w') | ||||
f.write(rules) | ||||
f.close() | ||||
else: | ||||
f = open(rules) | ||||
rules = f.read() | ||||
f.close() | ||||
rules = [l for l in (r.strip() for r in rules.splitlines()) | ||||
if l and not l[0] == '#'] | ||||
rules = verifyrules(rules, repo, ctxs) | ||||
parentctx = repo[parent].parents()[0] | ||||
keep = opts.get('keep', False) | ||||
replaced = [] | ||||
replacemap = {} | ||||
tmpnodes = [] | ||||
created = [] | ||||
while rules: | ||||
Augie Fackler
|
r17066 | writestate(repo, parentctx.node(), created, replaced, | ||
tmpnodes, existing, rules, keep, tip, replacemap) | ||||
Augie Fackler
|
r17064 | action, ha = rules.pop(0) | ||
Augie Fackler
|
r17066 | (parentctx, created_, replaced_, tmpnodes_) = actiontable[action]( | ||
ui, repo, parentctx, ha, opts) | ||||
Augie Fackler
|
r17064 | |||
if replaced_: | ||||
clen, rlen = len(created_), len(replaced_) | ||||
if clen == rlen == 1: | ||||
ui.debug('histedit: exact replacement of %s with %s\n' % ( | ||||
Augie Fackler
|
r17129 | node.short(replaced_[0]), node.short(created_[0]))) | ||
Augie Fackler
|
r17064 | |||
replacemap[replaced_[0]] = created_[0] | ||||
elif clen > rlen: | ||||
assert rlen == 1, ('unexpected replacement of ' | ||||
'%d changes with %d changes' % (rlen, clen)) | ||||
# made more changesets than we're replacing | ||||
# TODO synthesize patch names for created patches | ||||
replacemap[replaced_[0]] = created_[-1] | ||||
Augie Fackler
|
r17066 | ui.debug('histedit: created many, assuming %s replaced by %s' % | ||
Augie Fackler
|
r17129 | (node.short(replaced_[0]), node.short(created_[-1]))) | ||
Augie Fackler
|
r17064 | elif rlen > clen: | ||
if not created_: | ||||
# This must be a drop. Try and put our metadata on | ||||
# the parent change. | ||||
assert rlen == 1 | ||||
r = replaced_[0] | ||||
ui.debug('histedit: %s seems replaced with nothing, ' | ||||
Augie Fackler
|
r17129 | 'finding a parent\n' % (node.short(r))) | ||
Augie Fackler
|
r17064 | pctx = repo[r].parents()[0] | ||
if pctx.node() in replacemap: | ||||
ui.debug('histedit: parent is already replaced\n') | ||||
replacemap[r] = replacemap[pctx.node()] | ||||
else: | ||||
replacemap[r] = pctx.node() | ||||
ui.debug('histedit: %s best replaced by %s\n' % ( | ||||
Augie Fackler
|
r17129 | node.short(r), node.short(replacemap[r]))) | ||
Augie Fackler
|
r17064 | else: | ||
assert len(created_) == 1 | ||||
for r in replaced_: | ||||
ui.debug('histedit: %s replaced by %s\n' % ( | ||||
Augie Fackler
|
r17129 | node.short(r), node.short(created_[0]))) | ||
Augie Fackler
|
r17064 | replacemap[r] = created_[0] | ||
else: | ||||
assert False, ( | ||||
'Unhandled case in replacement mapping! ' | ||||
'replacing %d changes with %d changes' % (rlen, clen)) | ||||
created.extend(created_) | ||||
replaced.extend(replaced_) | ||||
tmpnodes.extend(tmpnodes_) | ||||
hg.update(repo, parentctx.node()) | ||||
if not keep: | ||||
if replacemap: | ||||
Augie Fackler
|
r17066 | ui.note(_('histedit: Should update metadata for the following ' | ||
'changes:\n')) | ||||
Augie Fackler
|
r17064 | |||
def copybms(old, new): | ||||
if old in tmpnodes or old in created: | ||||
# can't have any metadata we'd want to update | ||||
return | ||||
while new in replacemap: | ||||
new = replacemap[new] | ||||
Augie Fackler
|
r17129 | ui.note(_('histedit: %s to %s\n') % (node.short(old), | ||
node.short(new))) | ||||
Augie Fackler
|
r17064 | octx = repo[old] | ||
marks = octx.bookmarks() | ||||
if marks: | ||||
Augie Fackler
|
r17066 | ui.note(_('histedit: moving bookmarks %s\n') % | ||
', '.join(marks)) | ||||
Augie Fackler
|
r17064 | for mark in marks: | ||
repo._bookmarks[mark] = new | ||||
bookmarks.write(repo) | ||||
# We assume that bookmarks on the tip should remain | ||||
# tipmost, but bookmarks on non-tip changesets should go | ||||
# to their most reasonable successor. As a result, find | ||||
# the old tip and new tip and copy those bookmarks first, | ||||
# then do the rest of the bookmark copies. | ||||
oldtip = sorted(replacemap.keys(), key=repo.changelog.rev)[-1] | ||||
newtip = sorted(replacemap.values(), key=repo.changelog.rev)[-1] | ||||
copybms(oldtip, newtip) | ||||
Mads Kiilerich
|
r17084 | for old, new in sorted(replacemap.iteritems()): | ||
Augie Fackler
|
r17064 | copybms(old, new) | ||
# TODO update mq state | ||||
ui.debug('should strip replaced nodes %s\n' % | ||||
', '.join([node.hex(n)[:12] for n in replaced])) | ||||
Augie Fackler
|
r17326 | lock = None | ||
try: | ||||
lock = repo.lock() | ||||
for n in sorted(replaced, key=lambda x: repo[x].rev()): | ||||
try: | ||||
repair.strip(ui, repo, n) | ||||
except error.LookupError: | ||||
pass | ||||
finally: | ||||
lockmod.release(lock) | ||||
ui.debug('should strip temp nodes %s\n' % | ||||
', '.join([node.hex(n)[:12] for n in tmpnodes])) | ||||
lock = None | ||||
try: | ||||
lock = repo.lock() | ||||
for n in reversed(tmpnodes): | ||||
Augie Fackler
|
r17064 | try: | ||
repair.strip(ui, repo, n) | ||||
except error.LookupError: | ||||
pass | ||||
Augie Fackler
|
r17326 | finally: | ||
lockmod.release(lock) | ||||
Augie Fackler
|
r17064 | os.unlink(os.path.join(repo.path, 'histedit-state')) | ||
if os.path.exists(repo.sjoin('undo')): | ||||
os.unlink(repo.sjoin('undo')) | ||||
def writestate(repo, parentctxnode, created, replaced, | ||||
tmpnodes, existing, rules, keep, oldtip, replacemap): | ||||
fp = open(os.path.join(repo.path, 'histedit-state'), 'w') | ||||
pickle.dump((parentctxnode, created, replaced, | ||||
tmpnodes, existing, rules, keep, oldtip, replacemap), | ||||
fp) | ||||
fp.close() | ||||
def readstate(repo): | ||||
"""Returns a tuple of (parentnode, created, replaced, tmp, existing, rules, | ||||
keep, oldtip, replacemap ). | ||||
""" | ||||
fp = open(os.path.join(repo.path, 'histedit-state')) | ||||
return pickle.load(fp) | ||||
def verifyrules(rules, repo, ctxs): | ||||
"""Verify that there exists exactly one edit rule per given changeset. | ||||
Will abort if there are to many or too few rules, a malformed rule, | ||||
or a rule on a changeset outside of the user-given range. | ||||
""" | ||||
parsed = [] | ||||
if len(rules) != len(ctxs): | ||||
raise util.Abort(_('must specify a rule for each changeset once')) | ||||
for r in rules: | ||||
if ' ' not in r: | ||||
raise util.Abort(_('malformed line "%s"') % r) | ||||
action, rest = r.split(' ', 1) | ||||
if ' ' in rest.strip(): | ||||
ha, rest = rest.split(' ', 1) | ||||
else: | ||||
ha = r.strip() | ||||
try: | ||||
if repo[ha] not in ctxs: | ||||
Augie Fackler
|
r17066 | raise util.Abort( | ||
_('may not use changesets other than the ones listed')) | ||||
Augie Fackler
|
r17064 | except error.RepoError: | ||
raise util.Abort(_('unknown changeset %s listed') % ha) | ||||
if action not in actiontable: | ||||
raise util.Abort(_('unknown action "%s"') % action) | ||||
parsed.append([action, ha]) | ||||
return parsed | ||||