diff options
Diffstat (limited to 'hgext/histedit.py')
-rw-r--r-- | hgext/histedit.py | 715 |
1 files changed, 0 insertions, 715 deletions
diff --git a/hgext/histedit.py b/hgext/histedit.py deleted file mode 100644 index 88e0e93..0000000 --- a/hgext/histedit.py +++ /dev/null @@ -1,715 +0,0 @@ -# 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. -"""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 - # f, fold = use commit, but fold into previous commit - # 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 - # f, fold = use commit, but fold into previous commit - # 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 - -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. -""" - -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 -from mercurial import lock as lockmod -from mercurial import node -from mercurial import patch -from mercurial import repair -from mercurial import scmutil -from mercurial import util -from mercurial.i18n import _ - -cmdtable = {} -command = cmdutil.command(cmdtable) - -testedwith = 'internal' - -editcomment = _("""# Edit history between %s and %s -# -# 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 -# -""") - -def between(repo, old, new, keep): - revs = [old] - 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) - except Exception: - raise util.Abort(_('Fix up the change and run ' - 'hg histedit --continue')) - n = repo.commit(text=oldctx.description(), user=oldctx.user(), - date=oldctx.date(), extra=oldctx.extra()) - return repo[n], [n], [oldctx.node()], [] - - -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) - except Exception: - 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) - except Exception: - raise util.Abort(_('Fix up the change and run ' - 'hg histedit --continue')) - n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(), - date=oldctx.date(), extra=oldctx.extra()) - 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( - [ctx.description()] + - [repo[r].description() for r in internalchanges] + - [oldctx.description()]) + '\n' - # 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) - 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] - -def drop(ui, repo, ctx, ha, opts): - return ctx, [], [repo[ha].node()], [] - - -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) - except Exception: - raise util.Abort(_('Fix up the change and run ' - 'hg histedit --continue')) - message = oldctx.description() + '\n' - 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, - } - -@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]")) -def histedit(ui, repo, *parent, **opts): - """interactively edit changeset history - """ - # 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: - raise util.Abort( - _('only one repo argument allowed with --outgoing')) - 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) - other = hg.peer(repo, opts, dest) - - 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, - tmpnodes, existing, rules, keep, tip, replacemap) = readstate(repo) - currentparent, wantnull = repo.dirstate.parents() - parentctx = repo[parentctxnode] - # 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)) - action, currentnode = rules.pop(0) - if action in ('f', 'fold'): - tmpnodes.extend(newchildren) - else: - created.extend(newchildren) - - m, a, r, d = repo.status()[:4] - oldctx = repo[currentnode] - message = oldctx.description() + '\n' - if action in ('e', 'edit', 'm', 'mess'): - message = ui.edit(message, ui.username()) - elif action in ('f', 'fold'): - message = 'fold-temp-revision %s' % currentnode - new = None - if m or a or r or d: - new = repo.commit(text=message, user=oldctx.user(), - date=oldctx.date(), extra=oldctx.extra()) - - # 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): - if new: - tmpnodes.append(new) - else: - new = newchildren[-1] - (parentctx, created_, replaced_, tmpnodes_) = finishfold( - ui, repo, parentctx, oldctx, new, opts, newchildren) - 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])) - for nodes in (created, tmpnodes): - 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) - 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]) - rules += '\n\n' - rules += editcomment % (node.hex(parent)[:12], node.hex(tip)[:12]) - 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: - writestate(repo, parentctx.node(), created, replaced, - tmpnodes, existing, rules, keep, tip, replacemap) - action, ha = rules.pop(0) - (parentctx, created_, replaced_, tmpnodes_) = actiontable[action]( - ui, repo, parentctx, ha, opts) - - if replaced_: - clen, rlen = len(created_), len(replaced_) - if clen == rlen == 1: - ui.debug('histedit: exact replacement of %s with %s\n' % ( - node.short(replaced_[0]), node.short(created_[0]))) - - 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] - ui.debug('histedit: created many, assuming %s replaced by %s' % - (node.short(replaced_[0]), node.short(created_[-1]))) - 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, ' - 'finding a parent\n' % (node.short(r))) - 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' % ( - node.short(r), node.short(replacemap[r]))) - else: - assert len(created_) == 1 - for r in replaced_: - ui.debug('histedit: %s replaced by %s\n' % ( - node.short(r), node.short(created_[0]))) - 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: - ui.note(_('histedit: Should update metadata for the following ' - 'changes:\n')) - - 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] - ui.note(_('histedit: %s to %s\n') % (node.short(old), - node.short(new))) - octx = repo[old] - marks = octx.bookmarks() - if marks: - ui.note(_('histedit: moving bookmarks %s\n') % - ', '.join(marks)) - 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) - - for old, new in sorted(replacemap.iteritems()): - copybms(old, new) - # TODO update mq state - - ui.debug('should strip replaced nodes %s\n' % - ', '.join([node.hex(n)[:12] for n in replaced])) - 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): - try: - repair.strip(ui, repo, n) - except error.LookupError: - pass - finally: - lockmod.release(lock) - 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: - raise util.Abort( - _('may not use changesets other than the ones listed')) - 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 |