hgext/histedit.py
changeset 17066 baf8887d40e2
parent 17064 168cc52ad7c2
child 17068 a86e110430f6
equal deleted inserted replaced
17065:949e241b5573 17066:baf8887d40e2
     6 # GNU General Public License version 2 or any later version.
     6 # GNU General Public License version 2 or any later version.
     7 """Interactive history editing.
     7 """Interactive history editing.
     8 
     8 
     9 Inspired by git rebase --interactive.
     9 Inspired by git rebase --interactive.
    10 """
    10 """
    11 from inspect import getargspec
       
    12 try:
    11 try:
    13     import cPickle as pickle
    12     import cPickle as pickle
    14 except ImportError:
    13 except ImportError:
    15     import pickle
    14     import pickle
    16 import tempfile
    15 import tempfile
    23 from mercurial import hg
    22 from mercurial import hg
    24 from mercurial import node
    23 from mercurial import node
    25 from mercurial import patch
    24 from mercurial import patch
    26 from mercurial import repair
    25 from mercurial import repair
    27 from mercurial import scmutil
    26 from mercurial import scmutil
    28 from mercurial import url
       
    29 from mercurial import util
    27 from mercurial import util
    30 from mercurial.i18n import _
    28 from mercurial.i18n import _
    31 
    29 
    32 
    30 
    33 editcomment = """
    31 editcomment = """
    42 #  m, mess = edit message without changing commit content
    40 #  m, mess = edit message without changing commit content
    43 #
    41 #
    44 """
    42 """
    45 
    43 
    46 def between(repo, old, new, keep):
    44 def between(repo, old, new, keep):
    47     revs = [old, ]
    45     revs = [old]
    48     current = old
    46     current = old
    49     while current != new:
    47     while current != new:
    50         ctx = repo[current]
    48         ctx = repo[current]
    51         if not keep and len(ctx.children()) > 1:
    49         if not keep and len(ctx.children()) > 1:
    52             raise util.Abort(_('cannot edit history that would orphan nodes'))
    50             raise util.Abort(_('cannot edit history that would orphan nodes'))
    87                 ui.warn(_('%s: empty changeset')
    85                 ui.warn(_('%s: empty changeset')
    88                              % node.hex(ha))
    86                              % node.hex(ha))
    89                 return ctx, [], [], []
    87                 return ctx, [], [], []
    90         finally:
    88         finally:
    91             os.unlink(patchfile)
    89             os.unlink(patchfile)
    92     except Exception, inst:
    90     except Exception:
    93         raise util.Abort(_('Fix up the change and run '
    91         raise util.Abort(_('Fix up the change and run '
    94                            'hg histedit --continue'))
    92                            'hg histedit --continue'))
    95     n = repo.commit(text=oldctx.description(), user=oldctx.user(), date=oldctx.date(),
    93     n = repo.commit(text=oldctx.description(), user=oldctx.user(),
    96                     extra=oldctx.extra())
    94                     date=oldctx.date(), extra=oldctx.extra())
    97     return repo[n], [n, ], [oldctx.node(), ], []
    95     return repo[n], [n], [oldctx.node()], []
    98 
    96 
    99 
    97 
   100 def edit(ui, repo, ctx, ha, opts):
    98 def edit(ui, repo, ctx, ha, opts):
   101     oldctx = repo[ha]
    99     oldctx = repo[ha]
   102     hg.update(repo, ctx.node())
   100     hg.update(repo, ctx.node())
   115         files = set()
   113         files = set()
   116         try:
   114         try:
   117             patch.patch(ui, repo, patchfile, files=files, eolmode=None)
   115             patch.patch(ui, repo, patchfile, files=files, eolmode=None)
   118         finally:
   116         finally:
   119             os.unlink(patchfile)
   117             os.unlink(patchfile)
   120     except Exception, inst:
   118     except Exception:
   121         pass
   119         pass
   122     raise util.Abort(_('Make changes as needed, you may commit or record as '
   120     raise util.Abort(_('Make changes as needed, you may commit or record as '
   123                        'needed now.\nWhen you are finished, run hg'
   121                        'needed now.\nWhen you are finished, run hg'
   124                        ' histedit --continue to resume.'))
   122                        ' histedit --continue to resume.'))
   125 
   123 
   145                 ui.warn(_('%s: empty changeset')
   143                 ui.warn(_('%s: empty changeset')
   146                              % node.hex(ha))
   144                              % node.hex(ha))
   147                 return ctx, [], [], []
   145                 return ctx, [], [], []
   148         finally:
   146         finally:
   149             os.unlink(patchfile)
   147             os.unlink(patchfile)
   150     except Exception, inst:
   148     except Exception:
   151         raise util.Abort(_('Fix up the change and run '
   149         raise util.Abort(_('Fix up the change and run '
   152                            'hg histedit --continue'))
   150                            'hg histedit --continue'))
   153     n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(), date=oldctx.date(),
   151     n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
   154                     extra=oldctx.extra())
   152                     date=oldctx.date(), extra=oldctx.extra())
   155     return finishfold(ui, repo, ctx, oldctx, n, opts, [])
   153     return finishfold(ui, repo, ctx, oldctx, n, opts, [])
   156 
   154 
   157 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
   155 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
   158     parent = ctx.parents()[0].node()
   156     parent = ctx.parents()[0].node()
   159     hg.update(repo, parent)
   157     hg.update(repo, parent)
   172     try:
   170     try:
   173         patch.patch(ui, repo, patchfile, files=files, eolmode=None)
   171         patch.patch(ui, repo, patchfile, files=files, eolmode=None)
   174     finally:
   172     finally:
   175         os.unlink(patchfile)
   173         os.unlink(patchfile)
   176     newmessage = '\n***\n'.join(
   174     newmessage = '\n***\n'.join(
   177         [ctx.description(), ] +
   175         [ctx.description()] +
   178         [repo[r].description() for r in internalchanges] +
   176         [repo[r].description() for r in internalchanges] +
   179         [oldctx.description(), ])
   177         [oldctx.description()])
   180     # If the changesets are from the same author, keep it.
   178     # If the changesets are from the same author, keep it.
   181     if ctx.user() == oldctx.user():
   179     if ctx.user() == oldctx.user():
   182         username = ctx.user()
   180         username = ctx.user()
   183     else:
   181     else:
   184         username = ui.username()
   182         username = ui.username()
   185     newmessage = ui.edit(newmessage, username)
   183     newmessage = ui.edit(newmessage, username)
   186     n = repo.commit(text=newmessage, user=username, date=max(ctx.date(), oldctx.date()),
   184     n = repo.commit(text=newmessage, user=username,
   187                     extra=oldctx.extra())
   185                     date=max(ctx.date(), oldctx.date()), extra=oldctx.extra())
   188     return repo[n], [n, ], [oldctx.node(), ctx.node() ], [newnode, ]
   186     return repo[n], [n], [oldctx.node(), ctx.node()], [newnode]
   189 
   187 
   190 def drop(ui, repo, ctx, ha, opts):
   188 def drop(ui, repo, ctx, ha, opts):
   191     return ctx, [], [repo[ha].node(), ], []
   189     return ctx, [], [repo[ha].node()], []
   192 
   190 
   193 
   191 
   194 def message(ui, repo, ctx, ha, opts):
   192 def message(ui, repo, ctx, ha, opts):
   195     oldctx = repo[ha]
   193     oldctx = repo[ha]
   196     hg.update(repo, ctx.node())
   194     hg.update(repo, ctx.node())
   209         files = set()
   207         files = set()
   210         try:
   208         try:
   211             patch.patch(ui, repo, patchfile, files=files, eolmode=None)
   209             patch.patch(ui, repo, patchfile, files=files, eolmode=None)
   212         finally:
   210         finally:
   213             os.unlink(patchfile)
   211             os.unlink(patchfile)
   214     except Exception, inst:
   212     except Exception:
   215         raise util.Abort(_('Fix up the change and run '
   213         raise util.Abort(_('Fix up the change and run '
   216                            'hg histedit --continue'))
   214                            'hg histedit --continue'))
   217     message = oldctx.description()
   215     message = oldctx.description()
   218     message = ui.edit(message, ui.username())
   216     message = ui.edit(message, ui.username())
   219     new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
   217     new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
   253         raise util.Abort(_('source has mq patches applied'))
   251         raise util.Abort(_('source has mq patches applied'))
   254 
   252 
   255     parent = list(parent) + opts.get('rev', [])
   253     parent = list(parent) + opts.get('rev', [])
   256     if opts.get('outgoing'):
   254     if opts.get('outgoing'):
   257         if len(parent) > 1:
   255         if len(parent) > 1:
   258             raise util.Abort(_('only one repo argument allowed with --outgoing'))
   256             raise util.Abort(
       
   257                 _('only one repo argument allowed with --outgoing'))
   259         elif parent:
   258         elif parent:
   260             parent = parent[0]
   259             parent = parent[0]
   261 
   260 
   262         dest = ui.expandpath(parent or 'default-push', parent or 'default')
   261         dest = ui.expandpath(parent or 'default-push', parent or 'default')
   263         dest, revs = hg.parseurl(dest, None)[:2]
   262         dest, revs = hg.parseurl(dest, None)[:2]
   277 
   276 
   278     if opts.get('continue', False):
   277     if opts.get('continue', False):
   279         if len(parent) != 0:
   278         if len(parent) != 0:
   280             raise util.Abort(_('no arguments allowed with --continue'))
   279             raise util.Abort(_('no arguments allowed with --continue'))
   281         (parentctxnode, created, replaced,
   280         (parentctxnode, created, replaced,
   282          tmpnodes, existing, rules, keep, tip, replacemap ) = readstate(repo)
   281          tmpnodes, existing, rules, keep, tip, replacemap) = readstate(repo)
   283         currentparent, wantnull = repo.dirstate.parents()
   282         currentparent, wantnull = repo.dirstate.parents()
   284         parentctx = repo[parentctxnode]
   283         parentctx = repo[parentctxnode]
   285         # discover any nodes the user has added in the interim
   284         # discover any nodes the user has added in the interim
   286         newchildren = [c for c in parentctx.children()
   285         newchildren = [c for c in parentctx.children()
   287                        if c.node() not in existing]
   286                        if c.node() not in existing]
   288         action, currentnode = rules.pop(0)
   287         action, currentnode = rules.pop(0)
   289         while newchildren:
   288         while newchildren:
   290             if action in ['f', 'fold', ]:
   289             if action in ('f', 'fold'):
   291                 tmpnodes.extend([n.node() for n in newchildren])
   290                 tmpnodes.extend([n.node() for n in newchildren])
   292             else:
   291             else:
   293                 created.extend([n.node() for n in newchildren])
   292                 created.extend([n.node() for n in newchildren])
   294             newchildren = filter(lambda x: x.node() not in existing,
   293             newchildren = filter(lambda x: x.node() not in existing,
   295                                  reduce(lambda x, y: x + y,
   294                                  reduce(lambda x, y: x + y,
   298         m, a, r, d = repo.status()[:4]
   297         m, a, r, d = repo.status()[:4]
   299         oldctx = repo[currentnode]
   298         oldctx = repo[currentnode]
   300         message = oldctx.description()
   299         message = oldctx.description()
   301         if action in ('e', 'edit', 'm', 'mess'):
   300         if action in ('e', 'edit', 'm', 'mess'):
   302             message = ui.edit(message, ui.username())
   301             message = ui.edit(message, ui.username())
   303         elif action in ('f', 'fold', ):
   302         elif action in ('f', 'fold'):
   304             message = 'fold-temp-revision %s' % currentnode
   303             message = 'fold-temp-revision %s' % currentnode
   305         new = None
   304         new = None
   306         if m or a or r or d:
   305         if m or a or r or d:
   307             new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
   306             new = repo.commit(text=message, user=oldctx.user(),
   308                               extra=oldctx.extra())
   307                               date=oldctx.date(), extra=oldctx.extra())
   309 
   308 
   310         if action in ('f', 'fold'):
   309         if action in ('f', 'fold'):
   311             if new:
   310             if new:
   312                 tmpnodes.append(new)
   311                 tmpnodes.append(new)
   313             else:
   312             else:
   314                 new = newchildren[-1]
   313                 new = newchildren[-1]
   315             (parentctx, created_,
   314             (parentctx, created_, replaced_, tmpnodes_) = finishfold(
   316              replaced_, tmpnodes_, ) = finishfold(ui, repo,
   315                 ui, repo, parentctx, oldctx, new, opts, newchildren)
   317                                                   parentctx, oldctx, new,
       
   318                                                   opts, newchildren)
       
   319             replaced.extend(replaced_)
   316             replaced.extend(replaced_)
   320             created.extend(created_)
   317             created.extend(created_)
   321             tmpnodes.extend(tmpnodes_)
   318             tmpnodes.extend(tmpnodes_)
   322         elif action not in ('d', 'drop'):
   319         elif action not in ('d', 'drop'):
   323             if new != oldctx.node():
   320             if new != oldctx.node():
   336         hg.clean(repo, tip)
   333         hg.clean(repo, tip)
   337         ui.debug('should strip created nodes %s\n' %
   334         ui.debug('should strip created nodes %s\n' %
   338                  ', '.join([node.hex(n)[:12] for n in created]))
   335                  ', '.join([node.hex(n)[:12] for n in created]))
   339         ui.debug('should strip temp nodes %s\n' %
   336         ui.debug('should strip temp nodes %s\n' %
   340                  ', '.join([node.hex(n)[:12] for n in tmpnodes]))
   337                  ', '.join([node.hex(n)[:12] for n in tmpnodes]))
   341         for nodes in (created, tmpnodes, ):
   338         for nodes in (created, tmpnodes):
   342             for n in reversed(nodes):
   339             for n in reversed(nodes):
   343                 try:
   340                 try:
   344                     repair.strip(ui, repo, n)
   341                     repair.strip(ui, repo, n)
   345                 except error.LookupError:
   342                 except error.LookupError:
   346                     pass
   343                     pass
   365         ctxs = [repo[r] for r in revs]
   362         ctxs = [repo[r] for r in revs]
   366         existing = [r.node() for r in ctxs]
   363         existing = [r.node() for r in ctxs]
   367         rules = opts.get('commands', '')
   364         rules = opts.get('commands', '')
   368         if not rules:
   365         if not rules:
   369             rules = '\n'.join([makedesc(c) for c in ctxs])
   366             rules = '\n'.join([makedesc(c) for c in ctxs])
   370             rules += editcomment % (node.hex(parent)[:12], node.hex(tip)[:12], )
   367             rules += editcomment % (node.hex(parent)[:12], node.hex(tip)[:12])
   371             rules = ui.edit(rules, ui.username())
   368             rules = ui.edit(rules, ui.username())
   372             # Save edit rules in .hg/histedit-last-edit.txt in case
   369             # Save edit rules in .hg/histedit-last-edit.txt in case
   373             # the user needs to ask for help after something
   370             # the user needs to ask for help after something
   374             # surprising happens.
   371             # surprising happens.
   375             f = open(repo.join('histedit-last-edit.txt'), 'w')
   372             f = open(repo.join('histedit-last-edit.txt'), 'w')
   390         tmpnodes = []
   387         tmpnodes = []
   391         created = []
   388         created = []
   392 
   389 
   393 
   390 
   394     while rules:
   391     while rules:
   395         writestate(repo, parentctx.node(), created, replaced, tmpnodes, existing,
   392         writestate(repo, parentctx.node(), created, replaced,
   396                    rules, keep, tip, replacemap)
   393                    tmpnodes, existing, rules, keep, tip, replacemap)
   397         action, ha = rules.pop(0)
   394         action, ha = rules.pop(0)
   398         (parentctx, created_,
   395         (parentctx, created_, replaced_, tmpnodes_) = actiontable[action](
   399          replaced_, tmpnodes_, ) = actiontable[action](ui, repo,
   396             ui, repo, parentctx, ha, opts)
   400                                                        parentctx, ha,
       
   401                                                        opts)
       
   402 
   397 
   403         hexshort = lambda x: node.hex(x)[:12]
   398         hexshort = lambda x: node.hex(x)[:12]
   404 
   399 
   405         if replaced_:
   400         if replaced_:
   406             clen, rlen = len(created_), len(replaced_)
   401             clen, rlen = len(created_), len(replaced_)
   413                 assert rlen == 1, ('unexpected replacement of '
   408                 assert rlen == 1, ('unexpected replacement of '
   414                                    '%d changes with %d changes' % (rlen, clen))
   409                                    '%d changes with %d changes' % (rlen, clen))
   415                 # made more changesets than we're replacing
   410                 # made more changesets than we're replacing
   416                 # TODO synthesize patch names for created patches
   411                 # TODO synthesize patch names for created patches
   417                 replacemap[replaced_[0]] = created_[-1]
   412                 replacemap[replaced_[0]] = created_[-1]
   418                 ui.debug('histedit: created many, assuming %s replaced by %s' % (
   413                 ui.debug('histedit: created many, assuming %s replaced by %s' %
   419                     hexshort(replaced_[0]), hexshort(created_[-1])))
   414                          (hexshort(replaced_[0]), hexshort(created_[-1])))
   420             elif rlen > clen:
   415             elif rlen > clen:
   421                 if not created_:
   416                 if not created_:
   422                     # This must be a drop. Try and put our metadata on
   417                     # This must be a drop. Try and put our metadata on
   423                     # the parent change.
   418                     # the parent change.
   424                     assert rlen == 1
   419                     assert rlen == 1
   449 
   444 
   450     hg.update(repo, parentctx.node())
   445     hg.update(repo, parentctx.node())
   451 
   446 
   452     if not keep:
   447     if not keep:
   453         if replacemap:
   448         if replacemap:
   454             ui.note('histedit: Should update metadata for the following '
   449             ui.note(_('histedit: Should update metadata for the following '
   455                     'changes:\n')
   450                       'changes:\n'))
   456 
   451 
   457             def copybms(old, new):
   452             def copybms(old, new):
   458                 if old in tmpnodes or old in created:
   453                 if old in tmpnodes or old in created:
   459                     # can't have any metadata we'd want to update
   454                     # can't have any metadata we'd want to update
   460                     return
   455                     return
   461                 while new in replacemap:
   456                 while new in replacemap:
   462                     new = replacemap[new]
   457                     new = replacemap[new]
   463                 ui.note('histedit:  %s to %s\n' % (hexshort(old), hexshort(new)))
   458                 ui.note(_('histedit:  %s to %s\n') % (hexshort(old),
       
   459                                                       hexshort(new)))
   464                 octx = repo[old]
   460                 octx = repo[old]
   465                 marks = octx.bookmarks()
   461                 marks = octx.bookmarks()
   466                 if marks:
   462                 if marks:
   467                     ui.note('histedit:     moving bookmarks %s\n' %
   463                     ui.note(_('histedit:     moving bookmarks %s\n') %
   468                             ', '.join(marks))
   464                               ', '.join(marks))
   469                     for mark in marks:
   465                     for mark in marks:
   470                         repo._bookmarks[mark] = new
   466                         repo._bookmarks[mark] = new
   471                     bookmarks.write(repo)
   467                     bookmarks.write(repo)
   472 
   468 
   473             # We assume that bookmarks on the tip should remain
   469             # We assume that bookmarks on the tip should remain
   524 
   520 
   525     Will abort if there are to many or too few rules, a malformed rule,
   521     Will abort if there are to many or too few rules, a malformed rule,
   526     or a rule on a changeset outside of the user-given range.
   522     or a rule on a changeset outside of the user-given range.
   527     """
   523     """
   528     parsed = []
   524     parsed = []
   529     first = True
       
   530     if len(rules) != len(ctxs):
   525     if len(rules) != len(ctxs):
   531         raise util.Abort(_('must specify a rule for each changeset once'))
   526         raise util.Abort(_('must specify a rule for each changeset once'))
   532     for r in rules:
   527     for r in rules:
   533         if ' ' not in r:
   528         if ' ' not in r:
   534             raise util.Abort(_('malformed line "%s"') % r)
   529             raise util.Abort(_('malformed line "%s"') % r)
   537             ha, rest = rest.split(' ', 1)
   532             ha, rest = rest.split(' ', 1)
   538         else:
   533         else:
   539             ha = r.strip()
   534             ha = r.strip()
   540         try:
   535         try:
   541             if repo[ha] not in ctxs:
   536             if repo[ha] not in ctxs:
   542                 raise util.Abort(_('may not use changesets other than the ones listed'))
   537                 raise util.Abort(
       
   538                     _('may not use changesets other than the ones listed'))
   543         except error.RepoError:
   539         except error.RepoError:
   544             raise util.Abort(_('unknown changeset %s listed') % ha)
   540             raise util.Abort(_('unknown changeset %s listed') % ha)
   545         if action not in actiontable:
   541         if action not in actiontable:
   546             raise util.Abort(_('unknown action "%s"') % action)
   542             raise util.Abort(_('unknown action "%s"') % action)
   547         parsed.append([action, ha])
   543         parsed.append([action, ha])
   549 
   545 
   550 
   546 
   551 cmdtable = {
   547 cmdtable = {
   552     "histedit":
   548     "histedit":
   553         (histedit,
   549         (histedit,
   554          [('', 'commands', '', _('Read history edits from the specified file.')),
   550          [('', 'commands', '', _(
       
   551              'Read history edits from the specified file.')),
   555           ('c', 'continue', False, _('continue an edit already in progress')),
   552           ('c', 'continue', False, _('continue an edit already in progress')),
   556           ('k', 'keep', False, _("don't strip old nodes after edit is complete")),
   553           ('k', 'keep', False, _(
       
   554               "don't strip old nodes after edit is complete")),
   557           ('', 'abort', False, _('abort an edit in progress')),
   555           ('', 'abort', False, _('abort an edit in progress')),
   558           ('o', 'outgoing', False, _('changesets not found in destination')),
   556           ('o', 'outgoing', False, _('changesets not found in destination')),
   559           ('f', 'force', False, _('force outgoing even for unrelated repositories')),
   557           ('f', 'force', False, _(
       
   558               'force outgoing even for unrelated repositories')),
   560           ('r', 'rev', [], _('first revision to be edited')),
   559           ('r', 'rev', [], _('first revision to be edited')),
   561           ],
   560           ],
   562          __doc__,
   561          __doc__,
   563          ),
   562          ),
   564 }
   563 }