Show More
@@ -0,0 +1,86 b'' | |||||
|
1 | A python hook for "hg fix" that prints out the number of files and revisions | |||
|
2 | that were affected, along with which fixer tools were applied. Also checks how | |||
|
3 | many times it sees a specific key generated by one of the fixer tools defined | |||
|
4 | below. | |||
|
5 | ||||
|
6 | $ cat >> $TESTTMP/postfixhook.py <<EOF | |||
|
7 | > import collections | |||
|
8 | > def file(ui, repo, rev=None, path='', metadata=None, **kwargs): | |||
|
9 | > ui.status('fixed %s in revision %d using %s\n' % | |||
|
10 | > (path, rev, ', '.join(metadata.keys()))) | |||
|
11 | > def summarize(ui, repo, replacements=None, wdirwritten=False, | |||
|
12 | > metadata=None, **kwargs): | |||
|
13 | > counts = collections.defaultdict(int) | |||
|
14 | > keys = 0 | |||
|
15 | > for fixername, metadatalist in metadata.items(): | |||
|
16 | > for metadata in metadatalist: | |||
|
17 | > if metadata is None: | |||
|
18 | > continue | |||
|
19 | > counts[fixername] += 1 | |||
|
20 | > if 'key' in metadata: | |||
|
21 | > keys += 1 | |||
|
22 | > ui.status('saw "key" %d times\n' % (keys,)) | |||
|
23 | > for name, count in sorted(counts.items()): | |||
|
24 | > ui.status('fixed %d files with %s\n' % (count, name)) | |||
|
25 | > if replacements: | |||
|
26 | > ui.status('fixed %d revisions\n' % (len(replacements),)) | |||
|
27 | > if wdirwritten: | |||
|
28 | > ui.status('fixed the working copy\n') | |||
|
29 | > EOF | |||
|
30 | ||||
|
31 | Some mock output for fixer tools that demonstrate what could go wrong with | |||
|
32 | expecting the metadata output format. | |||
|
33 | ||||
|
34 | $ printf 'new content\n' > $TESTTMP/missing | |||
|
35 | $ printf 'not valid json\0new content\n' > $TESTTMP/invalid | |||
|
36 | $ printf '{"key": "value"}\0new content\n' > $TESTTMP/valid | |||
|
37 | ||||
|
38 | Configure some fixer tools based on the output defined above, and enable the | |||
|
39 | hooks defined above. Disable parallelism to make output of the parallel file | |||
|
40 | processing phase stable. | |||
|
41 | ||||
|
42 | $ cat >> $HGRCPATH <<EOF | |||
|
43 | > [extensions] | |||
|
44 | > fix = | |||
|
45 | > [fix] | |||
|
46 | > missing:command=cat $TESTTMP/missing | |||
|
47 | > missing:pattern=missing | |||
|
48 | > missing:metadata=true | |||
|
49 | > invalid:command=cat $TESTTMP/invalid | |||
|
50 | > invalid:pattern=invalid | |||
|
51 | > invalid:metadata=true | |||
|
52 | > valid:command=cat $TESTTMP/valid | |||
|
53 | > valid:pattern=valid | |||
|
54 | > valid:metadata=true | |||
|
55 | > [hooks] | |||
|
56 | > postfixfile = python:$TESTTMP/postfixhook.py:file | |||
|
57 | > postfix = python:$TESTTMP/postfixhook.py:summarize | |||
|
58 | > [worker] | |||
|
59 | > enabled=false | |||
|
60 | > EOF | |||
|
61 | ||||
|
62 | See what happens when we execute each of the fixer tools. Some print warnings, | |||
|
63 | some write back to the file. | |||
|
64 | ||||
|
65 | $ hg init repo | |||
|
66 | $ cd repo | |||
|
67 | ||||
|
68 | $ printf "old content\n" > invalid | |||
|
69 | $ printf "old content\n" > missing | |||
|
70 | $ printf "old content\n" > valid | |||
|
71 | $ hg add -q | |||
|
72 | ||||
|
73 | $ hg fix -w | |||
|
74 | ignored invalid output from fixer tool: invalid | |||
|
75 | ignored invalid output from fixer tool: missing | |||
|
76 | fixed valid in revision 2147483647 using valid | |||
|
77 | saw "key" 1 times | |||
|
78 | fixed 1 files with valid | |||
|
79 | fixed the working copy | |||
|
80 | ||||
|
81 | $ cat missing invalid valid | |||
|
82 | old content | |||
|
83 | old content | |||
|
84 | new content | |||
|
85 | ||||
|
86 | $ cd .. |
@@ -72,12 +72,43 b" in a text file by ensuring that 'sort' r" | |||||
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 | |
@@ -117,13 +148,14 b' command = registrar.command(cmdtable)' | |||||
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(): | |
@@ -201,10 +233,12 b' def fix(ui, repo, *pats, **opts):' | |||||
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, |
|
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, |
|
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 | |||
@@ -215,15 +249,25 b' def fix(ui, repo, *pats, **opts):' | |||||
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 | |
@@ -240,6 +284,12 b' def fix(ui, repo, *pats, **opts):' | |||||
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. | |
@@ -491,6 +541,7 b' def fixfile(ui, opts, fixers, fixctx, pa' | |||||
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): | |
@@ -506,9 +557,20 b' def fixfile(ui, opts, fixers, fixctx, pa' | |||||
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 |
|
|
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: | |
@@ -519,7 +581,7 b' def fixfile(ui, opts, fixers, fixctx, pa' | |||||
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 | |
@@ -667,6 +729,10 b' class Fixer(object):' | |||||
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 |
@@ -185,6 +185,36 b' Help text for fix.' | |||||
185 | tool may see different values for the arguments added by the :linerange |
|
185 | tool may see different values for the arguments added by the :linerange | |
186 | suboption. |
|
186 | suboption. | |
187 |
|
187 | |||
|
188 | Each fixer tool is allowed to return some metadata in addition to the fixed | |||
|
189 | file content. The metadata must be placed before the file content on stdout, | |||
|
190 | separated from the file content by a zero byte. The metadata is parsed as a | |||
|
191 | JSON value (so, it should be UTF-8 encoded and contain no zero bytes). A fixer | |||
|
192 | tool is expected to produce this metadata encoding if and only if the | |||
|
193 | :metadata suboption is true: | |||
|
194 | ||||
|
195 | [fix] | |||
|
196 | tool:command = tool --prepend-json-metadata | |||
|
197 | tool:metadata = true | |||
|
198 | ||||
|
199 | The metadata values are passed to hooks, which can be used to print summaries | |||
|
200 | or perform other post-fixing work. The supported hooks are: | |||
|
201 | ||||
|
202 | "postfixfile" | |||
|
203 | Run once for each file in each revision where any fixer tools made changes | |||
|
204 | to the file content. Provides "$HG_REV" and "$HG_PATH" to identify the file, | |||
|
205 | and "$HG_METADATA" with a map of fixer names to metadata values from fixer | |||
|
206 | tools that affected the file. Fixer tools that didn't affect the file have a | |||
|
207 | valueof None. Only fixer tools that executed are present in the metadata. | |||
|
208 | ||||
|
209 | "postfix" | |||
|
210 | Run once after all files and revisions have been handled. Provides | |||
|
211 | "$HG_REPLACEMENTS" with information about what revisions were created and | |||
|
212 | made obsolete. Provides a boolean "$HG_WDIRWRITTEN" to indicate whether any | |||
|
213 | files in the working copy were updated. Provides a list "$HG_METADATA" | |||
|
214 | mapping fixer tool names to lists of metadata values returned from | |||
|
215 | executions that modified a file. This aggregates the same metadata | |||
|
216 | previously passed to the "postfixfile" hook. | |||
|
217 | ||||
188 | list of commands: |
|
218 | list of commands: | |
189 |
|
|
219 | ||
190 | fix rewrite file content in changesets or working directory |
|
220 | fix rewrite file content in changesets or working directory |
General Comments 0
You need to be logged in to leave comments.
Login now