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 _ |
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. |