hgext/fix.py
changeset 42229 0da689a60163
parent 42009 8f427f7c1f71
child 42685 9ed63cd0026c
equal deleted inserted replaced
42228:7c0ece3cd3ee 42229:0da689a60163
    70   head:priority = 1
    70   head:priority = 1
    71 
    71 
    72 To account for changes made by each tool, the line numbers used for incremental
    72 To account for changes made by each tool, the line numbers used for incremental
    73 formatting are recomputed before executing the next tool. So, each tool may see
    73 formatting are recomputed before executing the next tool. So, each tool may see
    74 different values for the arguments added by the :linerange suboption.
    74 different values for the arguments added by the :linerange suboption.
       
    75 
       
    76 Each fixer tool is allowed to return some metadata in addition to the fixed file
       
    77 content. The metadata must be placed before the file content on stdout,
       
    78 separated from the file content by a zero byte. The metadata is parsed as a JSON
       
    79 value (so, it should be UTF-8 encoded and contain no zero bytes). A fixer tool
       
    80 is expected to produce this metadata encoding if and only if the :metadata
       
    81 suboption is true::
       
    82 
       
    83   [fix]
       
    84   tool:command = tool --prepend-json-metadata
       
    85   tool:metadata = true
       
    86 
       
    87 The metadata values are passed to hooks, which can be used to print summaries or
       
    88 perform other post-fixing work. The supported hooks are::
       
    89 
       
    90   "postfixfile"
       
    91     Run once for each file in each revision where any fixer tools made changes
       
    92     to the file content. Provides "$HG_REV" and "$HG_PATH" to identify the file,
       
    93     and "$HG_METADATA" with a map of fixer names to metadata values from fixer
       
    94     tools that affected the file. Fixer tools that didn't affect the file have a
       
    95     valueof None. Only fixer tools that executed are present in the metadata.
       
    96 
       
    97   "postfix"
       
    98     Run once after all files and revisions have been handled. Provides
       
    99     "$HG_REPLACEMENTS" with information about what revisions were created and
       
   100     made obsolete. Provides a boolean "$HG_WDIRWRITTEN" to indicate whether any
       
   101     files in the working copy were updated. Provides a list "$HG_METADATA"
       
   102     mapping fixer tool names to lists of metadata values returned from
       
   103     executions that modified a file. This aggregates the same metadata
       
   104     previously passed to the "postfixfile" hook.
    75 """
   105 """
    76 
   106 
    77 from __future__ import absolute_import
   107 from __future__ import absolute_import
    78 
   108 
    79 import collections
   109 import collections
    80 import itertools
   110 import itertools
       
   111 import json
    81 import os
   112 import os
    82 import re
   113 import re
    83 import subprocess
   114 import subprocess
    84 
   115 
    85 from mercurial.i18n import _
   116 from mercurial.i18n import _
   115 command = registrar.command(cmdtable)
   146 command = registrar.command(cmdtable)
   116 
   147 
   117 configtable = {}
   148 configtable = {}
   118 configitem = registrar.configitem(configtable)
   149 configitem = registrar.configitem(configtable)
   119 
   150 
   120 # Register the suboptions allowed for each configured fixer.
   151 # Register the suboptions allowed for each configured fixer, and default values.
   121 FIXER_ATTRS = {
   152 FIXER_ATTRS = {
   122     'command': None,
   153     'command': None,
   123     'linerange': None,
   154     'linerange': None,
   124     'fileset': None,
   155     'fileset': None,
   125     'pattern': None,
   156     'pattern': None,
   126     'priority': 0,
   157     'priority': 0,
       
   158     'metadata': False,
   127 }
   159 }
   128 
   160 
   129 for key, default in FIXER_ATTRS.items():
   161 for key, default in FIXER_ATTRS.items():
   130     configitem('fix', '.*(:%s)?' % key, default=default, generic=True)
   162     configitem('fix', '.*(:%s)?' % key, default=default, generic=True)
   131 
   163 
   199         # revision, so we can use all available parallelism.
   231         # revision, so we can use all available parallelism.
   200         def getfixes(items):
   232         def getfixes(items):
   201             for rev, path in items:
   233             for rev, path in items:
   202                 ctx = repo[rev]
   234                 ctx = repo[rev]
   203                 olddata = ctx[path].data()
   235                 olddata = ctx[path].data()
   204                 newdata = fixfile(ui, opts, fixers, ctx, path, basectxs[rev])
   236                 metadata, newdata = fixfile(ui, opts, fixers, ctx, path,
       
   237                                             basectxs[rev])
   205                 # Don't waste memory/time passing unchanged content back, but
   238                 # Don't waste memory/time passing unchanged content back, but
   206                 # produce one result per item either way.
   239                 # produce one result per item either way.
   207                 yield (rev, path, newdata if newdata != olddata else None)
   240                 yield (rev, path, metadata,
       
   241                        newdata if newdata != olddata else None)
   208         results = worker.worker(ui, 1.0, getfixes, tuple(), workqueue,
   242         results = worker.worker(ui, 1.0, getfixes, tuple(), workqueue,
   209                                 threadsafe=False)
   243                                 threadsafe=False)
   210 
   244 
   211         # We have to hold on to the data for each successor revision in memory
   245         # We have to hold on to the data for each successor revision in memory
   212         # until all its parents are committed. We ensure this by committing and
   246         # until all its parents are committed. We ensure this by committing and
   213         # freeing memory for the revisions in some topological order. This
   247         # freeing memory for the revisions in some topological order. This
   214         # leaves a little bit of memory efficiency on the table, but also makes
   248         # leaves a little bit of memory efficiency on the table, but also makes
   215         # the tests deterministic. It might also be considered a feature since
   249         # the tests deterministic. It might also be considered a feature since
   216         # it makes the results more easily reproducible.
   250         # it makes the results more easily reproducible.
   217         filedata = collections.defaultdict(dict)
   251         filedata = collections.defaultdict(dict)
       
   252         aggregatemetadata = collections.defaultdict(list)
   218         replacements = {}
   253         replacements = {}
   219         wdirwritten = False
   254         wdirwritten = False
   220         commitorder = sorted(revstofix, reverse=True)
   255         commitorder = sorted(revstofix, reverse=True)
   221         with ui.makeprogress(topic=_('fixing'), unit=_('files'),
   256         with ui.makeprogress(topic=_('fixing'), unit=_('files'),
   222                              total=sum(numitems.values())) as progress:
   257                              total=sum(numitems.values())) as progress:
   223             for rev, path, newdata in results:
   258             for rev, path, filerevmetadata, newdata in results:
   224                 progress.increment(item=path)
   259                 progress.increment(item=path)
       
   260                 for fixername, fixermetadata in filerevmetadata.items():
       
   261                     aggregatemetadata[fixername].append(fixermetadata)
   225                 if newdata is not None:
   262                 if newdata is not None:
   226                     filedata[rev][path] = newdata
   263                     filedata[rev][path] = newdata
       
   264                     hookargs = {
       
   265                       'rev': rev,
       
   266                       'path': path,
       
   267                       'metadata': filerevmetadata,
       
   268                     }
       
   269                     repo.hook('postfixfile', throw=False,
       
   270                               **pycompat.strkwargs(hookargs))
   227                 numitems[rev] -= 1
   271                 numitems[rev] -= 1
   228                 # Apply the fixes for this and any other revisions that are
   272                 # Apply the fixes for this and any other revisions that are
   229                 # ready and sitting at the front of the queue. Using a loop here
   273                 # ready and sitting at the front of the queue. Using a loop here
   230                 # prevents the queue from being blocked by the first revision to
   274                 # prevents the queue from being blocked by the first revision to
   231                 # be ready out of order.
   275                 # be ready out of order.
   238                     else:
   282                     else:
   239                         replacerev(ui, repo, ctx, filedata[rev], replacements)
   283                         replacerev(ui, repo, ctx, filedata[rev], replacements)
   240                     del filedata[rev]
   284                     del filedata[rev]
   241 
   285 
   242         cleanup(repo, replacements, wdirwritten)
   286         cleanup(repo, replacements, wdirwritten)
       
   287         hookargs = {
       
   288             'replacements': replacements,
       
   289             'wdirwritten': wdirwritten,
       
   290             'metadata': aggregatemetadata,
       
   291         }
       
   292         repo.hook('postfix', throw=True, **pycompat.strkwargs(hookargs))
   243 
   293 
   244 def cleanup(repo, replacements, wdirwritten):
   294 def cleanup(repo, replacements, wdirwritten):
   245     """Calls scmutil.cleanupnodes() with the given replacements.
   295     """Calls scmutil.cleanupnodes() with the given replacements.
   246 
   296 
   247     "replacements" is a dict from nodeid to nodeid, with one key and one value
   297     "replacements" is a dict from nodeid to nodeid, with one key and one value
   489     (i.e. they will only avoid lines that are common to all basectxs).
   539     (i.e. they will only avoid lines that are common to all basectxs).
   490 
   540 
   491     A fixer tool's stdout will become the file's new content if and only if it
   541     A fixer tool's stdout will become the file's new content if and only if it
   492     exits with code zero.
   542     exits with code zero.
   493     """
   543     """
       
   544     metadata = {}
   494     newdata = fixctx[path].data()
   545     newdata = fixctx[path].data()
   495     for fixername, fixer in fixers.iteritems():
   546     for fixername, fixer in fixers.iteritems():
   496         if fixer.affects(opts, fixctx, path):
   547         if fixer.affects(opts, fixctx, path):
   497             rangesfn = lambda: lineranges(opts, path, basectxs, fixctx, newdata)
   548             rangesfn = lambda: lineranges(opts, path, basectxs, fixctx, newdata)
   498             command = fixer.command(ui, path, rangesfn)
   549             command = fixer.command(ui, path, rangesfn)
   504                 shell=True,
   555                 shell=True,
   505                 cwd=procutil.tonativestr(b'/'),
   556                 cwd=procutil.tonativestr(b'/'),
   506                 stdin=subprocess.PIPE,
   557                 stdin=subprocess.PIPE,
   507                 stdout=subprocess.PIPE,
   558                 stdout=subprocess.PIPE,
   508                 stderr=subprocess.PIPE)
   559                 stderr=subprocess.PIPE)
   509             newerdata, stderr = proc.communicate(newdata)
   560             stdout, stderr = proc.communicate(newdata)
   510             if stderr:
   561             if stderr:
   511                 showstderr(ui, fixctx.rev(), fixername, stderr)
   562                 showstderr(ui, fixctx.rev(), fixername, stderr)
       
   563             newerdata = stdout
       
   564             if fixer.shouldoutputmetadata():
       
   565                 try:
       
   566                     metadatajson, newerdata = stdout.split('\0', 1)
       
   567                     metadata[fixername] = json.loads(metadatajson)
       
   568                 except ValueError:
       
   569                     ui.warn(_('ignored invalid output from fixer tool: %s\n') %
       
   570                             (fixername,))
       
   571                     continue
       
   572             else:
       
   573                 metadata[fixername] = None
   512             if proc.returncode == 0:
   574             if proc.returncode == 0:
   513                 newdata = newerdata
   575                 newdata = newerdata
   514             else:
   576             else:
   515                 if not stderr:
   577                 if not stderr:
   516                     message = _('exited with status %d\n') % (proc.returncode,)
   578                     message = _('exited with status %d\n') % (proc.returncode,)
   517                     showstderr(ui, fixctx.rev(), fixername, message)
   579                     showstderr(ui, fixctx.rev(), fixername, message)
   518                 checktoolfailureaction(
   580                 checktoolfailureaction(
   519                     ui, _('no fixes will be applied'),
   581                     ui, _('no fixes will be applied'),
   520                     hint=_('use --config fix.failure=continue to apply any '
   582                     hint=_('use --config fix.failure=continue to apply any '
   521                            'successful fixes anyway'))
   583                            'successful fixes anyway'))
   522     return newdata
   584     return metadata, newdata
   523 
   585 
   524 def showstderr(ui, rev, fixername, stderr):
   586 def showstderr(ui, rev, fixername, stderr):
   525     """Writes the lines of the stderr string as warnings on the ui
   587     """Writes the lines of the stderr string as warnings on the ui
   526 
   588 
   527     Uses the revision number and fixername to give more context to each line of
   589     Uses the revision number and fixername to give more context to each line of
   665 
   727 
   666     def affects(self, opts, fixctx, path):
   728     def affects(self, opts, fixctx, path):
   667         """Should this fixer run on the file at the given path and context?"""
   729         """Should this fixer run on the file at the given path and context?"""
   668         return scmutil.match(fixctx, [self._pattern], opts)(path)
   730         return scmutil.match(fixctx, [self._pattern], opts)(path)
   669 
   731 
       
   732     def shouldoutputmetadata(self):
       
   733         """Should the stdout of this fixer start with JSON and a null byte?"""
       
   734         return self._metadata
       
   735 
   670     def command(self, ui, path, rangesfn):
   736     def command(self, ui, path, rangesfn):
   671         """A shell command to use to invoke this fixer on the given file/lines
   737         """A shell command to use to invoke this fixer on the given file/lines
   672 
   738 
   673         May return None if there is no appropriate command to run for the given
   739         May return None if there is no appropriate command to run for the given
   674         parameters.
   740         parameters.