##// END OF EJS Templates
fix: allow fixer tools to return metadata in addition to the file content...
Danny Hooper -
r42372:0da689a6 default
parent child Browse files
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, 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
@@ -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 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:
@@ -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