##// END OF EJS Templates
fix: use a worker pool to parallelize running tools...
Danny Hooper -
r38554:5ffe2041 default
parent child Browse files
Show More
@@ -1,551 +1,581
1 # fix - rewrite file content in changesets and working copy
1 # fix - rewrite file content in changesets and working copy
2 #
2 #
3 # Copyright 2018 Google LLC.
3 # Copyright 2018 Google LLC.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """rewrite file content in changesets or working copy (EXPERIMENTAL)
7 """rewrite file content in changesets or working copy (EXPERIMENTAL)
8
8
9 Provides a command that runs configured tools on the contents of modified files,
9 Provides a command that runs configured tools on the contents of modified files,
10 writing back any fixes to the working copy or replacing changesets.
10 writing back any fixes to the working copy or replacing changesets.
11
11
12 Here is an example configuration that causes :hg:`fix` to apply automatic
12 Here is an example configuration that causes :hg:`fix` to apply automatic
13 formatting fixes to modified lines in C++ code::
13 formatting fixes to modified lines in C++ code::
14
14
15 [fix]
15 [fix]
16 clang-format:command=clang-format --assume-filename={rootpath}
16 clang-format:command=clang-format --assume-filename={rootpath}
17 clang-format:linerange=--lines={first}:{last}
17 clang-format:linerange=--lines={first}:{last}
18 clang-format:fileset=set:**.cpp or **.hpp
18 clang-format:fileset=set:**.cpp or **.hpp
19
19
20 The :command suboption forms the first part of the shell command that will be
20 The :command suboption forms the first part of the shell command that will be
21 used to fix a file. The content of the file is passed on standard input, and the
21 used to fix a file. The content of the file is passed on standard input, and the
22 fixed file content is expected on standard output. If there is any output on
22 fixed file content is expected on standard output. If there is any output on
23 standard error, the file will not be affected. Some values may be substituted
23 standard error, the file will not be affected. Some values may be substituted
24 into the command::
24 into the command::
25
25
26 {rootpath} The path of the file being fixed, relative to the repo root
26 {rootpath} The path of the file being fixed, relative to the repo root
27 {basename} The name of the file being fixed, without the directory path
27 {basename} The name of the file being fixed, without the directory path
28
28
29 If the :linerange suboption is set, the tool will only be run if there are
29 If the :linerange suboption is set, the tool will only be run if there are
30 changed lines in a file. The value of this suboption is appended to the shell
30 changed lines in a file. The value of this suboption is appended to the shell
31 command once for every range of changed lines in the file. Some values may be
31 command once for every range of changed lines in the file. Some values may be
32 substituted into the command::
32 substituted into the command::
33
33
34 {first} The 1-based line number of the first line in the modified range
34 {first} The 1-based line number of the first line in the modified range
35 {last} The 1-based line number of the last line in the modified range
35 {last} The 1-based line number of the last line in the modified range
36
36
37 The :fileset suboption determines which files will be passed through each
37 The :fileset suboption determines which files will be passed through each
38 configured tool. See :hg:`help fileset` for possible values. If there are file
38 configured tool. See :hg:`help fileset` for possible values. If there are file
39 arguments to :hg:`fix`, the intersection of these filesets is used.
39 arguments to :hg:`fix`, the intersection of these filesets is used.
40
40
41 There is also a configurable limit for the maximum size of file that will be
41 There is also a configurable limit for the maximum size of file that will be
42 processed by :hg:`fix`::
42 processed by :hg:`fix`::
43
43
44 [fix]
44 [fix]
45 maxfilesize=2MB
45 maxfilesize=2MB
46
46
47 """
47 """
48
48
49 from __future__ import absolute_import
49 from __future__ import absolute_import
50
50
51 import collections
51 import collections
52 import itertools
52 import itertools
53 import os
53 import os
54 import re
54 import re
55 import subprocess
55 import subprocess
56
56
57 from mercurial.i18n import _
57 from mercurial.i18n import _
58 from mercurial.node import nullrev
58 from mercurial.node import nullrev
59 from mercurial.node import wdirrev
59 from mercurial.node import wdirrev
60
60
61 from mercurial import (
61 from mercurial import (
62 cmdutil,
62 cmdutil,
63 context,
63 context,
64 copies,
64 copies,
65 error,
65 error,
66 mdiff,
66 mdiff,
67 merge,
67 merge,
68 obsolete,
68 obsolete,
69 pycompat,
69 pycompat,
70 registrar,
70 registrar,
71 scmutil,
71 scmutil,
72 util,
72 util,
73 worker,
73 )
74 )
74
75
75 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
76 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
76 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
77 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
77 # be specifying the version(s) of Mercurial they are tested with, or
78 # be specifying the version(s) of Mercurial they are tested with, or
78 # leave the attribute unspecified.
79 # leave the attribute unspecified.
79 testedwith = 'ships-with-hg-core'
80 testedwith = 'ships-with-hg-core'
80
81
81 cmdtable = {}
82 cmdtable = {}
82 command = registrar.command(cmdtable)
83 command = registrar.command(cmdtable)
83
84
84 configtable = {}
85 configtable = {}
85 configitem = registrar.configitem(configtable)
86 configitem = registrar.configitem(configtable)
86
87
87 # Register the suboptions allowed for each configured fixer.
88 # Register the suboptions allowed for each configured fixer.
88 FIXER_ATTRS = ('command', 'linerange', 'fileset')
89 FIXER_ATTRS = ('command', 'linerange', 'fileset')
89
90
90 for key in FIXER_ATTRS:
91 for key in FIXER_ATTRS:
91 configitem('fix', '.*(:%s)?' % key, default=None, generic=True)
92 configitem('fix', '.*(:%s)?' % key, default=None, generic=True)
92
93
93 # A good default size allows most source code files to be fixed, but avoids
94 # A good default size allows most source code files to be fixed, but avoids
94 # letting fixer tools choke on huge inputs, which could be surprising to the
95 # letting fixer tools choke on huge inputs, which could be surprising to the
95 # user.
96 # user.
96 configitem('fix', 'maxfilesize', default='2MB')
97 configitem('fix', 'maxfilesize', default='2MB')
97
98
98 @command('fix',
99 @command('fix',
99 [('', 'all', False, _('fix all non-public non-obsolete revisions')),
100 [('', 'all', False, _('fix all non-public non-obsolete revisions')),
100 ('', 'base', [], _('revisions to diff against (overrides automatic '
101 ('', 'base', [], _('revisions to diff against (overrides automatic '
101 'selection, and applies to every revision being '
102 'selection, and applies to every revision being '
102 'fixed)'), _('REV')),
103 'fixed)'), _('REV')),
103 ('r', 'rev', [], _('revisions to fix'), _('REV')),
104 ('r', 'rev', [], _('revisions to fix'), _('REV')),
104 ('w', 'working-dir', False, _('fix the working directory')),
105 ('w', 'working-dir', False, _('fix the working directory')),
105 ('', 'whole', False, _('always fix every line of a file'))],
106 ('', 'whole', False, _('always fix every line of a file'))],
106 _('[OPTION]... [FILE]...'))
107 _('[OPTION]... [FILE]...'))
107 def fix(ui, repo, *pats, **opts):
108 def fix(ui, repo, *pats, **opts):
108 """rewrite file content in changesets or working directory
109 """rewrite file content in changesets or working directory
109
110
110 Runs any configured tools to fix the content of files. Only affects files
111 Runs any configured tools to fix the content of files. Only affects files
111 with changes, unless file arguments are provided. Only affects changed lines
112 with changes, unless file arguments are provided. Only affects changed lines
112 of files, unless the --whole flag is used. Some tools may always affect the
113 of files, unless the --whole flag is used. Some tools may always affect the
113 whole file regardless of --whole.
114 whole file regardless of --whole.
114
115
115 If revisions are specified with --rev, those revisions will be checked, and
116 If revisions are specified with --rev, those revisions will be checked, and
116 they may be replaced with new revisions that have fixed file content. It is
117 they may be replaced with new revisions that have fixed file content. It is
117 desirable to specify all descendants of each specified revision, so that the
118 desirable to specify all descendants of each specified revision, so that the
118 fixes propagate to the descendants. If all descendants are fixed at the same
119 fixes propagate to the descendants. If all descendants are fixed at the same
119 time, no merging, rebasing, or evolution will be required.
120 time, no merging, rebasing, or evolution will be required.
120
121
121 If --working-dir is used, files with uncommitted changes in the working copy
122 If --working-dir is used, files with uncommitted changes in the working copy
122 will be fixed. If the checked-out revision is also fixed, the working
123 will be fixed. If the checked-out revision is also fixed, the working
123 directory will update to the replacement revision.
124 directory will update to the replacement revision.
124
125
125 When determining what lines of each file to fix at each revision, the whole
126 When determining what lines of each file to fix at each revision, the whole
126 set of revisions being fixed is considered, so that fixes to earlier
127 set of revisions being fixed is considered, so that fixes to earlier
127 revisions are not forgotten in later ones. The --base flag can be used to
128 revisions are not forgotten in later ones. The --base flag can be used to
128 override this default behavior, though it is not usually desirable to do so.
129 override this default behavior, though it is not usually desirable to do so.
129 """
130 """
130 opts = pycompat.byteskwargs(opts)
131 opts = pycompat.byteskwargs(opts)
131 if opts['all']:
132 if opts['all']:
132 if opts['rev']:
133 if opts['rev']:
133 raise error.Abort(_('cannot specify both "--rev" and "--all"'))
134 raise error.Abort(_('cannot specify both "--rev" and "--all"'))
134 opts['rev'] = ['not public() and not obsolete()']
135 opts['rev'] = ['not public() and not obsolete()']
135 opts['working_dir'] = True
136 opts['working_dir'] = True
136 with repo.wlock(), repo.lock(), repo.transaction('fix'):
137 with repo.wlock(), repo.lock(), repo.transaction('fix'):
137 revstofix = getrevstofix(ui, repo, opts)
138 revstofix = getrevstofix(ui, repo, opts)
138 basectxs = getbasectxs(repo, opts, revstofix)
139 basectxs = getbasectxs(repo, opts, revstofix)
139 workqueue, numitems = getworkqueue(ui, repo, pats, opts, revstofix,
140 workqueue, numitems = getworkqueue(ui, repo, pats, opts, revstofix,
140 basectxs)
141 basectxs)
142 fixers = getfixers(ui)
143
144 # There are no data dependencies between the workers fixing each file
145 # revision, so we can use all available parallelism.
146 def getfixes(items):
147 for rev, path in items:
148 ctx = repo[rev]
149 olddata = ctx[path].data()
150 newdata = fixfile(ui, opts, fixers, ctx, path, basectxs[rev])
151 # Don't waste memory/time passing unchanged content back, but
152 # produce one result per item either way.
153 yield (rev, path, newdata if newdata != olddata else None)
154 results = worker.worker(ui, 1.0, getfixes, tuple(), workqueue)
155
156 # We have to hold on to the data for each successor revision in memory
157 # until all its parents are committed. We ensure this by committing and
158 # freeing memory for the revisions in some topological order. This
159 # leaves a little bit of memory efficiency on the table, but also makes
160 # the tests deterministic. It might also be considered a feature since
161 # it makes the results more easily reproducible.
141 filedata = collections.defaultdict(dict)
162 filedata = collections.defaultdict(dict)
142 replacements = {}
163 replacements = {}
143 fixers = getfixers(ui)
164 commitorder = sorted(revstofix, reverse=True)
144 # Some day this loop can become a worker pool, but for now it's easier
165 for rev, path, newdata in results:
145 # to fix everything serially in topological order.
166 if newdata is not None:
146 for rev, path in sorted(workqueue):
147 ctx = repo[rev]
148 olddata = ctx[path].data()
149 newdata = fixfile(ui, opts, fixers, ctx, path, basectxs[rev])
150 if newdata != olddata:
151 filedata[rev][path] = newdata
167 filedata[rev][path] = newdata
152 numitems[rev] -= 1
168 numitems[rev] -= 1
153 if not numitems[rev]:
169 # Apply the fixes for this and any other revisions that are ready
170 # and sitting at the front of the queue. Using a loop here prevents
171 # the queue from being blocked by the first revision to be ready out
172 # of order.
173 while commitorder and not numitems[commitorder[-1]]:
174 rev = commitorder.pop()
175 ctx = repo[rev]
154 if rev == wdirrev:
176 if rev == wdirrev:
155 writeworkingdir(repo, ctx, filedata[rev], replacements)
177 writeworkingdir(repo, ctx, filedata[rev], replacements)
156 else:
178 else:
157 replacerev(ui, repo, ctx, filedata[rev], replacements)
179 replacerev(ui, repo, ctx, filedata[rev], replacements)
158 del filedata[rev]
180 del filedata[rev]
159
181
160 replacements = {prec: [succ] for prec, succ in replacements.iteritems()}
182 replacements = {prec: [succ] for prec, succ in replacements.iteritems()}
161 scmutil.cleanupnodes(repo, replacements, 'fix', fixphase=True)
183 scmutil.cleanupnodes(repo, replacements, 'fix', fixphase=True)
162
184
163 def getworkqueue(ui, repo, pats, opts, revstofix, basectxs):
185 def getworkqueue(ui, repo, pats, opts, revstofix, basectxs):
164 """"Constructs the list of files to be fixed at specific revisions
186 """"Constructs the list of files to be fixed at specific revisions
165
187
166 It is up to the caller how to consume the work items, and the only
188 It is up to the caller how to consume the work items, and the only
167 dependence between them is that replacement revisions must be committed in
189 dependence between them is that replacement revisions must be committed in
168 topological order. Each work item represents a file in the working copy or
190 topological order. Each work item represents a file in the working copy or
169 in some revision that should be fixed and written back to the working copy
191 in some revision that should be fixed and written back to the working copy
170 or into a replacement revision.
192 or into a replacement revision.
193
194 Work items for the same revision are grouped together, so that a worker
195 pool starting with the first N items in parallel is likely to finish the
196 first revision's work before other revisions. This can allow us to write
197 the result to disk and reduce memory footprint. At time of writing, the
198 partition strategy in worker.py seems favorable to this. We also sort the
199 items by ascending revision number to match the order in which we commit
200 the fixes later.
171 """
201 """
172 workqueue = []
202 workqueue = []
173 numitems = collections.defaultdict(int)
203 numitems = collections.defaultdict(int)
174 maxfilesize = ui.configbytes('fix', 'maxfilesize')
204 maxfilesize = ui.configbytes('fix', 'maxfilesize')
175 for rev in revstofix:
205 for rev in sorted(revstofix):
176 fixctx = repo[rev]
206 fixctx = repo[rev]
177 match = scmutil.match(fixctx, pats, opts)
207 match = scmutil.match(fixctx, pats, opts)
178 for path in pathstofix(ui, repo, pats, opts, match, basectxs[rev],
208 for path in pathstofix(ui, repo, pats, opts, match, basectxs[rev],
179 fixctx):
209 fixctx):
180 if path not in fixctx:
210 if path not in fixctx:
181 continue
211 continue
182 fctx = fixctx[path]
212 fctx = fixctx[path]
183 if fctx.islink():
213 if fctx.islink():
184 continue
214 continue
185 if fctx.size() > maxfilesize:
215 if fctx.size() > maxfilesize:
186 ui.warn(_('ignoring file larger than %s: %s\n') %
216 ui.warn(_('ignoring file larger than %s: %s\n') %
187 (util.bytecount(maxfilesize), path))
217 (util.bytecount(maxfilesize), path))
188 continue
218 continue
189 workqueue.append((rev, path))
219 workqueue.append((rev, path))
190 numitems[rev] += 1
220 numitems[rev] += 1
191 return workqueue, numitems
221 return workqueue, numitems
192
222
193 def getrevstofix(ui, repo, opts):
223 def getrevstofix(ui, repo, opts):
194 """Returns the set of revision numbers that should be fixed"""
224 """Returns the set of revision numbers that should be fixed"""
195 revs = set(scmutil.revrange(repo, opts['rev']))
225 revs = set(scmutil.revrange(repo, opts['rev']))
196 for rev in revs:
226 for rev in revs:
197 checkfixablectx(ui, repo, repo[rev])
227 checkfixablectx(ui, repo, repo[rev])
198 if revs:
228 if revs:
199 cmdutil.checkunfinished(repo)
229 cmdutil.checkunfinished(repo)
200 checknodescendants(repo, revs)
230 checknodescendants(repo, revs)
201 if opts.get('working_dir'):
231 if opts.get('working_dir'):
202 revs.add(wdirrev)
232 revs.add(wdirrev)
203 if list(merge.mergestate.read(repo).unresolved()):
233 if list(merge.mergestate.read(repo).unresolved()):
204 raise error.Abort('unresolved conflicts', hint="use 'hg resolve'")
234 raise error.Abort('unresolved conflicts', hint="use 'hg resolve'")
205 if not revs:
235 if not revs:
206 raise error.Abort(
236 raise error.Abort(
207 'no changesets specified', hint='use --rev or --working-dir')
237 'no changesets specified', hint='use --rev or --working-dir')
208 return revs
238 return revs
209
239
210 def checknodescendants(repo, revs):
240 def checknodescendants(repo, revs):
211 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
241 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
212 repo.revs('(%ld::) - (%ld)', revs, revs)):
242 repo.revs('(%ld::) - (%ld)', revs, revs)):
213 raise error.Abort(_('can only fix a changeset together '
243 raise error.Abort(_('can only fix a changeset together '
214 'with all its descendants'))
244 'with all its descendants'))
215
245
216 def checkfixablectx(ui, repo, ctx):
246 def checkfixablectx(ui, repo, ctx):
217 """Aborts if the revision shouldn't be replaced with a fixed one."""
247 """Aborts if the revision shouldn't be replaced with a fixed one."""
218 if not ctx.mutable():
248 if not ctx.mutable():
219 raise error.Abort('can\'t fix immutable changeset %s' %
249 raise error.Abort('can\'t fix immutable changeset %s' %
220 (scmutil.formatchangeid(ctx),))
250 (scmutil.formatchangeid(ctx),))
221 if ctx.obsolete():
251 if ctx.obsolete():
222 # It would be better to actually check if the revision has a successor.
252 # It would be better to actually check if the revision has a successor.
223 allowdivergence = ui.configbool('experimental',
253 allowdivergence = ui.configbool('experimental',
224 'evolution.allowdivergence')
254 'evolution.allowdivergence')
225 if not allowdivergence:
255 if not allowdivergence:
226 raise error.Abort('fixing obsolete revision could cause divergence')
256 raise error.Abort('fixing obsolete revision could cause divergence')
227
257
228 def pathstofix(ui, repo, pats, opts, match, basectxs, fixctx):
258 def pathstofix(ui, repo, pats, opts, match, basectxs, fixctx):
229 """Returns the set of files that should be fixed in a context
259 """Returns the set of files that should be fixed in a context
230
260
231 The result depends on the base contexts; we include any file that has
261 The result depends on the base contexts; we include any file that has
232 changed relative to any of the base contexts. Base contexts should be
262 changed relative to any of the base contexts. Base contexts should be
233 ancestors of the context being fixed.
263 ancestors of the context being fixed.
234 """
264 """
235 files = set()
265 files = set()
236 for basectx in basectxs:
266 for basectx in basectxs:
237 stat = repo.status(
267 stat = repo.status(
238 basectx, fixctx, match=match, clean=bool(pats), unknown=bool(pats))
268 basectx, fixctx, match=match, clean=bool(pats), unknown=bool(pats))
239 files.update(
269 files.update(
240 set(itertools.chain(stat.added, stat.modified, stat.clean,
270 set(itertools.chain(stat.added, stat.modified, stat.clean,
241 stat.unknown)))
271 stat.unknown)))
242 return files
272 return files
243
273
244 def lineranges(opts, path, basectxs, fixctx, content2):
274 def lineranges(opts, path, basectxs, fixctx, content2):
245 """Returns the set of line ranges that should be fixed in a file
275 """Returns the set of line ranges that should be fixed in a file
246
276
247 Of the form [(10, 20), (30, 40)].
277 Of the form [(10, 20), (30, 40)].
248
278
249 This depends on the given base contexts; we must consider lines that have
279 This depends on the given base contexts; we must consider lines that have
250 changed versus any of the base contexts, and whether the file has been
280 changed versus any of the base contexts, and whether the file has been
251 renamed versus any of them.
281 renamed versus any of them.
252
282
253 Another way to understand this is that we exclude line ranges that are
283 Another way to understand this is that we exclude line ranges that are
254 common to the file in all base contexts.
284 common to the file in all base contexts.
255 """
285 """
256 if opts.get('whole'):
286 if opts.get('whole'):
257 # Return a range containing all lines. Rely on the diff implementation's
287 # Return a range containing all lines. Rely on the diff implementation's
258 # idea of how many lines are in the file, instead of reimplementing it.
288 # idea of how many lines are in the file, instead of reimplementing it.
259 return difflineranges('', content2)
289 return difflineranges('', content2)
260
290
261 rangeslist = []
291 rangeslist = []
262 for basectx in basectxs:
292 for basectx in basectxs:
263 basepath = copies.pathcopies(basectx, fixctx).get(path, path)
293 basepath = copies.pathcopies(basectx, fixctx).get(path, path)
264 if basepath in basectx:
294 if basepath in basectx:
265 content1 = basectx[basepath].data()
295 content1 = basectx[basepath].data()
266 else:
296 else:
267 content1 = ''
297 content1 = ''
268 rangeslist.extend(difflineranges(content1, content2))
298 rangeslist.extend(difflineranges(content1, content2))
269 return unionranges(rangeslist)
299 return unionranges(rangeslist)
270
300
271 def unionranges(rangeslist):
301 def unionranges(rangeslist):
272 """Return the union of some closed intervals
302 """Return the union of some closed intervals
273
303
274 >>> unionranges([])
304 >>> unionranges([])
275 []
305 []
276 >>> unionranges([(1, 100)])
306 >>> unionranges([(1, 100)])
277 [(1, 100)]
307 [(1, 100)]
278 >>> unionranges([(1, 100), (1, 100)])
308 >>> unionranges([(1, 100), (1, 100)])
279 [(1, 100)]
309 [(1, 100)]
280 >>> unionranges([(1, 100), (2, 100)])
310 >>> unionranges([(1, 100), (2, 100)])
281 [(1, 100)]
311 [(1, 100)]
282 >>> unionranges([(1, 99), (1, 100)])
312 >>> unionranges([(1, 99), (1, 100)])
283 [(1, 100)]
313 [(1, 100)]
284 >>> unionranges([(1, 100), (40, 60)])
314 >>> unionranges([(1, 100), (40, 60)])
285 [(1, 100)]
315 [(1, 100)]
286 >>> unionranges([(1, 49), (50, 100)])
316 >>> unionranges([(1, 49), (50, 100)])
287 [(1, 100)]
317 [(1, 100)]
288 >>> unionranges([(1, 48), (50, 100)])
318 >>> unionranges([(1, 48), (50, 100)])
289 [(1, 48), (50, 100)]
319 [(1, 48), (50, 100)]
290 >>> unionranges([(1, 2), (3, 4), (5, 6)])
320 >>> unionranges([(1, 2), (3, 4), (5, 6)])
291 [(1, 6)]
321 [(1, 6)]
292 """
322 """
293 rangeslist = sorted(set(rangeslist))
323 rangeslist = sorted(set(rangeslist))
294 unioned = []
324 unioned = []
295 if rangeslist:
325 if rangeslist:
296 unioned, rangeslist = [rangeslist[0]], rangeslist[1:]
326 unioned, rangeslist = [rangeslist[0]], rangeslist[1:]
297 for a, b in rangeslist:
327 for a, b in rangeslist:
298 c, d = unioned[-1]
328 c, d = unioned[-1]
299 if a > d + 1:
329 if a > d + 1:
300 unioned.append((a, b))
330 unioned.append((a, b))
301 else:
331 else:
302 unioned[-1] = (c, max(b, d))
332 unioned[-1] = (c, max(b, d))
303 return unioned
333 return unioned
304
334
305 def difflineranges(content1, content2):
335 def difflineranges(content1, content2):
306 """Return list of line number ranges in content2 that differ from content1.
336 """Return list of line number ranges in content2 that differ from content1.
307
337
308 Line numbers are 1-based. The numbers are the first and last line contained
338 Line numbers are 1-based. The numbers are the first and last line contained
309 in the range. Single-line ranges have the same line number for the first and
339 in the range. Single-line ranges have the same line number for the first and
310 last line. Excludes any empty ranges that result from lines that are only
340 last line. Excludes any empty ranges that result from lines that are only
311 present in content1. Relies on mdiff's idea of where the line endings are in
341 present in content1. Relies on mdiff's idea of where the line endings are in
312 the string.
342 the string.
313
343
314 >>> from mercurial import pycompat
344 >>> from mercurial import pycompat
315 >>> lines = lambda s: b'\\n'.join([c for c in pycompat.iterbytestr(s)])
345 >>> lines = lambda s: b'\\n'.join([c for c in pycompat.iterbytestr(s)])
316 >>> difflineranges2 = lambda a, b: difflineranges(lines(a), lines(b))
346 >>> difflineranges2 = lambda a, b: difflineranges(lines(a), lines(b))
317 >>> difflineranges2(b'', b'')
347 >>> difflineranges2(b'', b'')
318 []
348 []
319 >>> difflineranges2(b'a', b'')
349 >>> difflineranges2(b'a', b'')
320 []
350 []
321 >>> difflineranges2(b'', b'A')
351 >>> difflineranges2(b'', b'A')
322 [(1, 1)]
352 [(1, 1)]
323 >>> difflineranges2(b'a', b'a')
353 >>> difflineranges2(b'a', b'a')
324 []
354 []
325 >>> difflineranges2(b'a', b'A')
355 >>> difflineranges2(b'a', b'A')
326 [(1, 1)]
356 [(1, 1)]
327 >>> difflineranges2(b'ab', b'')
357 >>> difflineranges2(b'ab', b'')
328 []
358 []
329 >>> difflineranges2(b'', b'AB')
359 >>> difflineranges2(b'', b'AB')
330 [(1, 2)]
360 [(1, 2)]
331 >>> difflineranges2(b'abc', b'ac')
361 >>> difflineranges2(b'abc', b'ac')
332 []
362 []
333 >>> difflineranges2(b'ab', b'aCb')
363 >>> difflineranges2(b'ab', b'aCb')
334 [(2, 2)]
364 [(2, 2)]
335 >>> difflineranges2(b'abc', b'aBc')
365 >>> difflineranges2(b'abc', b'aBc')
336 [(2, 2)]
366 [(2, 2)]
337 >>> difflineranges2(b'ab', b'AB')
367 >>> difflineranges2(b'ab', b'AB')
338 [(1, 2)]
368 [(1, 2)]
339 >>> difflineranges2(b'abcde', b'aBcDe')
369 >>> difflineranges2(b'abcde', b'aBcDe')
340 [(2, 2), (4, 4)]
370 [(2, 2), (4, 4)]
341 >>> difflineranges2(b'abcde', b'aBCDe')
371 >>> difflineranges2(b'abcde', b'aBCDe')
342 [(2, 4)]
372 [(2, 4)]
343 """
373 """
344 ranges = []
374 ranges = []
345 for lines, kind in mdiff.allblocks(content1, content2):
375 for lines, kind in mdiff.allblocks(content1, content2):
346 firstline, lastline = lines[2:4]
376 firstline, lastline = lines[2:4]
347 if kind == '!' and firstline != lastline:
377 if kind == '!' and firstline != lastline:
348 ranges.append((firstline + 1, lastline))
378 ranges.append((firstline + 1, lastline))
349 return ranges
379 return ranges
350
380
351 def getbasectxs(repo, opts, revstofix):
381 def getbasectxs(repo, opts, revstofix):
352 """Returns a map of the base contexts for each revision
382 """Returns a map of the base contexts for each revision
353
383
354 The base contexts determine which lines are considered modified when we
384 The base contexts determine which lines are considered modified when we
355 attempt to fix just the modified lines in a file.
385 attempt to fix just the modified lines in a file.
356 """
386 """
357 # The --base flag overrides the usual logic, and we give every revision
387 # The --base flag overrides the usual logic, and we give every revision
358 # exactly the set of baserevs that the user specified.
388 # exactly the set of baserevs that the user specified.
359 if opts.get('base'):
389 if opts.get('base'):
360 baserevs = set(scmutil.revrange(repo, opts.get('base')))
390 baserevs = set(scmutil.revrange(repo, opts.get('base')))
361 if not baserevs:
391 if not baserevs:
362 baserevs = {nullrev}
392 baserevs = {nullrev}
363 basectxs = {repo[rev] for rev in baserevs}
393 basectxs = {repo[rev] for rev in baserevs}
364 return {rev: basectxs for rev in revstofix}
394 return {rev: basectxs for rev in revstofix}
365
395
366 # Proceed in topological order so that we can easily determine each
396 # Proceed in topological order so that we can easily determine each
367 # revision's baserevs by looking at its parents and their baserevs.
397 # revision's baserevs by looking at its parents and their baserevs.
368 basectxs = collections.defaultdict(set)
398 basectxs = collections.defaultdict(set)
369 for rev in sorted(revstofix):
399 for rev in sorted(revstofix):
370 ctx = repo[rev]
400 ctx = repo[rev]
371 for pctx in ctx.parents():
401 for pctx in ctx.parents():
372 if pctx.rev() in basectxs:
402 if pctx.rev() in basectxs:
373 basectxs[rev].update(basectxs[pctx.rev()])
403 basectxs[rev].update(basectxs[pctx.rev()])
374 else:
404 else:
375 basectxs[rev].add(pctx)
405 basectxs[rev].add(pctx)
376 return basectxs
406 return basectxs
377
407
378 def fixfile(ui, opts, fixers, fixctx, path, basectxs):
408 def fixfile(ui, opts, fixers, fixctx, path, basectxs):
379 """Run any configured fixers that should affect the file in this context
409 """Run any configured fixers that should affect the file in this context
380
410
381 Returns the file content that results from applying the fixers in some order
411 Returns the file content that results from applying the fixers in some order
382 starting with the file's content in the fixctx. Fixers that support line
412 starting with the file's content in the fixctx. Fixers that support line
383 ranges will affect lines that have changed relative to any of the basectxs
413 ranges will affect lines that have changed relative to any of the basectxs
384 (i.e. they will only avoid lines that are common to all basectxs).
414 (i.e. they will only avoid lines that are common to all basectxs).
385 """
415 """
386 newdata = fixctx[path].data()
416 newdata = fixctx[path].data()
387 for fixername, fixer in fixers.iteritems():
417 for fixername, fixer in fixers.iteritems():
388 if fixer.affects(opts, fixctx, path):
418 if fixer.affects(opts, fixctx, path):
389 ranges = lineranges(opts, path, basectxs, fixctx, newdata)
419 ranges = lineranges(opts, path, basectxs, fixctx, newdata)
390 command = fixer.command(ui, path, ranges)
420 command = fixer.command(ui, path, ranges)
391 if command is None:
421 if command is None:
392 continue
422 continue
393 ui.debug('subprocess: %s\n' % (command,))
423 ui.debug('subprocess: %s\n' % (command,))
394 proc = subprocess.Popen(
424 proc = subprocess.Popen(
395 command,
425 command,
396 shell=True,
426 shell=True,
397 cwd='/',
427 cwd='/',
398 stdin=subprocess.PIPE,
428 stdin=subprocess.PIPE,
399 stdout=subprocess.PIPE,
429 stdout=subprocess.PIPE,
400 stderr=subprocess.PIPE)
430 stderr=subprocess.PIPE)
401 newerdata, stderr = proc.communicate(newdata)
431 newerdata, stderr = proc.communicate(newdata)
402 if stderr:
432 if stderr:
403 showstderr(ui, fixctx.rev(), fixername, stderr)
433 showstderr(ui, fixctx.rev(), fixername, stderr)
404 else:
434 else:
405 newdata = newerdata
435 newdata = newerdata
406 return newdata
436 return newdata
407
437
408 def showstderr(ui, rev, fixername, stderr):
438 def showstderr(ui, rev, fixername, stderr):
409 """Writes the lines of the stderr string as warnings on the ui
439 """Writes the lines of the stderr string as warnings on the ui
410
440
411 Uses the revision number and fixername to give more context to each line of
441 Uses the revision number and fixername to give more context to each line of
412 the error message. Doesn't include file names, since those take up a lot of
442 the error message. Doesn't include file names, since those take up a lot of
413 space and would tend to be included in the error message if they were
443 space and would tend to be included in the error message if they were
414 relevant.
444 relevant.
415 """
445 """
416 for line in re.split('[\r\n]+', stderr):
446 for line in re.split('[\r\n]+', stderr):
417 if line:
447 if line:
418 ui.warn(('['))
448 ui.warn(('['))
419 if rev is None:
449 if rev is None:
420 ui.warn(_('wdir'), label='evolve.rev')
450 ui.warn(_('wdir'), label='evolve.rev')
421 else:
451 else:
422 ui.warn((str(rev)), label='evolve.rev')
452 ui.warn((str(rev)), label='evolve.rev')
423 ui.warn(('] %s: %s\n') % (fixername, line))
453 ui.warn(('] %s: %s\n') % (fixername, line))
424
454
425 def writeworkingdir(repo, ctx, filedata, replacements):
455 def writeworkingdir(repo, ctx, filedata, replacements):
426 """Write new content to the working copy and check out the new p1 if any
456 """Write new content to the working copy and check out the new p1 if any
427
457
428 We check out a new revision if and only if we fixed something in both the
458 We check out a new revision if and only if we fixed something in both the
429 working directory and its parent revision. This avoids the need for a full
459 working directory and its parent revision. This avoids the need for a full
430 update/merge, and means that the working directory simply isn't affected
460 update/merge, and means that the working directory simply isn't affected
431 unless the --working-dir flag is given.
461 unless the --working-dir flag is given.
432
462
433 Directly updates the dirstate for the affected files.
463 Directly updates the dirstate for the affected files.
434 """
464 """
435 for path, data in filedata.iteritems():
465 for path, data in filedata.iteritems():
436 fctx = ctx[path]
466 fctx = ctx[path]
437 fctx.write(data, fctx.flags())
467 fctx.write(data, fctx.flags())
438 if repo.dirstate[path] == 'n':
468 if repo.dirstate[path] == 'n':
439 repo.dirstate.normallookup(path)
469 repo.dirstate.normallookup(path)
440
470
441 oldparentnodes = repo.dirstate.parents()
471 oldparentnodes = repo.dirstate.parents()
442 newparentnodes = [replacements.get(n, n) for n in oldparentnodes]
472 newparentnodes = [replacements.get(n, n) for n in oldparentnodes]
443 if newparentnodes != oldparentnodes:
473 if newparentnodes != oldparentnodes:
444 repo.setparents(*newparentnodes)
474 repo.setparents(*newparentnodes)
445
475
446 def replacerev(ui, repo, ctx, filedata, replacements):
476 def replacerev(ui, repo, ctx, filedata, replacements):
447 """Commit a new revision like the given one, but with file content changes
477 """Commit a new revision like the given one, but with file content changes
448
478
449 "ctx" is the original revision to be replaced by a modified one.
479 "ctx" is the original revision to be replaced by a modified one.
450
480
451 "filedata" is a dict that maps paths to their new file content. All other
481 "filedata" is a dict that maps paths to their new file content. All other
452 paths will be recreated from the original revision without changes.
482 paths will be recreated from the original revision without changes.
453 "filedata" may contain paths that didn't exist in the original revision;
483 "filedata" may contain paths that didn't exist in the original revision;
454 they will be added.
484 they will be added.
455
485
456 "replacements" is a dict that maps a single node to a single node, and it is
486 "replacements" is a dict that maps a single node to a single node, and it is
457 updated to indicate the original revision is replaced by the newly created
487 updated to indicate the original revision is replaced by the newly created
458 one. No entry is added if the replacement's node already exists.
488 one. No entry is added if the replacement's node already exists.
459
489
460 The new revision has the same parents as the old one, unless those parents
490 The new revision has the same parents as the old one, unless those parents
461 have already been replaced, in which case those replacements are the parents
491 have already been replaced, in which case those replacements are the parents
462 of this new revision. Thus, if revisions are replaced in topological order,
492 of this new revision. Thus, if revisions are replaced in topological order,
463 there is no need to rebase them into the original topology later.
493 there is no need to rebase them into the original topology later.
464 """
494 """
465
495
466 p1rev, p2rev = repo.changelog.parentrevs(ctx.rev())
496 p1rev, p2rev = repo.changelog.parentrevs(ctx.rev())
467 p1ctx, p2ctx = repo[p1rev], repo[p2rev]
497 p1ctx, p2ctx = repo[p1rev], repo[p2rev]
468 newp1node = replacements.get(p1ctx.node(), p1ctx.node())
498 newp1node = replacements.get(p1ctx.node(), p1ctx.node())
469 newp2node = replacements.get(p2ctx.node(), p2ctx.node())
499 newp2node = replacements.get(p2ctx.node(), p2ctx.node())
470
500
471 def filectxfn(repo, memctx, path):
501 def filectxfn(repo, memctx, path):
472 if path not in ctx:
502 if path not in ctx:
473 return None
503 return None
474 fctx = ctx[path]
504 fctx = ctx[path]
475 copied = fctx.renamed()
505 copied = fctx.renamed()
476 if copied:
506 if copied:
477 copied = copied[0]
507 copied = copied[0]
478 return context.memfilectx(
508 return context.memfilectx(
479 repo,
509 repo,
480 memctx,
510 memctx,
481 path=fctx.path(),
511 path=fctx.path(),
482 data=filedata.get(path, fctx.data()),
512 data=filedata.get(path, fctx.data()),
483 islink=fctx.islink(),
513 islink=fctx.islink(),
484 isexec=fctx.isexec(),
514 isexec=fctx.isexec(),
485 copied=copied)
515 copied=copied)
486
516
487 memctx = context.memctx(
517 memctx = context.memctx(
488 repo,
518 repo,
489 parents=(newp1node, newp2node),
519 parents=(newp1node, newp2node),
490 text=ctx.description(),
520 text=ctx.description(),
491 files=set(ctx.files()) | set(filedata.keys()),
521 files=set(ctx.files()) | set(filedata.keys()),
492 filectxfn=filectxfn,
522 filectxfn=filectxfn,
493 user=ctx.user(),
523 user=ctx.user(),
494 date=ctx.date(),
524 date=ctx.date(),
495 extra=ctx.extra(),
525 extra=ctx.extra(),
496 branch=ctx.branch(),
526 branch=ctx.branch(),
497 editor=None)
527 editor=None)
498 sucnode = memctx.commit()
528 sucnode = memctx.commit()
499 prenode = ctx.node()
529 prenode = ctx.node()
500 if prenode == sucnode:
530 if prenode == sucnode:
501 ui.debug('node %s already existed\n' % (ctx.hex()))
531 ui.debug('node %s already existed\n' % (ctx.hex()))
502 else:
532 else:
503 replacements[ctx.node()] = sucnode
533 replacements[ctx.node()] = sucnode
504
534
505 def getfixers(ui):
535 def getfixers(ui):
506 """Returns a map of configured fixer tools indexed by their names
536 """Returns a map of configured fixer tools indexed by their names
507
537
508 Each value is a Fixer object with methods that implement the behavior of the
538 Each value is a Fixer object with methods that implement the behavior of the
509 fixer's config suboptions. Does not validate the config values.
539 fixer's config suboptions. Does not validate the config values.
510 """
540 """
511 result = {}
541 result = {}
512 for name in fixernames(ui):
542 for name in fixernames(ui):
513 result[name] = Fixer()
543 result[name] = Fixer()
514 attrs = ui.configsuboptions('fix', name)[1]
544 attrs = ui.configsuboptions('fix', name)[1]
515 for key in FIXER_ATTRS:
545 for key in FIXER_ATTRS:
516 setattr(result[name], pycompat.sysstr('_' + key),
546 setattr(result[name], pycompat.sysstr('_' + key),
517 attrs.get(key, ''))
547 attrs.get(key, ''))
518 return result
548 return result
519
549
520 def fixernames(ui):
550 def fixernames(ui):
521 """Returns the names of [fix] config options that have suboptions"""
551 """Returns the names of [fix] config options that have suboptions"""
522 names = set()
552 names = set()
523 for k, v in ui.configitems('fix'):
553 for k, v in ui.configitems('fix'):
524 if ':' in k:
554 if ':' in k:
525 names.add(k.split(':', 1)[0])
555 names.add(k.split(':', 1)[0])
526 return names
556 return names
527
557
528 class Fixer(object):
558 class Fixer(object):
529 """Wraps the raw config values for a fixer with methods"""
559 """Wraps the raw config values for a fixer with methods"""
530
560
531 def affects(self, opts, fixctx, path):
561 def affects(self, opts, fixctx, path):
532 """Should this fixer run on the file at the given path and context?"""
562 """Should this fixer run on the file at the given path and context?"""
533 return scmutil.match(fixctx, [self._fileset], opts)(path)
563 return scmutil.match(fixctx, [self._fileset], opts)(path)
534
564
535 def command(self, ui, path, ranges):
565 def command(self, ui, path, ranges):
536 """A shell command to use to invoke this fixer on the given file/lines
566 """A shell command to use to invoke this fixer on the given file/lines
537
567
538 May return None if there is no appropriate command to run for the given
568 May return None if there is no appropriate command to run for the given
539 parameters.
569 parameters.
540 """
570 """
541 expand = cmdutil.rendercommandtemplate
571 expand = cmdutil.rendercommandtemplate
542 parts = [expand(ui, self._command,
572 parts = [expand(ui, self._command,
543 {'rootpath': path, 'basename': os.path.basename(path)})]
573 {'rootpath': path, 'basename': os.path.basename(path)})]
544 if self._linerange:
574 if self._linerange:
545 if not ranges:
575 if not ranges:
546 # No line ranges to fix, so don't run the fixer.
576 # No line ranges to fix, so don't run the fixer.
547 return None
577 return None
548 for first, last in ranges:
578 for first, last in ranges:
549 parts.append(expand(ui, self._linerange,
579 parts.append(expand(ui, self._linerange,
550 {'first': first, 'last': last}))
580 {'first': first, 'last': last}))
551 return ' '.join(parts)
581 return ' '.join(parts)
General Comments 0
You need to be logged in to leave comments. Login now