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