hgext/histedit.py
changeset 43076 2372284d9457
parent 42979 b4093d1d3b18
child 43077 687b865b95ad
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
   238 cmdtable = {}
   238 cmdtable = {}
   239 command = registrar.command(cmdtable)
   239 command = registrar.command(cmdtable)
   240 
   240 
   241 configtable = {}
   241 configtable = {}
   242 configitem = registrar.configitem(configtable)
   242 configitem = registrar.configitem(configtable)
   243 configitem('experimental', 'histedit.autoverb',
   243 configitem(
   244     default=False,
   244     'experimental', 'histedit.autoverb', default=False,
   245 )
   245 )
   246 configitem('histedit', 'defaultrev',
   246 configitem(
   247     default=None,
   247     'histedit', 'defaultrev', default=None,
   248 )
   248 )
   249 configitem('histedit', 'dropmissing',
   249 configitem(
   250     default=False,
   250     'histedit', 'dropmissing', default=False,
   251 )
   251 )
   252 configitem('histedit', 'linelen',
   252 configitem(
   253     default=80,
   253     'histedit', 'linelen', default=80,
   254 )
   254 )
   255 configitem('histedit', 'singletransaction',
   255 configitem(
   256     default=False,
   256     'histedit', 'singletransaction', default=False,
   257 )
   257 )
   258 configitem('ui', 'interface.histedit',
   258 configitem(
   259     default=None,
   259     'ui', 'interface.histedit', default=None,
   260 )
   260 )
   261 configitem('histedit', 'summary-template',
   261 configitem('histedit', 'summary-template', default='{rev} {desc|firstline}')
   262            default='{rev} {desc|firstline}')
       
   263 
   262 
   264 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
   263 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
   265 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
   264 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
   266 # be specifying the version(s) of Mercurial they are tested with, or
   265 # be specifying the version(s) of Mercurial they are tested with, or
   267 # leave the attribute unspecified.
   266 # leave the attribute unspecified.
   270 actiontable = {}
   269 actiontable = {}
   271 primaryactions = set()
   270 primaryactions = set()
   272 secondaryactions = set()
   271 secondaryactions = set()
   273 tertiaryactions = set()
   272 tertiaryactions = set()
   274 internalactions = set()
   273 internalactions = set()
       
   274 
   275 
   275 
   276 def geteditcomment(ui, first, last):
   276 def geteditcomment(ui, first, last):
   277     """ construct the editor comment
   277     """ construct the editor comment
   278     The comment includes::
   278     The comment includes::
   279      - an intro
   279      - an intro
   282      - sorted long commands
   282      - sorted long commands
   283      - additional hints
   283      - additional hints
   284 
   284 
   285     Commands are only included once.
   285     Commands are only included once.
   286     """
   286     """
   287     intro = _("""Edit history between %s and %s
   287     intro = _(
       
   288         """Edit history between %s and %s
   288 
   289 
   289 Commits are listed from least to most recent
   290 Commits are listed from least to most recent
   290 
   291 
   291 You can reorder changesets by reordering the lines
   292 You can reorder changesets by reordering the lines
   292 
   293 
   293 Commands:
   294 Commands:
   294 """)
   295 """
       
   296     )
   295     actions = []
   297     actions = []
       
   298 
   296     def addverb(v):
   299     def addverb(v):
   297         a = actiontable[v]
   300         a = actiontable[v]
   298         lines = a.message.split("\n")
   301         lines = a.message.split("\n")
   299         if len(a.verbs):
   302         if len(a.verbs):
   300             v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
   303             v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
   301         actions.append(" %s = %s" % (v, lines[0]))
   304         actions.append(" %s = %s" % (v, lines[0]))
   302         actions.extend(['  %s' for l in lines[1:]])
   305         actions.extend(['  %s' for l in lines[1:]])
   303 
   306 
   304     for v in (
   307     for v in (
   305          sorted(primaryactions) +
   308         sorted(primaryactions)
   306          sorted(secondaryactions) +
   309         + sorted(secondaryactions)
   307          sorted(tertiaryactions)
   310         + sorted(tertiaryactions)
   308         ):
   311     ):
   309         addverb(v)
   312         addverb(v)
   310     actions.append('')
   313     actions.append('')
   311 
   314 
   312     hints = []
   315     hints = []
   313     if ui.configbool('histedit', 'dropmissing'):
   316     if ui.configbool('histedit', 'dropmissing'):
   314         hints.append("Deleting a changeset from the list "
   317         hints.append(
   315                      "will DISCARD it from the edited history!")
   318             "Deleting a changeset from the list "
       
   319             "will DISCARD it from the edited history!"
       
   320         )
   316 
   321 
   317     lines = (intro % (first, last)).split('\n') + actions + hints
   322     lines = (intro % (first, last)).split('\n') + actions + hints
   318 
   323 
   319     return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
   324     return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
       
   325 
   320 
   326 
   321 class histeditstate(object):
   327 class histeditstate(object):
   322     def __init__(self, repo):
   328     def __init__(self, repo):
   323         self.repo = repo
   329         self.repo = repo
   324         self.actions = None
   330         self.actions = None
   355             data = pickle.loads(fp)
   361             data = pickle.loads(fp)
   356             parentctxnode, rules, keep, topmost, replacements = data
   362             parentctxnode, rules, keep, topmost, replacements = data
   357             backupfile = None
   363             backupfile = None
   358         rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
   364         rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
   359 
   365 
   360         return {'parentctxnode': parentctxnode, "rules": rules, "keep": keep,
   366         return {
   361                 "topmost": topmost, "replacements": replacements,
   367             'parentctxnode': parentctxnode,
   362                 "backupfile": backupfile}
   368             "rules": rules,
       
   369             "keep": keep,
       
   370             "topmost": topmost,
       
   371             "replacements": replacements,
       
   372             "backupfile": backupfile,
       
   373         }
   363 
   374 
   364     def write(self, tr=None):
   375     def write(self, tr=None):
   365         if tr:
   376         if tr:
   366             tr.addfilegenerator('histedit-state', ('histedit-state',),
   377             tr.addfilegenerator(
   367                                 self._write, location='plain')
   378                 'histedit-state',
       
   379                 ('histedit-state',),
       
   380                 self._write,
       
   381                 location='plain',
       
   382             )
   368         else:
   383         else:
   369             with self.repo.vfs("histedit-state", "w") as f:
   384             with self.repo.vfs("histedit-state", "w") as f:
   370                 self._write(f)
   385                 self._write(f)
   371 
   386 
   372     def _write(self, fp):
   387     def _write(self, fp):
   377         fp.write('%d\n' % len(self.actions))
   392         fp.write('%d\n' % len(self.actions))
   378         for action in self.actions:
   393         for action in self.actions:
   379             fp.write('%s\n' % action.tostate())
   394             fp.write('%s\n' % action.tostate())
   380         fp.write('%d\n' % len(self.replacements))
   395         fp.write('%d\n' % len(self.replacements))
   381         for replacement in self.replacements:
   396         for replacement in self.replacements:
   382             fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
   397             fp.write(
   383                 for r in replacement[1])))
   398                 '%s%s\n'
       
   399                 % (
       
   400                     node.hex(replacement[0]),
       
   401                     ''.join(node.hex(r) for r in replacement[1]),
       
   402                 )
       
   403             )
   384         backupfile = self.backupfile
   404         backupfile = self.backupfile
   385         if not backupfile:
   405         if not backupfile:
   386             backupfile = ''
   406             backupfile = ''
   387         fp.write('%s\n' % backupfile)
   407         fp.write('%s\n' % backupfile)
   388 
   408 
   389     def _load(self):
   409     def _load(self):
   390         fp = self.repo.vfs('histedit-state', 'r')
   410         fp = self.repo.vfs('histedit-state', 'r')
   391         lines = [l[:-1] for l in fp.readlines()]
   411         lines = [l[:-1] for l in fp.readlines()]
   392 
   412 
   393         index = 0
   413         index = 0
   394         lines[index] # version number
   414         lines[index]  # version number
   395         index += 1
   415         index += 1
   396 
   416 
   397         parentctxnode = node.bin(lines[index])
   417         parentctxnode = node.bin(lines[index])
   398         index += 1
   418         index += 1
   399 
   419 
   419         replacementlen = int(lines[index])
   439         replacementlen = int(lines[index])
   420         index += 1
   440         index += 1
   421         for i in pycompat.xrange(replacementlen):
   441         for i in pycompat.xrange(replacementlen):
   422             replacement = lines[index]
   442             replacement = lines[index]
   423             original = node.bin(replacement[:40])
   443             original = node.bin(replacement[:40])
   424             succ = [node.bin(replacement[i:i + 40]) for i in
   444             succ = [
   425                     range(40, len(replacement), 40)]
   445                 node.bin(replacement[i : i + 40])
       
   446                 for i in range(40, len(replacement), 40)
       
   447             ]
   426             replacements.append((original, succ))
   448             replacements.append((original, succ))
   427             index += 1
   449             index += 1
   428 
   450 
   429         backupfile = lines[index]
   451         backupfile = lines[index]
   430         index += 1
   452         index += 1
   475         self._verifynodeconstraints(prev, expected, seen)
   497         self._verifynodeconstraints(prev, expected, seen)
   476 
   498 
   477     def _verifynodeconstraints(self, prev, expected, seen):
   499     def _verifynodeconstraints(self, prev, expected, seen):
   478         # by default command need a node in the edited list
   500         # by default command need a node in the edited list
   479         if self.node not in expected:
   501         if self.node not in expected:
   480             raise error.ParseError(_('%s "%s" changeset was not a candidate')
   502             raise error.ParseError(
   481                                    % (self.verb, node.short(self.node)),
   503                 _('%s "%s" changeset was not a candidate')
   482                                    hint=_('only use listed changesets'))
   504                 % (self.verb, node.short(self.node)),
       
   505                 hint=_('only use listed changesets'),
       
   506             )
   483         # and only one command per node
   507         # and only one command per node
   484         if self.node in seen:
   508         if self.node in seen:
   485             raise error.ParseError(_('duplicated command for changeset %s') %
   509             raise error.ParseError(
   486                                    node.short(self.node))
   510                 _('duplicated command for changeset %s') % node.short(self.node)
       
   511             )
   487 
   512 
   488     def torule(self):
   513     def torule(self):
   489         """build a histedit rule line for an action
   514         """build a histedit rule line for an action
   490 
   515 
   491         by default lines are in the form:
   516         by default lines are in the form:
   492         <hash> <rev> <summary>
   517         <hash> <rev> <summary>
   493         """
   518         """
   494         ctx = self.repo[self.node]
   519         ctx = self.repo[self.node]
   495         ui = self.repo.ui
   520         ui = self.repo.ui
   496         summary = cmdutil.rendertemplate(
   521         summary = (
   497             ctx, ui.config('histedit', 'summary-template')) or ''
   522             cmdutil.rendertemplate(
       
   523                 ctx, ui.config('histedit', 'summary-template')
       
   524             )
       
   525             or ''
       
   526         )
   498         summary = summary.splitlines()[0]
   527         summary = summary.splitlines()[0]
   499         line = '%s %s %s' % (self.verb, ctx, summary)
   528         line = '%s %s %s' % (self.verb, ctx, summary)
   500         # trim to 75 columns by default so it's not stupidly wide in my editor
   529         # trim to 75 columns by default so it's not stupidly wide in my editor
   501         # (the 5 more are left for verb)
   530         # (the 5 more are left for verb)
   502         maxlen = self.repo.ui.configint('histedit', 'linelen')
   531         maxlen = self.repo.ui.configint('histedit', 'linelen')
   503         maxlen = max(maxlen, 22) # avoid truncating hash
   532         maxlen = max(maxlen, 22)  # avoid truncating hash
   504         return stringutil.ellipsis(line, maxlen)
   533         return stringutil.ellipsis(line, maxlen)
   505 
   534 
   506     def tostate(self):
   535     def tostate(self):
   507         """Print an action in format used by histedit state files
   536         """Print an action in format used by histedit state files
   508            (the first line is a verb, the remainder is the second)
   537            (the first line is a verb, the remainder is the second)
   526         repo.ui.popbuffer()
   555         repo.ui.popbuffer()
   527         stats = applychanges(repo.ui, repo, rulectx, {})
   556         stats = applychanges(repo.ui, repo, rulectx, {})
   528         repo.dirstate.setbranch(rulectx.branch())
   557         repo.dirstate.setbranch(rulectx.branch())
   529         if stats.unresolvedcount:
   558         if stats.unresolvedcount:
   530             raise error.InterventionRequired(
   559             raise error.InterventionRequired(
   531                 _('Fix up the change (%s %s)') %
   560                 _('Fix up the change (%s %s)')
   532                 (self.verb, node.short(self.node)),
   561                 % (self.verb, node.short(self.node)),
   533                 hint=_('hg histedit --continue to resume'))
   562                 hint=_('hg histedit --continue to resume'),
       
   563             )
   534 
   564 
   535     def continuedirty(self):
   565     def continuedirty(self):
   536         """Continues the action when changes have been applied to the working
   566         """Continues the action when changes have been applied to the working
   537         copy. The default behavior is to commit the dirty changes."""
   567         copy. The default behavior is to commit the dirty changes."""
   538         repo = self.repo
   568         repo = self.repo
   542         commit = commitfuncfor(repo, rulectx)
   572         commit = commitfuncfor(repo, rulectx)
   543         if repo.ui.configbool('rewrite', 'update-timestamp'):
   573         if repo.ui.configbool('rewrite', 'update-timestamp'):
   544             date = dateutil.makedate()
   574             date = dateutil.makedate()
   545         else:
   575         else:
   546             date = rulectx.date()
   576             date = rulectx.date()
   547         commit(text=rulectx.description(), user=rulectx.user(),
   577         commit(
   548                date=date, extra=rulectx.extra(), editor=editor)
   578             text=rulectx.description(),
       
   579             user=rulectx.user(),
       
   580             date=date,
       
   581             extra=rulectx.extra(),
       
   582             editor=editor,
       
   583         )
   549 
   584 
   550     def commiteditor(self):
   585     def commiteditor(self):
   551         """The editor to be used to edit the commit message."""
   586         """The editor to be used to edit the commit message."""
   552         return False
   587         return False
   553 
   588 
   555         """Continues the action when the working copy is clean. The default
   590         """Continues the action when the working copy is clean. The default
   556         behavior is to accept the current commit as the new version of the
   591         behavior is to accept the current commit as the new version of the
   557         rulectx."""
   592         rulectx."""
   558         ctx = self.repo['.']
   593         ctx = self.repo['.']
   559         if ctx.node() == self.state.parentctxnode:
   594         if ctx.node() == self.state.parentctxnode:
   560             self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
   595             self.repo.ui.warn(
   561                               node.short(self.node))
   596                 _('%s: skipping changeset (no changes)\n')
       
   597                 % node.short(self.node)
       
   598             )
   562             return ctx, [(self.node, tuple())]
   599             return ctx, [(self.node, tuple())]
   563         if ctx.node() == self.node:
   600         if ctx.node() == self.node:
   564             # Nothing changed
   601             # Nothing changed
   565             return ctx, []
   602             return ctx, []
   566         return ctx, [(self.node, (ctx.node(),))]
   603         return ctx, [(self.node, (ctx.node(),))]
   567 
   604 
       
   605 
   568 def commitfuncfor(repo, src):
   606 def commitfuncfor(repo, src):
   569     """Build a commit function for the replacement of <src>
   607     """Build a commit function for the replacement of <src>
   570 
   608 
   571     This function ensure we apply the same treatment to all changesets.
   609     This function ensure we apply the same treatment to all changesets.
   572 
   610 
   574 
   612 
   575     Note that fold has its own separated logic because its handling is a bit
   613     Note that fold has its own separated logic because its handling is a bit
   576     different and not easily factored out of the fold method.
   614     different and not easily factored out of the fold method.
   577     """
   615     """
   578     phasemin = src.phase()
   616     phasemin = src.phase()
       
   617 
   579     def commitfunc(**kwargs):
   618     def commitfunc(**kwargs):
   580         overrides = {('phases', 'new-commit'): phasemin}
   619         overrides = {('phases', 'new-commit'): phasemin}
   581         with repo.ui.configoverride(overrides, 'histedit'):
   620         with repo.ui.configoverride(overrides, 'histedit'):
   582             extra = kwargs.get(r'extra', {}).copy()
   621             extra = kwargs.get(r'extra', {}).copy()
   583             extra['histedit_source'] = src.hex()
   622             extra['histedit_source'] = src.hex()
   584             kwargs[r'extra'] = extra
   623             kwargs[r'extra'] = extra
   585             return repo.commit(**kwargs)
   624             return repo.commit(**kwargs)
       
   625 
   586     return commitfunc
   626     return commitfunc
       
   627 
   587 
   628 
   588 def applychanges(ui, repo, ctx, opts):
   629 def applychanges(ui, repo, ctx, opts):
   589     """Merge changeset from ctx (only) in the current working directory"""
   630     """Merge changeset from ctx (only) in the current working directory"""
   590     wcpar = repo.dirstate.p1()
   631     wcpar = repo.dirstate.p1()
   591     if ctx.p1().node() == wcpar:
   632     if ctx.p1().node() == wcpar:
   596         stats = mergemod.updateresult(0, 0, 0, 0)
   637         stats = mergemod.updateresult(0, 0, 0, 0)
   597         ui.popbuffer()
   638         ui.popbuffer()
   598     else:
   639     else:
   599         try:
   640         try:
   600             # ui.forcemerge is an internal variable, do not document
   641             # ui.forcemerge is an internal variable, do not document
   601             repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
   642             repo.ui.setconfig(
   602                               'histedit')
   643                 'ui', 'forcemerge', opts.get('tool', ''), 'histedit'
       
   644             )
   603             stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
   645             stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
   604         finally:
   646         finally:
   605             repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
   647             repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
   606     return stats
   648     return stats
       
   649 
   607 
   650 
   608 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
   651 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
   609     """collapse the set of revisions from first to last as new one.
   652     """collapse the set of revisions from first to last as new one.
   610 
   653 
   611     Expected commit options are:
   654     Expected commit options are:
   619     if not ctxs:
   662     if not ctxs:
   620         return None
   663         return None
   621     for c in ctxs:
   664     for c in ctxs:
   622         if not c.mutable():
   665         if not c.mutable():
   623             raise error.ParseError(
   666             raise error.ParseError(
   624                 _("cannot fold into public change %s") % node.short(c.node()))
   667                 _("cannot fold into public change %s") % node.short(c.node())
       
   668             )
   625     base = firstctx.p1()
   669     base = firstctx.p1()
   626 
   670 
   627     # commit a new version of the old changeset, including the update
   671     # commit a new version of the old changeset, including the update
   628     # collect all files which might be affected
   672     # collect all files which might be affected
   629     files = set()
   673     files = set()
   635 
   679 
   636     # prune files which were reverted by the updates
   680     # prune files which were reverted by the updates
   637     files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
   681     files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
   638     # commit version of these files as defined by head
   682     # commit version of these files as defined by head
   639     headmf = lastctx.manifest()
   683     headmf = lastctx.manifest()
       
   684 
   640     def filectxfn(repo, ctx, path):
   685     def filectxfn(repo, ctx, path):
   641         if path in headmf:
   686         if path in headmf:
   642             fctx = lastctx[path]
   687             fctx = lastctx[path]
   643             flags = fctx.flags()
   688             flags = fctx.flags()
   644             mctx = context.memfilectx(repo, ctx,
   689             mctx = context.memfilectx(
   645                                       fctx.path(), fctx.data(),
   690                 repo,
   646                                       islink='l' in flags,
   691                 ctx,
   647                                       isexec='x' in flags,
   692                 fctx.path(),
   648                                       copysource=copied.get(path))
   693                 fctx.data(),
       
   694                 islink='l' in flags,
       
   695                 isexec='x' in flags,
       
   696                 copysource=copied.get(path),
       
   697             )
   649             return mctx
   698             return mctx
   650         return None
   699         return None
   651 
   700 
   652     if commitopts.get('message'):
   701     if commitopts.get('message'):
   653         message = commitopts['message']
   702         message = commitopts['message']
   659 
   708 
   660     parents = (firstctx.p1().node(), firstctx.p2().node())
   709     parents = (firstctx.p1().node(), firstctx.p2().node())
   661     editor = None
   710     editor = None
   662     if not skipprompt:
   711     if not skipprompt:
   663         editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
   712         editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
   664     new = context.memctx(repo,
   713     new = context.memctx(
   665                          parents=parents,
   714         repo,
   666                          text=message,
   715         parents=parents,
   667                          files=files,
   716         text=message,
   668                          filectxfn=filectxfn,
   717         files=files,
   669                          user=user,
   718         filectxfn=filectxfn,
   670                          date=date,
   719         user=user,
   671                          extra=extra,
   720         date=date,
   672                          editor=editor)
   721         extra=extra,
       
   722         editor=editor,
       
   723     )
   673     return repo.commitctx(new)
   724     return repo.commitctx(new)
       
   725 
   674 
   726 
   675 def _isdirtywc(repo):
   727 def _isdirtywc(repo):
   676     return repo[None].dirty(missing=True)
   728     return repo[None].dirty(missing=True)
   677 
   729 
       
   730 
   678 def abortdirty():
   731 def abortdirty():
   679     raise error.Abort(_('working copy has pending changes'),
   732     raise error.Abort(
   680         hint=_('amend, commit, or revert them and run histedit '
   733         _('working copy has pending changes'),
   681             '--continue, or abort with histedit --abort'))
   734         hint=_(
       
   735             'amend, commit, or revert them and run histedit '
       
   736             '--continue, or abort with histedit --abort'
       
   737         ),
       
   738     )
       
   739 
   682 
   740 
   683 def action(verbs, message, priority=False, internal=False):
   741 def action(verbs, message, priority=False, internal=False):
   684     def wrap(cls):
   742     def wrap(cls):
   685         assert not priority or not internal
   743         assert not priority or not internal
   686         verb = verbs[0]
   744         verb = verbs[0]
   697         cls.verbs = verbs
   755         cls.verbs = verbs
   698         cls.message = message
   756         cls.message = message
   699         for verb in verbs:
   757         for verb in verbs:
   700             actiontable[verb] = cls
   758             actiontable[verb] = cls
   701         return cls
   759         return cls
       
   760 
   702     return wrap
   761     return wrap
   703 
   762 
   704 @action(['pick', 'p'],
   763 
   705         _('use commit'),
   764 @action(['pick', 'p'], _('use commit'), priority=True)
   706         priority=True)
       
   707 class pick(histeditaction):
   765 class pick(histeditaction):
   708     def run(self):
   766     def run(self):
   709         rulectx = self.repo[self.node]
   767         rulectx = self.repo[self.node]
   710         if rulectx.p1().node() == self.state.parentctxnode:
   768         if rulectx.p1().node() == self.state.parentctxnode:
   711             self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
   769             self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
   712             return rulectx, []
   770             return rulectx, []
   713 
   771 
   714         return super(pick, self).run()
   772         return super(pick, self).run()
   715 
   773 
   716 @action(['edit', 'e'],
   774 
   717         _('use commit, but stop for amending'),
   775 @action(['edit', 'e'], _('use commit, but stop for amending'), priority=True)
   718         priority=True)
       
   719 class edit(histeditaction):
   776 class edit(histeditaction):
   720     def run(self):
   777     def run(self):
   721         repo = self.repo
   778         repo = self.repo
   722         rulectx = repo[self.node]
   779         rulectx = repo[self.node]
   723         hg.update(repo, self.state.parentctxnode, quietempty=True)
   780         hg.update(repo, self.state.parentctxnode, quietempty=True)
   724         applychanges(repo.ui, repo, rulectx, {})
   781         applychanges(repo.ui, repo, rulectx, {})
   725         raise error.InterventionRequired(
   782         raise error.InterventionRequired(
   726             _('Editing (%s), you may commit or record as needed now.')
   783             _('Editing (%s), you may commit or record as needed now.')
   727             % node.short(self.node),
   784             % node.short(self.node),
   728             hint=_('hg histedit --continue to resume'))
   785             hint=_('hg histedit --continue to resume'),
       
   786         )
   729 
   787 
   730     def commiteditor(self):
   788     def commiteditor(self):
   731         return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
   789         return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
   732 
   790 
   733 @action(['fold', 'f'],
   791 
   734         _('use commit, but combine it with the one above'))
   792 @action(['fold', 'f'], _('use commit, but combine it with the one above'))
   735 class fold(histeditaction):
   793 class fold(histeditaction):
   736     def verify(self, prev, expected, seen):
   794     def verify(self, prev, expected, seen):
   737         """ Verifies semantic correctness of the fold rule"""
   795         """ Verifies semantic correctness of the fold rule"""
   738         super(fold, self).verify(prev, expected, seen)
   796         super(fold, self).verify(prev, expected, seen)
   739         repo = self.repo
   797         repo = self.repo
   743             return
   801             return
   744         else:
   802         else:
   745             c = repo[prev.node]
   803             c = repo[prev.node]
   746         if not c.mutable():
   804         if not c.mutable():
   747             raise error.ParseError(
   805             raise error.ParseError(
   748                 _("cannot fold into public change %s") % node.short(c.node()))
   806                 _("cannot fold into public change %s") % node.short(c.node())
   749 
   807             )
   750 
   808 
   751     def continuedirty(self):
   809     def continuedirty(self):
   752         repo = self.repo
   810         repo = self.repo
   753         rulectx = repo[self.node]
   811         rulectx = repo[self.node]
   754 
   812 
   755         commit = commitfuncfor(repo, rulectx)
   813         commit = commitfuncfor(repo, rulectx)
   756         commit(text='fold-temp-revision %s' % node.short(self.node),
   814         commit(
   757                user=rulectx.user(), date=rulectx.date(),
   815             text='fold-temp-revision %s' % node.short(self.node),
   758                extra=rulectx.extra())
   816             user=rulectx.user(),
       
   817             date=rulectx.date(),
       
   818             extra=rulectx.extra(),
       
   819         )
   759 
   820 
   760     def continueclean(self):
   821     def continueclean(self):
   761         repo = self.repo
   822         repo = self.repo
   762         ctx = repo['.']
   823         ctx = repo['.']
   763         rulectx = repo[self.node]
   824         rulectx = repo[self.node]
   764         parentctxnode = self.state.parentctxnode
   825         parentctxnode = self.state.parentctxnode
   765         if ctx.node() == parentctxnode:
   826         if ctx.node() == parentctxnode:
   766             repo.ui.warn(_('%s: empty changeset\n') %
   827             repo.ui.warn(_('%s: empty changeset\n') % node.short(self.node))
   767                               node.short(self.node))
       
   768             return ctx, [(self.node, (parentctxnode,))]
   828             return ctx, [(self.node, (parentctxnode,))]
   769 
   829 
   770         parentctx = repo[parentctxnode]
   830         parentctx = repo[parentctxnode]
   771         newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
   831         newcommits = set(
   772                                                     parentctx.rev(),
   832             c.node()
   773                                                     parentctx.rev()))
   833             for c in repo.set('(%d::. - %d)', parentctx.rev(), parentctx.rev())
       
   834         )
   774         if not newcommits:
   835         if not newcommits:
   775             repo.ui.warn(_('%s: cannot fold - working copy is not a '
   836             repo.ui.warn(
   776                            'descendant of previous commit %s\n') %
   837                 _(
   777                            (node.short(self.node), node.short(parentctxnode)))
   838                     '%s: cannot fold - working copy is not a '
       
   839                     'descendant of previous commit %s\n'
       
   840                 )
       
   841                 % (node.short(self.node), node.short(parentctxnode))
       
   842             )
   778             return ctx, [(self.node, (ctx.node(),))]
   843             return ctx, [(self.node, (ctx.node(),))]
   779 
   844 
   780         middlecommits = newcommits.copy()
   845         middlecommits = newcommits.copy()
   781         middlecommits.discard(ctx.node())
   846         middlecommits.discard(ctx.node())
   782 
   847 
   783         return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
   848         return self.finishfold(
   784                                middlecommits)
   849             repo.ui, repo, parentctx, rulectx, ctx.node(), middlecommits
       
   850         )
   785 
   851 
   786     def skipprompt(self):
   852     def skipprompt(self):
   787         """Returns true if the rule should skip the message editor.
   853         """Returns true if the rule should skip the message editor.
   788 
   854 
   789         For example, 'fold' wants to show an editor, but 'rollup'
   855         For example, 'fold' wants to show an editor, but 'rollup'
   816         commitopts['user'] = ctx.user()
   882         commitopts['user'] = ctx.user()
   817         # commit message
   883         # commit message
   818         if not self.mergedescs():
   884         if not self.mergedescs():
   819             newmessage = ctx.description()
   885             newmessage = ctx.description()
   820         else:
   886         else:
   821             newmessage = '\n***\n'.join(
   887             newmessage = (
   822                 [ctx.description()] +
   888                 '\n***\n'.join(
   823                 [repo[r].description() for r in internalchanges] +
   889                     [ctx.description()]
   824                 [oldctx.description()]) + '\n'
   890                     + [repo[r].description() for r in internalchanges]
       
   891                     + [oldctx.description()]
       
   892                 )
       
   893                 + '\n'
       
   894             )
   825         commitopts['message'] = newmessage
   895         commitopts['message'] = newmessage
   826         # date
   896         # date
   827         if self.firstdate():
   897         if self.firstdate():
   828             commitopts['date'] = ctx.date()
   898             commitopts['date'] = ctx.date()
   829         else:
   899         else:
   839         extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
   909         extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
   840         commitopts['extra'] = extra
   910         commitopts['extra'] = extra
   841         phasemin = max(ctx.phase(), oldctx.phase())
   911         phasemin = max(ctx.phase(), oldctx.phase())
   842         overrides = {('phases', 'new-commit'): phasemin}
   912         overrides = {('phases', 'new-commit'): phasemin}
   843         with repo.ui.configoverride(overrides, 'histedit'):
   913         with repo.ui.configoverride(overrides, 'histedit'):
   844             n = collapse(repo, ctx, repo[newnode], commitopts,
   914             n = collapse(
   845                          skipprompt=self.skipprompt())
   915                 repo,
       
   916                 ctx,
       
   917                 repo[newnode],
       
   918                 commitopts,
       
   919                 skipprompt=self.skipprompt(),
       
   920             )
   846         if n is None:
   921         if n is None:
   847             return ctx, []
   922             return ctx, []
   848         hg.updaterepo(repo, n, overwrite=False)
   923         hg.updaterepo(repo, n, overwrite=False)
   849         replacements = [(oldctx.node(), (newnode,)),
   924         replacements = [
   850                         (ctx.node(), (n,)),
   925             (oldctx.node(), (newnode,)),
   851                         (newnode, (n,)),
   926             (ctx.node(), (n,)),
   852                        ]
   927             (newnode, (n,)),
       
   928         ]
   853         for ich in internalchanges:
   929         for ich in internalchanges:
   854             replacements.append((ich, (n,)))
   930             replacements.append((ich, (n,)))
   855         return repo[n], replacements
   931         return repo[n], replacements
   856 
   932 
   857 @action(['base', 'b'],
   933 
   858         _('checkout changeset and apply further changesets from there'))
   934 @action(
       
   935     ['base', 'b'],
       
   936     _('checkout changeset and apply further changesets from there'),
       
   937 )
   859 class base(histeditaction):
   938 class base(histeditaction):
   860 
       
   861     def run(self):
   939     def run(self):
   862         if self.repo['.'].node() != self.node:
   940         if self.repo['.'].node() != self.node:
   863             mergemod.update(self.repo, self.node, branchmerge=False, force=True)
   941             mergemod.update(self.repo, self.node, branchmerge=False, force=True)
   864         return self.continueclean()
   942         return self.continueclean()
   865 
   943 
   874         # base can only be use with a node not in the edited set
   952         # base can only be use with a node not in the edited set
   875         if self.node in expected:
   953         if self.node in expected:
   876             msg = _('%s "%s" changeset was an edited list candidate')
   954             msg = _('%s "%s" changeset was an edited list candidate')
   877             raise error.ParseError(
   955             raise error.ParseError(
   878                 msg % (self.verb, node.short(self.node)),
   956                 msg % (self.verb, node.short(self.node)),
   879                 hint=_('base must only use unlisted changesets'))
   957                 hint=_('base must only use unlisted changesets'),
   880 
   958             )
   881 @action(['_multifold'],
   959 
   882         _(
   960 
   883     """fold subclass used for when multiple folds happen in a row
   961 @action(
       
   962     ['_multifold'],
       
   963     _(
       
   964         """fold subclass used for when multiple folds happen in a row
   884 
   965 
   885     We only want to fire the editor for the folded message once when
   966     We only want to fire the editor for the folded message once when
   886     (say) four changes are folded down into a single change. This is
   967     (say) four changes are folded down into a single change. This is
   887     similar to rollup, but we should preserve both messages so that
   968     similar to rollup, but we should preserve both messages so that
   888     when the last fold operation runs we can show the user all the
   969     when the last fold operation runs we can show the user all the
   889     commit messages in their editor.
   970     commit messages in their editor.
   890     """),
   971     """
   891         internal=True)
   972     ),
       
   973     internal=True,
       
   974 )
   892 class _multifold(fold):
   975 class _multifold(fold):
   893     def skipprompt(self):
   976     def skipprompt(self):
   894         return True
   977         return True
   895 
   978 
   896 @action(["roll", "r"],
   979 
   897         _("like fold, but discard this commit's description and date"))
   980 @action(
       
   981     ["roll", "r"],
       
   982     _("like fold, but discard this commit's description and date"),
       
   983 )
   898 class rollup(fold):
   984 class rollup(fold):
   899     def mergedescs(self):
   985     def mergedescs(self):
   900         return False
   986         return False
   901 
   987 
   902     def skipprompt(self):
   988     def skipprompt(self):
   903         return True
   989         return True
   904 
   990 
   905     def firstdate(self):
   991     def firstdate(self):
   906         return True
   992         return True
   907 
   993 
   908 @action(["drop", "d"],
   994 
   909         _('remove commit from history'))
   995 @action(["drop", "d"], _('remove commit from history'))
   910 class drop(histeditaction):
   996 class drop(histeditaction):
   911     def run(self):
   997     def run(self):
   912         parentctx = self.repo[self.state.parentctxnode]
   998         parentctx = self.repo[self.state.parentctxnode]
   913         return parentctx, [(self.node, tuple())]
   999         return parentctx, [(self.node, tuple())]
   914 
  1000 
   915 @action(["mess", "m"],
  1001 
   916         _('edit commit message without changing commit content'),
  1002 @action(
   917         priority=True)
  1003     ["mess", "m"],
       
  1004     _('edit commit message without changing commit content'),
       
  1005     priority=True,
       
  1006 )
   918 class message(histeditaction):
  1007 class message(histeditaction):
   919     def commiteditor(self):
  1008     def commiteditor(self):
   920         return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
  1009         return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
       
  1010 
   921 
  1011 
   922 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
  1012 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
   923     """utility function to find the first outgoing changeset
  1013     """utility function to find the first outgoing changeset
   924 
  1014 
   925     Used by initialization code"""
  1015     Used by initialization code"""
   943         msg = _('there are ambiguous outgoing revisions')
  1033         msg = _('there are ambiguous outgoing revisions')
   944         hint = _("see 'hg help histedit' for more detail")
  1034         hint = _("see 'hg help histedit' for more detail")
   945         raise error.Abort(msg, hint=hint)
  1035         raise error.Abort(msg, hint=hint)
   946     return repo[roots[0]].node()
  1036     return repo[roots[0]].node()
   947 
  1037 
       
  1038 
   948 # Curses Support
  1039 # Curses Support
   949 try:
  1040 try:
   950     import curses
  1041     import curses
   951 except ImportError:
  1042 except ImportError:
   952     curses = None
  1043     curses = None
   955 ACTION_LABELS = {
  1046 ACTION_LABELS = {
   956     'fold': '^fold',
  1047     'fold': '^fold',
   957     'roll': '^roll',
  1048     'roll': '^roll',
   958 }
  1049 }
   959 
  1050 
   960 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT  = 1, 2, 3, 4, 5
  1051 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT = 1, 2, 3, 4, 5
   961 COLOR_DIFF_ADD_LINE, COLOR_DIFF_DEL_LINE, COLOR_DIFF_OFFSET = 6, 7, 8
  1052 COLOR_DIFF_ADD_LINE, COLOR_DIFF_DEL_LINE, COLOR_DIFF_OFFSET = 6, 7, 8
   962 
  1053 
   963 E_QUIT, E_HISTEDIT = 1, 2
  1054 E_QUIT, E_HISTEDIT = 1, 2
   964 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
  1055 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
   965 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
  1056 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
   966 
  1057 
   967 KEYTABLE = {
  1058 KEYTABLE = {
   968     'global': {
  1059     'global': {
   969         'h':         'next-action',
  1060         'h': 'next-action',
   970         'KEY_RIGHT': 'next-action',
  1061         'KEY_RIGHT': 'next-action',
   971         'l':         'prev-action',
  1062         'l': 'prev-action',
   972         'KEY_LEFT':  'prev-action',
  1063         'KEY_LEFT': 'prev-action',
   973         'q':         'quit',
  1064         'q': 'quit',
   974         'c':         'histedit',
  1065         'c': 'histedit',
   975         'C':         'histedit',
  1066         'C': 'histedit',
   976         'v':         'showpatch',
  1067         'v': 'showpatch',
   977         '?':         'help',
  1068         '?': 'help',
   978     },
  1069     },
   979     MODE_RULES: {
  1070     MODE_RULES: {
   980         'd':         'action-drop',
  1071         'd': 'action-drop',
   981         'e':         'action-edit',
  1072         'e': 'action-edit',
   982         'f':         'action-fold',
  1073         'f': 'action-fold',
   983         'm':         'action-mess',
  1074         'm': 'action-mess',
   984         'p':         'action-pick',
  1075         'p': 'action-pick',
   985         'r':         'action-roll',
  1076         'r': 'action-roll',
   986         ' ':         'select',
  1077         ' ': 'select',
   987         'j':         'down',
  1078         'j': 'down',
   988         'k':         'up',
  1079         'k': 'up',
   989         'KEY_DOWN':  'down',
  1080         'KEY_DOWN': 'down',
   990         'KEY_UP':    'up',
  1081         'KEY_UP': 'up',
   991         'J':         'move-down',
  1082         'J': 'move-down',
   992         'K':         'move-up',
  1083         'K': 'move-up',
   993         'KEY_NPAGE': 'move-down',
  1084         'KEY_NPAGE': 'move-down',
   994         'KEY_PPAGE': 'move-up',
  1085         'KEY_PPAGE': 'move-up',
   995         '0':         'goto',  # Used for 0..9
  1086         '0': 'goto',  # Used for 0..9
   996     },
  1087     },
   997     MODE_PATCH: {
  1088     MODE_PATCH: {
   998         ' ':         'page-down',
  1089         ' ': 'page-down',
   999         'KEY_NPAGE': 'page-down',
  1090         'KEY_NPAGE': 'page-down',
  1000         'KEY_PPAGE': 'page-up',
  1091         'KEY_PPAGE': 'page-up',
  1001         'j':         'line-down',
  1092         'j': 'line-down',
  1002         'k':         'line-up',
  1093         'k': 'line-up',
  1003         'KEY_DOWN':  'line-down',
  1094         'KEY_DOWN': 'line-down',
  1004         'KEY_UP':    'line-up',
  1095         'KEY_UP': 'line-up',
  1005         'J':         'down',
  1096         'J': 'down',
  1006         'K':         'up',
  1097         'K': 'up',
  1007     },
  1098     },
  1008     MODE_HELP: {
  1099     MODE_HELP: {},
  1009     },
       
  1010 }
  1100 }
       
  1101 
  1011 
  1102 
  1012 def screen_size():
  1103 def screen_size():
  1013     return struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, '    '))
  1104     return struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, '    '))
       
  1105 
  1014 
  1106 
  1015 class histeditrule(object):
  1107 class histeditrule(object):
  1016     def __init__(self, ctx, pos, action='pick'):
  1108     def __init__(self, ctx, pos, action='pick'):
  1017         self.ctx = ctx
  1109         self.ctx = ctx
  1018         self.action = action
  1110         self.action = action
  1037         r = self.ctx.rev()
  1129         r = self.ctx.rev()
  1038         desc = self.ctx.description().splitlines()[0].strip()
  1130         desc = self.ctx.description().splitlines()[0].strip()
  1039         if self.action == 'roll':
  1131         if self.action == 'roll':
  1040             desc = ''
  1132             desc = ''
  1041         return "#{0:<2} {1:<6} {2}:{3}   {4}".format(
  1133         return "#{0:<2} {1:<6} {2}:{3}   {4}".format(
  1042                 self.origpos, action, r, h, desc)
  1134             self.origpos, action, r, h, desc
       
  1135         )
  1043 
  1136 
  1044     def checkconflicts(self, other):
  1137     def checkconflicts(self, other):
  1045         if other.pos > self.pos and other.origpos <= self.origpos:
  1138         if other.pos > self.pos and other.origpos <= self.origpos:
  1046             if set(other.ctx.files()) & set(self.ctx.files()) != set():
  1139             if set(other.ctx.files()) & set(self.ctx.files()) != set():
  1047                 self.conflicts.append(other)
  1140                 self.conflicts.append(other)
  1048                 return self.conflicts
  1141                 return self.conflicts
  1049 
  1142 
  1050         if other in self.conflicts:
  1143         if other in self.conflicts:
  1051             self.conflicts.remove(other)
  1144             self.conflicts.remove(other)
  1052         return self.conflicts
  1145         return self.conflicts
       
  1146 
  1053 
  1147 
  1054 # ============ EVENTS ===============
  1148 # ============ EVENTS ===============
  1055 def movecursor(state, oldpos, newpos):
  1149 def movecursor(state, oldpos, newpos):
  1056     '''Change the rule/changeset that the cursor is pointing to, regardless of
  1150     '''Change the rule/changeset that the cursor is pointing to, regardless of
  1057     current mode (you can switch between patches from the view patch window).'''
  1151     current mode (you can switch between patches from the view patch window).'''
  1069             modestate['line_offset'] = newpos - state['page_height'] + 1
  1163             modestate['line_offset'] = newpos - state['page_height'] + 1
  1070 
  1164 
  1071     # Reset the patch view region to the top of the new patch.
  1165     # Reset the patch view region to the top of the new patch.
  1072     state['modes'][MODE_PATCH]['line_offset'] = 0
  1166     state['modes'][MODE_PATCH]['line_offset'] = 0
  1073 
  1167 
       
  1168 
  1074 def changemode(state, mode):
  1169 def changemode(state, mode):
  1075     curmode, _ = state['mode']
  1170     curmode, _ = state['mode']
  1076     state['mode'] = (mode, curmode)
  1171     state['mode'] = (mode, curmode)
  1077     if mode == MODE_PATCH:
  1172     if mode == MODE_PATCH:
  1078         state['modes'][MODE_PATCH]['patchcontents'] = patchcontents(state)
  1173         state['modes'][MODE_PATCH]['patchcontents'] = patchcontents(state)
  1079 
  1174 
       
  1175 
  1080 def makeselection(state, pos):
  1176 def makeselection(state, pos):
  1081     state['selected'] = pos
  1177     state['selected'] = pos
       
  1178 
  1082 
  1179 
  1083 def swap(state, oldpos, newpos):
  1180 def swap(state, oldpos, newpos):
  1084     """Swap two positions and calculate necessary conflicts in
  1181     """Swap two positions and calculate necessary conflicts in
  1085     O(|newpos-oldpos|) time"""
  1182     O(|newpos-oldpos|) time"""
  1086 
  1183 
  1100         rules[oldpos].checkconflicts(rules[r])
  1197         rules[oldpos].checkconflicts(rules[r])
  1101 
  1198 
  1102     if state['selected']:
  1199     if state['selected']:
  1103         makeselection(state, newpos)
  1200         makeselection(state, newpos)
  1104 
  1201 
       
  1202 
  1105 def changeaction(state, pos, action):
  1203 def changeaction(state, pos, action):
  1106     """Change the action state on the given position to the new action"""
  1204     """Change the action state on the given position to the new action"""
  1107     rules = state['rules']
  1205     rules = state['rules']
  1108     assert 0 <= pos < len(rules)
  1206     assert 0 <= pos < len(rules)
  1109     rules[pos].action = action
  1207     rules[pos].action = action
       
  1208 
  1110 
  1209 
  1111 def cycleaction(state, pos, next=False):
  1210 def cycleaction(state, pos, next=False):
  1112     """Changes the action state the next or the previous action from
  1211     """Changes the action state the next or the previous action from
  1113     the action list"""
  1212     the action list"""
  1114     rules = state['rules']
  1213     rules = state['rules']
  1121     if next:
  1220     if next:
  1122         index += 1
  1221         index += 1
  1123     else:
  1222     else:
  1124         index -= 1
  1223         index -= 1
  1125     changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
  1224     changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
       
  1225 
  1126 
  1226 
  1127 def changeview(state, delta, unit):
  1227 def changeview(state, delta, unit):
  1128     '''Change the region of whatever is being viewed (a patch or the list of
  1228     '''Change the region of whatever is being viewed (a patch or the list of
  1129     changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.'''
  1229     changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.'''
  1130     mode, _ = state['mode']
  1230     mode, _ = state['mode']
  1136     unit = page_height if unit == 'page' else 1
  1236     unit = page_height if unit == 'page' else 1
  1137     num_pages = 1 + (num_lines - 1) / page_height
  1237     num_pages = 1 + (num_lines - 1) / page_height
  1138     max_offset = (num_pages - 1) * page_height
  1238     max_offset = (num_pages - 1) * page_height
  1139     newline = mode_state['line_offset'] + delta * unit
  1239     newline = mode_state['line_offset'] + delta * unit
  1140     mode_state['line_offset'] = max(0, min(max_offset, newline))
  1240     mode_state['line_offset'] = max(0, min(max_offset, newline))
       
  1241 
  1141 
  1242 
  1142 def event(state, ch):
  1243 def event(state, ch):
  1143     """Change state based on the current character input
  1244     """Change state based on the current character input
  1144 
  1245 
  1145     This takes the current state and based on the current character input from
  1246     This takes the current state and based on the current character input from
  1199     elif action == 'line-down':
  1300     elif action == 'line-down':
  1200         return E_LINEDOWN
  1301         return E_LINEDOWN
  1201     elif action == 'line-up':
  1302     elif action == 'line-up':
  1202         return E_LINEUP
  1303         return E_LINEUP
  1203 
  1304 
       
  1305 
  1204 def makecommands(rules):
  1306 def makecommands(rules):
  1205     """Returns a list of commands consumable by histedit --commands based on
  1307     """Returns a list of commands consumable by histedit --commands based on
  1206     our list of rules"""
  1308     our list of rules"""
  1207     commands = []
  1309     commands = []
  1208     for rules in rules:
  1310     for rules in rules:
  1209         commands.append("{0} {1}\n".format(rules.action, rules.ctx))
  1311         commands.append("{0} {1}\n".format(rules.action, rules.ctx))
  1210     return commands
  1312     return commands
       
  1313 
  1211 
  1314 
  1212 def addln(win, y, x, line, color=None):
  1315 def addln(win, y, x, line, color=None):
  1213     """Add a line to the given window left padding but 100% filled with
  1316     """Add a line to the given window left padding but 100% filled with
  1214     whitespace characters, so that the color appears on the whole line"""
  1317     whitespace characters, so that the color appears on the whole line"""
  1215     maxy, maxx = win.getmaxyx()
  1318     maxy, maxx = win.getmaxyx()
  1222     if color:
  1325     if color:
  1223         win.addstr(y, x, line, color)
  1326         win.addstr(y, x, line, color)
  1224     else:
  1327     else:
  1225         win.addstr(y, x, line)
  1328         win.addstr(y, x, line)
  1226 
  1329 
       
  1330 
  1227 def _trunc_head(line, n):
  1331 def _trunc_head(line, n):
  1228     if len(line) <= n:
  1332     if len(line) <= n:
  1229         return line
  1333         return line
  1230     return '> ' + line[-(n - 2):]
  1334     return '> ' + line[-(n - 2) :]
       
  1335 
       
  1336 
  1231 def _trunc_tail(line, n):
  1337 def _trunc_tail(line, n):
  1232     if len(line) <= n:
  1338     if len(line) <= n:
  1233         return line
  1339         return line
  1234     return line[:n - 2] + ' >'
  1340     return line[: n - 2] + ' >'
       
  1341 
  1235 
  1342 
  1236 def patchcontents(state):
  1343 def patchcontents(state):
  1237     repo = state['repo']
  1344     repo = state['repo']
  1238     rule = state['rules'][state['pos']]
  1345     rule = state['rules'][state['pos']]
  1239     displayer = logcmdutil.changesetdisplayer(repo.ui, repo, {
  1346     displayer = logcmdutil.changesetdisplayer(
  1240         "patch": True,  "template": "status"
  1347         repo.ui, repo, {"patch": True, "template": "status"}, buffered=True
  1241     }, buffered=True)
  1348     )
  1242     overrides = {('ui',  'verbose'): True}
  1349     overrides = {('ui', 'verbose'): True}
  1243     with repo.ui.configoverride(overrides, source='histedit'):
  1350     with repo.ui.configoverride(overrides, source='histedit'):
  1244         displayer.show(rule.ctx)
  1351         displayer.show(rule.ctx)
  1245         displayer.close()
  1352         displayer.close()
  1246     return displayer.hunk[rule.ctx.rev()].splitlines()
  1353     return displayer.hunk[rule.ctx.rev()].splitlines()
       
  1354 
  1247 
  1355 
  1248 def _chisteditmain(repo, rules, stdscr):
  1356 def _chisteditmain(repo, rules, stdscr):
  1249     try:
  1357     try:
  1250         curses.use_default_colors()
  1358         curses.use_default_colors()
  1251     except curses.error:
  1359     except curses.error:
  1360             else:
  1468             else:
  1361                 rulesscr.addstr(y, 0, " ", curses.COLOR_BLACK)
  1469                 rulesscr.addstr(y, 0, " ", curses.COLOR_BLACK)
  1362             if y + start == selected:
  1470             if y + start == selected:
  1363                 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
  1471                 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
  1364             elif y + start == pos:
  1472             elif y + start == pos:
  1365                 addln(rulesscr, y, 2, rule,
  1473                 addln(
  1366                       curses.color_pair(COLOR_CURRENT) | curses.A_BOLD)
  1474                     rulesscr,
       
  1475                     y,
       
  1476                     2,
       
  1477                     rule,
       
  1478                     curses.color_pair(COLOR_CURRENT) | curses.A_BOLD,
       
  1479                 )
  1367             else:
  1480             else:
  1368                 addln(rulesscr, y, 2, rule)
  1481                 addln(rulesscr, y, 2, rule)
  1369         rulesscr.noutrefresh()
  1482         rulesscr.noutrefresh()
  1370 
  1483 
  1371     def renderstring(win, state, output, diffcolors=False):
  1484     def renderstring(win, state, output, diffcolors=False):
  1374         for y in range(0, length):
  1487         for y in range(0, length):
  1375             line = output[y]
  1488             line = output[y]
  1376             if diffcolors:
  1489             if diffcolors:
  1377                 if line and line[0] == '+':
  1490                 if line and line[0] == '+':
  1378                     win.addstr(
  1491                     win.addstr(
  1379                         y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE))
  1492                         y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE)
       
  1493                     )
  1380                 elif line and line[0] == '-':
  1494                 elif line and line[0] == '-':
  1381                     win.addstr(
  1495                     win.addstr(
  1382                         y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE))
  1496                         y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE)
       
  1497                     )
  1383                 elif line.startswith('@@ '):
  1498                 elif line.startswith('@@ '):
  1384                     win.addstr(
  1499                     win.addstr(y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
  1385                         y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
       
  1386                 else:
  1500                 else:
  1387                     win.addstr(y, 0, line)
  1501                     win.addstr(y, 0, line)
  1388             else:
  1502             else:
  1389                 win.addstr(y, 0, line)
  1503                 win.addstr(y, 0, line)
  1390         win.noutrefresh()
  1504         win.noutrefresh()
  1413         'rules': rules,
  1527         'rules': rules,
  1414         'selected': None,
  1528         'selected': None,
  1415         'mode': (MODE_INIT, MODE_INIT),
  1529         'mode': (MODE_INIT, MODE_INIT),
  1416         'page_height': None,
  1530         'page_height': None,
  1417         'modes': {
  1531         'modes': {
  1418             MODE_RULES: {
  1532             MODE_RULES: {'line_offset': 0,},
  1419                 'line_offset': 0,
  1533             MODE_PATCH: {'line_offset': 0,},
  1420             },
       
  1421             MODE_PATCH: {
       
  1422                 'line_offset': 0,
       
  1423             }
       
  1424         },
  1534         },
  1425         'repo': repo,
  1535         'repo': repo,
  1426     }
  1536     }
  1427 
  1537 
  1428     # eventloop
  1538     # eventloop
  1486                 # done rendering
  1596                 # done rendering
  1487                 ch = stdscr.getkey()
  1597                 ch = stdscr.getkey()
  1488         except curses.error:
  1598         except curses.error:
  1489             pass
  1599             pass
  1490 
  1600 
       
  1601 
  1491 def _chistedit(ui, repo, *freeargs, **opts):
  1602 def _chistedit(ui, repo, *freeargs, **opts):
  1492     """interactively edit changeset history via a curses interface
  1603     """interactively edit changeset history via a curses interface
  1493 
  1604 
  1494     Provides a ncurses interface to histedit. Press ? in chistedit mode
  1605     Provides a ncurses interface to histedit. Press ? in chistedit mode
  1495     to see an extensive help. Requires python-curses to be installed."""
  1606     to see an extensive help. Requires python-curses to be installed."""
  1505         revs = opts.get('rev', [])[:]
  1616         revs = opts.get('rev', [])[:]
  1506         cmdutil.checkunfinished(repo)
  1617         cmdutil.checkunfinished(repo)
  1507         cmdutil.bailifchanged(repo)
  1618         cmdutil.bailifchanged(repo)
  1508 
  1619 
  1509         if os.path.exists(os.path.join(repo.path, 'histedit-state')):
  1620         if os.path.exists(os.path.join(repo.path, 'histedit-state')):
  1510             raise error.Abort(_('history edit already in progress, try '
  1621             raise error.Abort(
  1511                                '--continue or --abort'))
  1622                 _(
       
  1623                     'history edit already in progress, try '
       
  1624                     '--continue or --abort'
       
  1625                 )
       
  1626             )
  1512         revs.extend(freeargs)
  1627         revs.extend(freeargs)
  1513         if not revs:
  1628         if not revs:
  1514             defaultrev = destutil.desthistedit(ui, repo)
  1629             defaultrev = destutil.desthistedit(ui, repo)
  1515             if defaultrev is not None:
  1630             if defaultrev is not None:
  1516                 revs.append(defaultrev)
  1631                 revs.append(defaultrev)
  1517         if len(revs) != 1:
  1632         if len(revs) != 1:
  1518             raise error.Abort(
  1633             raise error.Abort(
  1519                 _('histedit requires exactly one ancestor revision'))
  1634                 _('histedit requires exactly one ancestor revision')
       
  1635             )
  1520 
  1636 
  1521         rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
  1637         rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
  1522         if len(rr) != 1:
  1638         if len(rr) != 1:
  1523             raise error.Abort(_('The specified revisions must have '
  1639             raise error.Abort(
  1524                 'exactly one common root'))
  1640                 _(
       
  1641                     'The specified revisions must have '
       
  1642                     'exactly one common root'
       
  1643                 )
       
  1644             )
  1525         root = rr[0].node()
  1645         root = rr[0].node()
  1526 
  1646 
  1527         topmost = repo.dirstate.p1()
  1647         topmost = repo.dirstate.p1()
  1528         revs = between(repo, root, topmost, keep)
  1648         revs = between(repo, root, topmost, keep)
  1529         if not revs:
  1649         if not revs:
  1530             raise error.Abort(_('%s is not an ancestor of working directory') %
  1650             raise error.Abort(
  1531                              node.short(root))
  1651                 _('%s is not an ancestor of working directory')
       
  1652                 % node.short(root)
       
  1653             )
  1532 
  1654 
  1533         ctxs = []
  1655         ctxs = []
  1534         for i, r in enumerate(revs):
  1656         for i, r in enumerate(revs):
  1535             ctxs.append(histeditrule(repo[r], i))
  1657             ctxs.append(histeditrule(repo[r], i))
  1536         # Curses requires setting the locale or it will default to the C
  1658         # Curses requires setting the locale or it will default to the C
  1554             return _texthistedit(ui, repo, *freeargs, **opts)
  1676             return _texthistedit(ui, repo, *freeargs, **opts)
  1555     except KeyboardInterrupt:
  1677     except KeyboardInterrupt:
  1556         pass
  1678         pass
  1557     return -1
  1679     return -1
  1558 
  1680 
  1559 @command('histedit',
  1681 
  1560     [('', 'commands', '',
  1682 @command(
  1561       _('read history edits from the specified file'), _('FILE')),
  1683     'histedit',
  1562      ('c', 'continue', False, _('continue an edit already in progress')),
  1684     [
  1563      ('', 'edit-plan', False, _('edit remaining actions list')),
  1685         (
  1564      ('k', 'keep', False,
  1686             '',
  1565       _("don't strip old nodes after edit is complete")),
  1687             'commands',
  1566      ('', 'abort', False, _('abort an edit in progress')),
  1688             '',
  1567      ('o', 'outgoing', False, _('changesets not found in destination')),
  1689             _('read history edits from the specified file'),
  1568      ('f', 'force', False,
  1690             _('FILE'),
  1569       _('force outgoing even for unrelated repositories')),
  1691         ),
  1570      ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
  1692         ('c', 'continue', False, _('continue an edit already in progress')),
  1571     cmdutil.formatteropts,
  1693         ('', 'edit-plan', False, _('edit remaining actions list')),
  1572      _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
  1694         ('k', 'keep', False, _("don't strip old nodes after edit is complete")),
  1573     helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
  1695         ('', 'abort', False, _('abort an edit in progress')),
       
  1696         ('o', 'outgoing', False, _('changesets not found in destination')),
       
  1697         (
       
  1698             'f',
       
  1699             'force',
       
  1700             False,
       
  1701             _('force outgoing even for unrelated repositories'),
       
  1702         ),
       
  1703         ('r', 'rev', [], _('first revision to be edited'), _('REV')),
       
  1704     ]
       
  1705     + cmdutil.formatteropts,
       
  1706     _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
       
  1707     helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
       
  1708 )
  1574 def histedit(ui, repo, *freeargs, **opts):
  1709 def histedit(ui, repo, *freeargs, **opts):
  1575     """interactively edit changeset history
  1710     """interactively edit changeset history
  1576 
  1711 
  1577     This command lets you edit a linear series of changesets (up to
  1712     This command lets you edit a linear series of changesets (up to
  1578     and including the working directory, which should be clean).
  1713     and including the working directory, which should be clean).
  1671     conflicts).
  1806     conflicts).
  1672     """
  1807     """
  1673     # kludge: _chistedit only works for starting an edit, not aborting
  1808     # kludge: _chistedit only works for starting an edit, not aborting
  1674     # or continuing, so fall back to regular _texthistedit for those
  1809     # or continuing, so fall back to regular _texthistedit for those
  1675     # operations.
  1810     # operations.
  1676     if ui.interface('histedit') == 'curses' and  _getgoal(
  1811     if (
  1677             pycompat.byteskwargs(opts)) == goalnew:
  1812         ui.interface('histedit') == 'curses'
       
  1813         and _getgoal(pycompat.byteskwargs(opts)) == goalnew
       
  1814     ):
  1678         return _chistedit(ui, repo, *freeargs, **opts)
  1815         return _chistedit(ui, repo, *freeargs, **opts)
  1679     return _texthistedit(ui, repo, *freeargs, **opts)
  1816     return _texthistedit(ui, repo, *freeargs, **opts)
       
  1817 
  1680 
  1818 
  1681 def _texthistedit(ui, repo, *freeargs, **opts):
  1819 def _texthistedit(ui, repo, *freeargs, **opts):
  1682     state = histeditstate(repo)
  1820     state = histeditstate(repo)
  1683     with repo.wlock() as wlock, repo.lock() as lock:
  1821     with repo.wlock() as wlock, repo.lock() as lock:
  1684         state.wlock = wlock
  1822         state.wlock = wlock
  1685         state.lock = lock
  1823         state.lock = lock
  1686         _histedit(ui, repo, state, *freeargs, **opts)
  1824         _histedit(ui, repo, state, *freeargs, **opts)
  1687 
  1825 
       
  1826 
  1688 goalcontinue = 'continue'
  1827 goalcontinue = 'continue'
  1689 goalabort = 'abort'
  1828 goalabort = 'abort'
  1690 goaleditplan = 'edit-plan'
  1829 goaleditplan = 'edit-plan'
  1691 goalnew = 'new'
  1830 goalnew = 'new'
       
  1831 
  1692 
  1832 
  1693 def _getgoal(opts):
  1833 def _getgoal(opts):
  1694     if opts.get(b'continue'):
  1834     if opts.get(b'continue'):
  1695         return goalcontinue
  1835         return goalcontinue
  1696     if opts.get(b'abort'):
  1836     if opts.get(b'abort'):
  1697         return goalabort
  1837         return goalabort
  1698     if opts.get(b'edit_plan'):
  1838     if opts.get(b'edit_plan'):
  1699         return goaleditplan
  1839         return goaleditplan
  1700     return goalnew
  1840     return goalnew
  1701 
  1841 
       
  1842 
  1702 def _readfile(ui, path):
  1843 def _readfile(ui, path):
  1703     if path == '-':
  1844     if path == '-':
  1704         with ui.timeblockedsection('histedit'):
  1845         with ui.timeblockedsection('histedit'):
  1705             return ui.fin.read()
  1846             return ui.fin.read()
  1706     else:
  1847     else:
  1707         with open(path, 'rb') as f:
  1848         with open(path, 'rb') as f:
  1708             return f.read()
  1849             return f.read()
       
  1850 
  1709 
  1851 
  1710 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
  1852 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
  1711     # TODO only abort if we try to histedit mq patches, not just
  1853     # TODO only abort if we try to histedit mq patches, not just
  1712     # blanket if mq patches are applied somewhere
  1854     # blanket if mq patches are applied somewhere
  1713     mq = getattr(repo, 'mq', None)
  1855     mq = getattr(repo, 'mq', None)
  1727     elif goal == 'abort':
  1869     elif goal == 'abort':
  1728         if any((outg, revs, freeargs, rules, editplan)):
  1870         if any((outg, revs, freeargs, rules, editplan)):
  1729             raise error.Abort(_('no arguments allowed with --abort'))
  1871             raise error.Abort(_('no arguments allowed with --abort'))
  1730     elif goal == 'edit-plan':
  1872     elif goal == 'edit-plan':
  1731         if any((outg, revs, freeargs)):
  1873         if any((outg, revs, freeargs)):
  1732             raise error.Abort(_('only --commands argument allowed with '
  1874             raise error.Abort(
  1733                                '--edit-plan'))
  1875                 _('only --commands argument allowed with ' '--edit-plan')
       
  1876             )
  1734     else:
  1877     else:
  1735         if state.inprogress():
  1878         if state.inprogress():
  1736             raise error.Abort(_('history edit already in progress, try '
  1879             raise error.Abort(
  1737                                '--continue or --abort'))
  1880                 _(
       
  1881                     'history edit already in progress, try '
       
  1882                     '--continue or --abort'
       
  1883                 )
       
  1884             )
  1738         if outg:
  1885         if outg:
  1739             if revs:
  1886             if revs:
  1740                 raise error.Abort(_('no revisions allowed with --outgoing'))
  1887                 raise error.Abort(_('no revisions allowed with --outgoing'))
  1741             if len(freeargs) > 1:
  1888             if len(freeargs) > 1:
  1742                 raise error.Abort(
  1889                 raise error.Abort(
  1743                     _('only one repo argument allowed with --outgoing'))
  1890                     _('only one repo argument allowed with --outgoing')
       
  1891                 )
  1744         else:
  1892         else:
  1745             revs.extend(freeargs)
  1893             revs.extend(freeargs)
  1746             if len(revs) == 0:
  1894             if len(revs) == 0:
  1747                 defaultrev = destutil.desthistedit(ui, repo)
  1895                 defaultrev = destutil.desthistedit(ui, repo)
  1748                 if defaultrev is not None:
  1896                 if defaultrev is not None:
  1749                     revs.append(defaultrev)
  1897                     revs.append(defaultrev)
  1750 
  1898 
  1751             if len(revs) != 1:
  1899             if len(revs) != 1:
  1752                 raise error.Abort(
  1900                 raise error.Abort(
  1753                     _('histedit requires exactly one ancestor revision'))
  1901                     _('histedit requires exactly one ancestor revision')
       
  1902                 )
       
  1903 
  1754 
  1904 
  1755 def _histedit(ui, repo, state, *freeargs, **opts):
  1905 def _histedit(ui, repo, state, *freeargs, **opts):
  1756     opts = pycompat.byteskwargs(opts)
  1906     opts = pycompat.byteskwargs(opts)
  1757     fm = ui.formatter('histedit', opts)
  1907     fm = ui.formatter('histedit', opts)
  1758     fm.startitem()
  1908     fm.startitem()
  1771         for ctx in ctxs:
  1921         for ctx in ctxs:
  1772             tags = [tag for tag in ctx.tags() if tag != 'tip']
  1922             tags = [tag for tag in ctx.tags() if tag != 'tip']
  1773             if not hastags:
  1923             if not hastags:
  1774                 hastags = len(tags)
  1924                 hastags = len(tags)
  1775     if hastags:
  1925     if hastags:
  1776         if ui.promptchoice(_('warning: tags associated with the given'
  1926         if ui.promptchoice(
  1777                              ' changeset will be lost after histedit.\n'
  1927             _(
  1778                              'do you want to continue (yN)? $$ &Yes $$ &No'),
  1928                 'warning: tags associated with the given'
  1779                            default=1):
  1929                 ' changeset will be lost after histedit.\n'
       
  1930                 'do you want to continue (yN)? $$ &Yes $$ &No'
       
  1931             ),
       
  1932             default=1,
       
  1933         ):
  1780             raise error.Abort(_('histedit cancelled\n'))
  1934             raise error.Abort(_('histedit cancelled\n'))
  1781     # rebuild state
  1935     # rebuild state
  1782     if goal == goalcontinue:
  1936     if goal == goalcontinue:
  1783         state.read()
  1937         state.read()
  1784         state = bootstrapcontinue(ui, state, opts)
  1938         state = bootstrapcontinue(ui, state, opts)
  1794 
  1948 
  1795     _continuehistedit(ui, repo, state)
  1949     _continuehistedit(ui, repo, state)
  1796     _finishhistedit(ui, repo, state, fm)
  1950     _finishhistedit(ui, repo, state, fm)
  1797     fm.end()
  1951     fm.end()
  1798 
  1952 
       
  1953 
  1799 def _continuehistedit(ui, repo, state):
  1954 def _continuehistedit(ui, repo, state):
  1800     """This function runs after either:
  1955     """This function runs after either:
  1801     - bootstrapcontinue (if the goal is 'continue')
  1956     - bootstrapcontinue (if the goal is 'continue')
  1802     - _newhistedit (if the goal is 'new')
  1957     - _newhistedit (if the goal is 'new')
  1803     """
  1958     """
  1804     # preprocess rules so that we can hide inner folds from the user
  1959     # preprocess rules so that we can hide inner folds from the user
  1805     # and only show one editor
  1960     # and only show one editor
  1806     actions = state.actions[:]
  1961     actions = state.actions[:]
  1807     for idx, (action, nextact) in enumerate(
  1962     for idx, (action, nextact) in enumerate(zip(actions, actions[1:] + [None])):
  1808             zip(actions, actions[1:] + [None])):
       
  1809         if action.verb == 'fold' and nextact and nextact.verb == 'fold':
  1963         if action.verb == 'fold' and nextact and nextact.verb == 'fold':
  1810             state.actions[idx].__class__ = _multifold
  1964             state.actions[idx].__class__ = _multifold
  1811 
  1965 
  1812     # Force an initial state file write, so the user can run --abort/continue
  1966     # Force an initial state file write, so the user can run --abort/continue
  1813     # even if there's an exception before the first transaction serialize.
  1967     # even if there's an exception before the first transaction serialize.
  1820     if ui.configbool("histedit", "singletransaction"):
  1974     if ui.configbool("histedit", "singletransaction"):
  1821         # Don't use a 'with' for the transaction, since actions may close
  1975         # Don't use a 'with' for the transaction, since actions may close
  1822         # and reopen a transaction. For example, if the action executes an
  1976         # and reopen a transaction. For example, if the action executes an
  1823         # external process it may choose to commit the transaction first.
  1977         # external process it may choose to commit the transaction first.
  1824         tr = repo.transaction('histedit')
  1978         tr = repo.transaction('histedit')
  1825     progress = ui.makeprogress(_("editing"), unit=_('changes'),
  1979     progress = ui.makeprogress(
  1826                                total=len(state.actions))
  1980         _("editing"), unit=_('changes'), total=len(state.actions)
       
  1981     )
  1827     with progress, util.acceptintervention(tr):
  1982     with progress, util.acceptintervention(tr):
  1828         while state.actions:
  1983         while state.actions:
  1829             state.write(tr=tr)
  1984             state.write(tr=tr)
  1830             actobj = state.actions[0]
  1985             actobj = state.actions[0]
  1831             progress.increment(item=actobj.torule())
  1986             progress.increment(item=actobj.torule())
  1832             ui.debug('histedit: processing %s %s\n' % (actobj.verb,
  1987             ui.debug(
  1833                                                        actobj.torule()))
  1988                 'histedit: processing %s %s\n' % (actobj.verb, actobj.torule())
       
  1989             )
  1834             parentctx, replacement_ = actobj.run()
  1990             parentctx, replacement_ = actobj.run()
  1835             state.parentctxnode = parentctx.node()
  1991             state.parentctxnode = parentctx.node()
  1836             state.replacements.extend(replacement_)
  1992             state.replacements.extend(replacement_)
  1837             state.actions.pop(0)
  1993             state.actions.pop(0)
  1838 
  1994 
  1839     state.write()
  1995     state.write()
       
  1996 
  1840 
  1997 
  1841 def _finishhistedit(ui, repo, state, fm):
  1998 def _finishhistedit(ui, repo, state, fm):
  1842     """This action runs when histedit is finishing its session"""
  1999     """This action runs when histedit is finishing its session"""
  1843     hg.updaterepo(repo, state.parentctxnode, overwrite=False)
  2000     hg.updaterepo(repo, state.parentctxnode, overwrite=False)
  1844 
  2001 
  1846     if mapping:
  2003     if mapping:
  1847         for prec, succs in mapping.iteritems():
  2004         for prec, succs in mapping.iteritems():
  1848             if not succs:
  2005             if not succs:
  1849                 ui.debug('histedit: %s is dropped\n' % node.short(prec))
  2006                 ui.debug('histedit: %s is dropped\n' % node.short(prec))
  1850             else:
  2007             else:
  1851                 ui.debug('histedit: %s is replaced by %s\n' % (
  2008                 ui.debug(
  1852                     node.short(prec), node.short(succs[0])))
  2009                     'histedit: %s is replaced by %s\n'
       
  2010                     % (node.short(prec), node.short(succs[0]))
       
  2011                 )
  1853                 if len(succs) > 1:
  2012                 if len(succs) > 1:
  1854                     m = 'histedit:                            %s'
  2013                     m = 'histedit:                            %s'
  1855                     for n in succs[1:]:
  2014                     for n in succs[1:]:
  1856                         ui.debug(m % node.short(n))
  2015                         ui.debug(m % node.short(n))
  1857 
  2016 
  1866         if n in repo:
  2025         if n in repo:
  1867             mapping[n] = ()
  2026             mapping[n] = ()
  1868 
  2027 
  1869     # remove entries about unknown nodes
  2028     # remove entries about unknown nodes
  1870     nodemap = repo.unfiltered().changelog.nodemap
  2029     nodemap = repo.unfiltered().changelog.nodemap
  1871     mapping = {k: v for k, v in mapping.items()
  2030     mapping = {
  1872                if k in nodemap and all(n in nodemap for n in v)}
  2031         k: v
       
  2032         for k, v in mapping.items()
       
  2033         if k in nodemap and all(n in nodemap for n in v)
       
  2034     }
  1873     scmutil.cleanupnodes(repo, mapping, 'histedit')
  2035     scmutil.cleanupnodes(repo, mapping, 'histedit')
  1874     hf = fm.hexfunc
  2036     hf = fm.hexfunc
  1875     fl = fm.formatlist
  2037     fl = fm.formatlist
  1876     fd = fm.formatdict
  2038     fd = fm.formatdict
  1877     nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
  2039     nodechanges = fd(
  1878                       for oldn, newn in mapping.iteritems()},
  2040         {
  1879                      key="oldnode", value="newnodes")
  2041             hf(oldn): fl([hf(n) for n in newn], name='node')
       
  2042             for oldn, newn in mapping.iteritems()
       
  2043         },
       
  2044         key="oldnode",
       
  2045         value="newnodes",
       
  2046     )
  1880     fm.data(nodechanges=nodechanges)
  2047     fm.data(nodechanges=nodechanges)
  1881 
  2048 
  1882     state.clear()
  2049     state.clear()
  1883     if os.path.exists(repo.sjoin('undo')):
  2050     if os.path.exists(repo.sjoin('undo')):
  1884         os.unlink(repo.sjoin('undo'))
  2051         os.unlink(repo.sjoin('undo'))
  1885     if repo.vfs.exists('histedit-last-edit.txt'):
  2052     if repo.vfs.exists('histedit-last-edit.txt'):
  1886         repo.vfs.unlink('histedit-last-edit.txt')
  2053         repo.vfs.unlink('histedit-last-edit.txt')
  1887 
  2054 
       
  2055 
  1888 def _aborthistedit(ui, repo, state, nobackup=False):
  2056 def _aborthistedit(ui, repo, state, nobackup=False):
  1889     try:
  2057     try:
  1890         state.read()
  2058         state.read()
  1891         __, leafs, tmpnodes, __ = processreplacement(state)
  2059         __, leafs, tmpnodes, __ = processreplacement(state)
  1892         ui.debug('restore wc to old parent %s\n'
  2060         ui.debug('restore wc to old parent %s\n' % node.short(state.topmost))
  1893                 % node.short(state.topmost))
       
  1894 
  2061 
  1895         # Recover our old commits if necessary
  2062         # Recover our old commits if necessary
  1896         if not state.topmost in repo and state.backupfile:
  2063         if not state.topmost in repo and state.backupfile:
  1897             backupfile = repo.vfs.join(state.backupfile)
  2064             backupfile = repo.vfs.join(state.backupfile)
  1898             f = hg.openpath(ui, backupfile)
  2065             f = hg.openpath(ui, backupfile)
  1899             gen = exchange.readbundle(ui, f, backupfile)
  2066             gen = exchange.readbundle(ui, f, backupfile)
  1900             with repo.transaction('histedit.abort') as tr:
  2067             with repo.transaction('histedit.abort') as tr:
  1901                 bundle2.applybundle(repo, gen, tr, source='histedit',
  2068                 bundle2.applybundle(
  1902                                     url='bundle:' + backupfile)
  2069                     repo, gen, tr, source='histedit', url='bundle:' + backupfile
       
  2070                 )
  1903 
  2071 
  1904             os.remove(backupfile)
  2072             os.remove(backupfile)
  1905 
  2073 
  1906         # check whether we should update away
  2074         # check whether we should update away
  1907         if repo.unfiltered().revs('parents() and (%n  or %ln::)',
  2075         if repo.unfiltered().revs(
  1908                                 state.parentctxnode, leafs | tmpnodes):
  2076             'parents() and (%n  or %ln::)',
       
  2077             state.parentctxnode,
       
  2078             leafs | tmpnodes,
       
  2079         ):
  1909             hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
  2080             hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
  1910         cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
  2081         cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
  1911         cleanupnode(ui, repo, leafs, nobackup=nobackup)
  2082         cleanupnode(ui, repo, leafs, nobackup=nobackup)
  1912     except Exception:
  2083     except Exception:
  1913         if state.inprogress():
  2084         if state.inprogress():
  1914             ui.warn(_('warning: encountered an exception during histedit '
  2085             ui.warn(
  1915                 '--abort; the repository may not have been completely '
  2086                 _(
  1916                 'cleaned up\n'))
  2087                     'warning: encountered an exception during histedit '
       
  2088                     '--abort; the repository may not have been completely '
       
  2089                     'cleaned up\n'
       
  2090                 )
       
  2091             )
  1917         raise
  2092         raise
  1918     finally:
  2093     finally:
  1919             state.clear()
  2094         state.clear()
       
  2095 
  1920 
  2096 
  1921 def hgaborthistedit(ui, repo):
  2097 def hgaborthistedit(ui, repo):
  1922     state = histeditstate(repo)
  2098     state = histeditstate(repo)
  1923     nobackup = not ui.configbool('rewrite', 'backup-bundle')
  2099     nobackup = not ui.configbool('rewrite', 'backup-bundle')
  1924     with repo.wlock() as wlock, repo.lock() as lock:
  2100     with repo.wlock() as wlock, repo.lock() as lock:
  1925         state.wlock = wlock
  2101         state.wlock = wlock
  1926         state.lock = lock
  2102         state.lock = lock
  1927         _aborthistedit(ui, repo, state, nobackup=nobackup)
  2103         _aborthistedit(ui, repo, state, nobackup=nobackup)
  1928 
  2104 
       
  2105 
  1929 def _edithisteditplan(ui, repo, state, rules):
  2106 def _edithisteditplan(ui, repo, state, rules):
  1930     state.read()
  2107     state.read()
  1931     if not rules:
  2108     if not rules:
  1932         comment = geteditcomment(ui,
  2109         comment = geteditcomment(
  1933                                  node.short(state.parentctxnode),
  2110             ui, node.short(state.parentctxnode), node.short(state.topmost)
  1934                                  node.short(state.topmost))
  2111         )
  1935         rules = ruleeditor(repo, ui, state.actions, comment)
  2112         rules = ruleeditor(repo, ui, state.actions, comment)
  1936     else:
  2113     else:
  1937         rules = _readfile(ui, rules)
  2114         rules = _readfile(ui, rules)
  1938     actions = parserules(rules, state)
  2115     actions = parserules(rules, state)
  1939     ctxs = [repo[act.node]
  2116     ctxs = [repo[act.node] for act in state.actions if act.node]
  1940             for act in state.actions if act.node]
       
  1941     warnverifyactions(ui, repo, actions, state, ctxs)
  2117     warnverifyactions(ui, repo, actions, state, ctxs)
  1942     state.actions = actions
  2118     state.actions = actions
  1943     state.write()
  2119     state.write()
       
  2120 
  1944 
  2121 
  1945 def _newhistedit(ui, repo, state, revs, freeargs, opts):
  2122 def _newhistedit(ui, repo, state, revs, freeargs, opts):
  1946     outg = opts.get('outgoing')
  2123     outg = opts.get('outgoing')
  1947     rules = opts.get('commands', '')
  2124     rules = opts.get('commands', '')
  1948     force = opts.get('force')
  2125     force = opts.get('force')
  1958             remote = None
  2135             remote = None
  1959         root = findoutgoing(ui, repo, remote, force, opts)
  2136         root = findoutgoing(ui, repo, remote, force, opts)
  1960     else:
  2137     else:
  1961         rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
  2138         rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
  1962         if len(rr) != 1:
  2139         if len(rr) != 1:
  1963             raise error.Abort(_('The specified revisions must have '
  2140             raise error.Abort(
  1964                 'exactly one common root'))
  2141                 _(
       
  2142                     'The specified revisions must have '
       
  2143                     'exactly one common root'
       
  2144                 )
       
  2145             )
  1965         root = rr[0].node()
  2146         root = rr[0].node()
  1966 
  2147 
  1967     revs = between(repo, root, topmost, state.keep)
  2148     revs = between(repo, root, topmost, state.keep)
  1968     if not revs:
  2149     if not revs:
  1969         raise error.Abort(_('%s is not an ancestor of working directory') %
  2150         raise error.Abort(
  1970                          node.short(root))
  2151             _('%s is not an ancestor of working directory') % node.short(root)
       
  2152         )
  1971 
  2153 
  1972     ctxs = [repo[r] for r in revs]
  2154     ctxs = [repo[r] for r in revs]
  1973 
  2155 
  1974     wctx = repo[None]
  2156     wctx = repo[None]
  1975     # Please don't ask me why `ancestors` is this value. I figured it
  2157     # Please don't ask me why `ancestors` is this value. I figured it
  1981     # collision after we've started histedit and backing out gets ugly
  2163     # collision after we've started histedit and backing out gets ugly
  1982     # for everyone, especially the user.
  2164     # for everyone, especially the user.
  1983     for c in [ctxs[0].p1()] + ctxs:
  2165     for c in [ctxs[0].p1()] + ctxs:
  1984         try:
  2166         try:
  1985             mergemod.calculateupdates(
  2167             mergemod.calculateupdates(
  1986                 repo, wctx, c, ancs,
  2168                 repo,
       
  2169                 wctx,
       
  2170                 c,
       
  2171                 ancs,
  1987                 # These parameters were determined by print-debugging
  2172                 # These parameters were determined by print-debugging
  1988                 # what happens later on inside histedit.
  2173                 # what happens later on inside histedit.
  1989                 branchmerge=False, force=False, acceptremote=False,
  2174                 branchmerge=False,
  1990                 followcopies=False)
  2175                 force=False,
       
  2176                 acceptremote=False,
       
  2177                 followcopies=False,
       
  2178             )
  1991         except error.Abort:
  2179         except error.Abort:
  1992             raise error.Abort(
  2180             raise error.Abort(
  1993        _("untracked files in working directory conflict with files in %s") % (
  2181                 _(
  1994            c))
  2182                     "untracked files in working directory conflict with files in %s"
       
  2183                 )
       
  2184                 % c
       
  2185             )
  1995 
  2186 
  1996     if not rules:
  2187     if not rules:
  1997         comment = geteditcomment(ui, node.short(root), node.short(topmost))
  2188         comment = geteditcomment(ui, node.short(root), node.short(topmost))
  1998         actions = [pick(state, r) for r in revs]
  2189         actions = [pick(state, r) for r in revs]
  1999         rules = ruleeditor(repo, ui, actions, comment)
  2190         rules = ruleeditor(repo, ui, actions, comment)
  2007     state.parentctxnode = parentctxnode
  2198     state.parentctxnode = parentctxnode
  2008     state.actions = actions
  2199     state.actions = actions
  2009     state.topmost = topmost
  2200     state.topmost = topmost
  2010     state.replacements = []
  2201     state.replacements = []
  2011 
  2202 
  2012     ui.log("histedit", "%d actions to histedit\n", len(actions),
  2203     ui.log(
  2013            histedit_num_actions=len(actions))
  2204         "histedit",
       
  2205         "%d actions to histedit\n",
       
  2206         len(actions),
       
  2207         histedit_num_actions=len(actions),
       
  2208     )
  2014 
  2209 
  2015     # Create a backup so we can always abort completely.
  2210     # Create a backup so we can always abort completely.
  2016     backupfile = None
  2211     backupfile = None
  2017     if not obsolete.isenabled(repo, obsolete.createmarkersopt):
  2212     if not obsolete.isenabled(repo, obsolete.createmarkersopt):
  2018         backupfile = repair.backupbundle(repo, [parentctxnode],
  2213         backupfile = repair.backupbundle(
  2019                                          [topmost], root, 'histedit')
  2214             repo, [parentctxnode], [topmost], root, 'histedit'
       
  2215         )
  2020     state.backupfile = backupfile
  2216     state.backupfile = backupfile
       
  2217 
  2021 
  2218 
  2022 def _getsummary(ctx):
  2219 def _getsummary(ctx):
  2023     # a common pattern is to extract the summary but default to the empty
  2220     # a common pattern is to extract the summary but default to the empty
  2024     # string
  2221     # string
  2025     summary = ctx.description() or ''
  2222     summary = ctx.description() or ''
  2026     if summary:
  2223     if summary:
  2027         summary = summary.splitlines()[0]
  2224         summary = summary.splitlines()[0]
  2028     return summary
  2225     return summary
  2029 
  2226 
       
  2227 
  2030 def bootstrapcontinue(ui, state, opts):
  2228 def bootstrapcontinue(ui, state, opts):
  2031     repo = state.repo
  2229     repo = state.repo
  2032 
  2230 
  2033     ms = mergemod.mergestate.read(repo)
  2231     ms = mergemod.mergestate.read(repo)
  2034     mergeutil.checkunresolved(ms)
  2232     mergeutil.checkunresolved(ms)
  2046         state.parentctxnode = parentctx.node()
  2244         state.parentctxnode = parentctx.node()
  2047         state.replacements.extend(replacements)
  2245         state.replacements.extend(replacements)
  2048 
  2246 
  2049     return state
  2247     return state
  2050 
  2248 
       
  2249 
  2051 def between(repo, old, new, keep):
  2250 def between(repo, old, new, keep):
  2052     """select and validate the set of revision to edit
  2251     """select and validate the set of revision to edit
  2053 
  2252 
  2054     When keep is false, the specified set can't have children."""
  2253     When keep is false, the specified set can't have children."""
  2055     revs = repo.revs('%n::%n', old, new)
  2254     revs = repo.revs('%n::%n', old, new)
  2056     if revs and not keep:
  2255     if revs and not keep:
  2057         if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
  2256         if not obsolete.isenabled(
  2058             repo.revs('(%ld::) - (%ld)', revs, revs)):
  2257             repo, obsolete.allowunstableopt
  2059             raise error.Abort(_('can only histedit a changeset together '
  2258         ) and repo.revs('(%ld::) - (%ld)', revs, revs):
  2060                                 'with all its descendants'))
  2259             raise error.Abort(
       
  2260                 _(
       
  2261                     'can only histedit a changeset together '
       
  2262                     'with all its descendants'
       
  2263                 )
       
  2264             )
  2061         if repo.revs('(%ld) and merge()', revs):
  2265         if repo.revs('(%ld) and merge()', revs):
  2062             raise error.Abort(_('cannot edit history that contains merges'))
  2266             raise error.Abort(_('cannot edit history that contains merges'))
  2063         root = repo[revs.first()]  # list is already sorted by repo.revs()
  2267         root = repo[revs.first()]  # list is already sorted by repo.revs()
  2064         if not root.mutable():
  2268         if not root.mutable():
  2065             raise error.Abort(_('cannot edit public changeset: %s') % root,
  2269             raise error.Abort(
  2066                              hint=_("see 'hg help phases' for details"))
  2270                 _('cannot edit public changeset: %s') % root,
       
  2271                 hint=_("see 'hg help phases' for details"),
       
  2272             )
  2067     return pycompat.maplist(repo.changelog.node, revs)
  2273     return pycompat.maplist(repo.changelog.node, revs)
       
  2274 
  2068 
  2275 
  2069 def ruleeditor(repo, ui, actions, editcomment=""):
  2276 def ruleeditor(repo, ui, actions, editcomment=""):
  2070     """open an editor to edit rules
  2277     """open an editor to edit rules
  2071 
  2278 
  2072     rules are in the format [ [act, ctx], ...] like in state.rules
  2279     rules are in the format [ [act, ctx], ...] like in state.rules
  2083             if fword.endswith('!'):
  2290             if fword.endswith('!'):
  2084                 fword = fword[:-1]
  2291                 fword = fword[:-1]
  2085                 if fword in primaryactions | secondaryactions | tertiaryactions:
  2292                 if fword in primaryactions | secondaryactions | tertiaryactions:
  2086                     act.verb = fword
  2293                     act.verb = fword
  2087                     # get the target summary
  2294                     # get the target summary
  2088                     tsum = summary[len(fword) + 1:].lstrip()
  2295                     tsum = summary[len(fword) + 1 :].lstrip()
  2089                     # safe but slow: reverse iterate over the actions so we
  2296                     # safe but slow: reverse iterate over the actions so we
  2090                     # don't clash on two commits having the same summary
  2297                     # don't clash on two commits having the same summary
  2091                     for na, l in reversed(list(newact.iteritems())):
  2298                     for na, l in reversed(list(newact.iteritems())):
  2092                         actx = repo[na.node]
  2299                         actx = repo[na.node]
  2093                         asum = _getsummary(actx)
  2300                         asum = _getsummary(actx)
  2106             actions += l
  2313             actions += l
  2107 
  2314 
  2108     rules = '\n'.join([act.torule() for act in actions])
  2315     rules = '\n'.join([act.torule() for act in actions])
  2109     rules += '\n\n'
  2316     rules += '\n\n'
  2110     rules += editcomment
  2317     rules += editcomment
  2111     rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
  2318     rules = ui.edit(
  2112                     repopath=repo.path, action='histedit')
  2319         rules,
       
  2320         ui.username(),
       
  2321         {'prefix': 'histedit'},
       
  2322         repopath=repo.path,
       
  2323         action='histedit',
       
  2324     )
  2113 
  2325 
  2114     # Save edit rules in .hg/histedit-last-edit.txt in case
  2326     # Save edit rules in .hg/histedit-last-edit.txt in case
  2115     # the user needs to ask for help after something
  2327     # the user needs to ask for help after something
  2116     # surprising happens.
  2328     # surprising happens.
  2117     with repo.vfs('histedit-last-edit.txt', 'wb') as f:
  2329     with repo.vfs('histedit-last-edit.txt', 'wb') as f:
  2118         f.write(rules)
  2330         f.write(rules)
  2119 
  2331 
  2120     return rules
  2332     return rules
  2121 
  2333 
       
  2334 
  2122 def parserules(rules, state):
  2335 def parserules(rules, state):
  2123     """Read the histedit rules string and return list of action objects """
  2336     """Read the histedit rules string and return list of action objects """
  2124     rules = [l for l in (r.strip() for r in rules.splitlines())
  2337     rules = [
  2125                 if l and not l.startswith('#')]
  2338         l
       
  2339         for l in (r.strip() for r in rules.splitlines())
       
  2340         if l and not l.startswith('#')
       
  2341     ]
  2126     actions = []
  2342     actions = []
  2127     for r in rules:
  2343     for r in rules:
  2128         if ' ' not in r:
  2344         if ' ' not in r:
  2129             raise error.ParseError(_('malformed line "%s"') % r)
  2345             raise error.ParseError(_('malformed line "%s"') % r)
  2130         verb, rest = r.split(' ', 1)
  2346         verb, rest = r.split(' ', 1)
  2134 
  2350 
  2135         action = actiontable[verb].fromrule(state, rest)
  2351         action = actiontable[verb].fromrule(state, rest)
  2136         actions.append(action)
  2352         actions.append(action)
  2137     return actions
  2353     return actions
  2138 
  2354 
       
  2355 
  2139 def warnverifyactions(ui, repo, actions, state, ctxs):
  2356 def warnverifyactions(ui, repo, actions, state, ctxs):
  2140     try:
  2357     try:
  2141         verifyactions(actions, state, ctxs)
  2358         verifyactions(actions, state, ctxs)
  2142     except error.ParseError:
  2359     except error.ParseError:
  2143         if repo.vfs.exists('histedit-last-edit.txt'):
  2360         if repo.vfs.exists('histedit-last-edit.txt'):
  2144             ui.warn(_('warning: histedit rules saved '
  2361             ui.warn(
  2145                       'to: .hg/histedit-last-edit.txt\n'))
  2362                 _(
       
  2363                     'warning: histedit rules saved '
       
  2364                     'to: .hg/histedit-last-edit.txt\n'
       
  2365                 )
       
  2366             )
  2146         raise
  2367         raise
       
  2368 
  2147 
  2369 
  2148 def verifyactions(actions, state, ctxs):
  2370 def verifyactions(actions, state, ctxs):
  2149     """Verify that there exists exactly one action per given changeset and
  2371     """Verify that there exists exactly one action per given changeset and
  2150     other constraints.
  2372     other constraints.
  2151 
  2373 
  2155     expected = set(c.node() for c in ctxs)
  2377     expected = set(c.node() for c in ctxs)
  2156     seen = set()
  2378     seen = set()
  2157     prev = None
  2379     prev = None
  2158 
  2380 
  2159     if actions and actions[0].verb in ['roll', 'fold']:
  2381     if actions and actions[0].verb in ['roll', 'fold']:
  2160         raise error.ParseError(_('first changeset cannot use verb "%s"') %
  2382         raise error.ParseError(
  2161                                actions[0].verb)
  2383             _('first changeset cannot use verb "%s"') % actions[0].verb
       
  2384         )
  2162 
  2385 
  2163     for action in actions:
  2386     for action in actions:
  2164         action.verify(prev, expected, seen)
  2387         action.verify(prev, expected, seen)
  2165         prev = action
  2388         prev = action
  2166         if action.node is not None:
  2389         if action.node is not None:
  2167             seen.add(action.node)
  2390             seen.add(action.node)
  2168     missing = sorted(expected - seen)  # sort to stabilize output
  2391     missing = sorted(expected - seen)  # sort to stabilize output
  2169 
  2392 
  2170     if state.repo.ui.configbool('histedit', 'dropmissing'):
  2393     if state.repo.ui.configbool('histedit', 'dropmissing'):
  2171         if len(actions) == 0:
  2394         if len(actions) == 0:
  2172             raise error.ParseError(_('no rules provided'),
  2395             raise error.ParseError(
  2173                     hint=_('use strip extension to remove commits'))
  2396                 _('no rules provided'),
       
  2397                 hint=_('use strip extension to remove commits'),
       
  2398             )
  2174 
  2399 
  2175         drops = [drop(state, n) for n in missing]
  2400         drops = [drop(state, n) for n in missing]
  2176         # put the in the beginning so they execute immediately and
  2401         # put the in the beginning so they execute immediately and
  2177         # don't show in the edit-plan in the future
  2402         # don't show in the edit-plan in the future
  2178         actions[:0] = drops
  2403         actions[:0] = drops
  2179     elif missing:
  2404     elif missing:
  2180         raise error.ParseError(_('missing rules for changeset %s') %
  2405         raise error.ParseError(
  2181                 node.short(missing[0]),
  2406             _('missing rules for changeset %s') % node.short(missing[0]),
  2182                 hint=_('use "drop %s" to discard, see also: '
  2407             hint=_(
  2183                        "'hg help -e histedit.config'")
  2408                 'use "drop %s" to discard, see also: '
  2184                        % node.short(missing[0]))
  2409                 "'hg help -e histedit.config'"
       
  2410             )
       
  2411             % node.short(missing[0]),
       
  2412         )
       
  2413 
  2185 
  2414 
  2186 def adjustreplacementsfrommarkers(repo, oldreplacements):
  2415 def adjustreplacementsfrommarkers(repo, oldreplacements):
  2187     """Adjust replacements from obsolescence markers
  2416     """Adjust replacements from obsolescence markers
  2188 
  2417 
  2189     Replacements structure is originally generated based on
  2418     Replacements structure is originally generated based on
  2197     nm = unfi.changelog.nodemap
  2426     nm = unfi.changelog.nodemap
  2198     obsstore = repo.obsstore
  2427     obsstore = repo.obsstore
  2199     newreplacements = list(oldreplacements)
  2428     newreplacements = list(oldreplacements)
  2200     oldsuccs = [r[1] for r in oldreplacements]
  2429     oldsuccs = [r[1] for r in oldreplacements]
  2201     # successors that have already been added to succstocheck once
  2430     # successors that have already been added to succstocheck once
  2202     seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
  2431     seensuccs = set().union(
       
  2432         *oldsuccs
       
  2433     )  # create a set from an iterable of tuples
  2203     succstocheck = list(seensuccs)
  2434     succstocheck = list(seensuccs)
  2204     while succstocheck:
  2435     while succstocheck:
  2205         n = succstocheck.pop()
  2436         n = succstocheck.pop()
  2206         missing = nm.get(n) is None
  2437         missing = nm.get(n) is None
  2207         markers = obsstore.successors.get(n, ())
  2438         markers = obsstore.successors.get(n, ())
  2215                 if nsucc not in seensuccs:
  2446                 if nsucc not in seensuccs:
  2216                     seensuccs.add(nsucc)
  2447                     seensuccs.add(nsucc)
  2217                     succstocheck.append(nsucc)
  2448                     succstocheck.append(nsucc)
  2218 
  2449 
  2219     return newreplacements
  2450     return newreplacements
       
  2451 
  2220 
  2452 
  2221 def processreplacement(state):
  2453 def processreplacement(state):
  2222     """process the list of replacements to return
  2454     """process the list of replacements to return
  2223 
  2455 
  2224     1) the final mapping between original and created nodes
  2456     1) the final mapping between original and created nodes
  2277         r = state.repo.changelog.rev
  2509         r = state.repo.changelog.rev
  2278         newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
  2510         newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
  2279 
  2511 
  2280     return final, tmpnodes, new, newtopmost
  2512     return final, tmpnodes, new, newtopmost
  2281 
  2513 
       
  2514 
  2282 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
  2515 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
  2283     """Move bookmark from oldtopmost to newly created topmost
  2516     """Move bookmark from oldtopmost to newly created topmost
  2284 
  2517 
  2285     This is arguably a feature and we may only want that for the active
  2518     This is arguably a feature and we may only want that for the active
  2286     bookmark. But the behavior is kept compatible with the old version for now.
  2519     bookmark. But the behavior is kept compatible with the old version for now.
  2293             marks = repo._bookmarks
  2526             marks = repo._bookmarks
  2294             changes = []
  2527             changes = []
  2295             for name in oldbmarks:
  2528             for name in oldbmarks:
  2296                 changes.append((name, newtopmost))
  2529                 changes.append((name, newtopmost))
  2297             marks.applychanges(repo, tr, changes)
  2530             marks.applychanges(repo, tr, changes)
       
  2531 
  2298 
  2532 
  2299 def cleanupnode(ui, repo, nodes, nobackup=False):
  2533 def cleanupnode(ui, repo, nodes, nobackup=False):
  2300     """strip a group of nodes from the repository
  2534     """strip a group of nodes from the repository
  2301 
  2535 
  2302     The set of node to strip may contains unknown nodes."""
  2536     The set of node to strip may contains unknown nodes."""
  2312         roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
  2546         roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
  2313         if roots:
  2547         if roots:
  2314             backup = not nobackup
  2548             backup = not nobackup
  2315             repair.strip(ui, repo, roots, backup=backup)
  2549             repair.strip(ui, repo, roots, backup=backup)
  2316 
  2550 
       
  2551 
  2317 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
  2552 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
  2318     if isinstance(nodelist, str):
  2553     if isinstance(nodelist, str):
  2319         nodelist = [nodelist]
  2554         nodelist = [nodelist]
  2320     state = histeditstate(repo)
  2555     state = histeditstate(repo)
  2321     if state.inprogress():
  2556     if state.inprogress():
  2322         state.read()
  2557         state.read()
  2323         histedit_nodes = {action.node for action
  2558         histedit_nodes = {
  2324                           in state.actions if action.node}
  2559             action.node for action in state.actions if action.node
       
  2560         }
  2325         common_nodes = histedit_nodes & set(nodelist)
  2561         common_nodes = histedit_nodes & set(nodelist)
  2326         if common_nodes:
  2562         if common_nodes:
  2327             raise error.Abort(_("histedit in progress, can't strip %s")
  2563             raise error.Abort(
  2328                              % ', '.join(node.short(x) for x in common_nodes))
  2564                 _("histedit in progress, can't strip %s")
       
  2565                 % ', '.join(node.short(x) for x in common_nodes)
       
  2566             )
  2329     return orig(ui, repo, nodelist, *args, **kwargs)
  2567     return orig(ui, repo, nodelist, *args, **kwargs)
  2330 
  2568 
       
  2569 
  2331 extensions.wrapfunction(repair, 'strip', stripwrapper)
  2570 extensions.wrapfunction(repair, 'strip', stripwrapper)
       
  2571 
  2332 
  2572 
  2333 def summaryhook(ui, repo):
  2573 def summaryhook(ui, repo):
  2334     state = histeditstate(repo)
  2574     state = histeditstate(repo)
  2335     if not state.inprogress():
  2575     if not state.inprogress():
  2336         return
  2576         return
  2337     state.read()
  2577     state.read()
  2338     if state.actions:
  2578     if state.actions:
  2339         # i18n: column positioning for "hg summary"
  2579         # i18n: column positioning for "hg summary"
  2340         ui.write(_('hist:   %s (histedit --continue)\n') %
  2580         ui.write(
  2341                  (ui.label(_('%d remaining'), 'histedit.remaining') %
  2581             _('hist:   %s (histedit --continue)\n')
  2342                   len(state.actions)))
  2582             % (
       
  2583                 ui.label(_('%d remaining'), 'histedit.remaining')
       
  2584                 % len(state.actions)
       
  2585             )
       
  2586         )
       
  2587 
  2343 
  2588 
  2344 def extsetup(ui):
  2589 def extsetup(ui):
  2345     cmdutil.summaryhooks.add('histedit', summaryhook)
  2590     cmdutil.summaryhooks.add('histedit', summaryhook)
  2346     statemod.addunfinished('histedit', fname='histedit-state', allowcommit=True,
  2591     statemod.addunfinished(
  2347                             continueflag=True, abortfunc=hgaborthistedit)
  2592         'histedit',
       
  2593         fname='histedit-state',
       
  2594         allowcommit=True,
       
  2595         continueflag=True,
       
  2596         abortfunc=hgaborthistedit,
       
  2597     )