##// END OF EJS Templates
fix: use cmdutil.check_at_most_one_arg()...
Martin von Zweigbergk -
r44349:dda49ec2 default
parent child Browse files
Show More
@@ -1,882 +1,881 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:pattern=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. Any output on standard error
23 23 will be displayed as a warning. If the exit status is not zero, the file will
24 24 not be affected. A placeholder warning is displayed if there is a non-zero exit
25 25 status but no standard error output. Some values may be substituted into the
26 26 command::
27 27
28 28 {rootpath} The path of the file being fixed, relative to the repo root
29 29 {basename} The name of the file being fixed, without the directory path
30 30
31 31 If the :linerange suboption is set, the tool will only be run if there are
32 32 changed lines in a file. The value of this suboption is appended to the shell
33 33 command once for every range of changed lines in the file. Some values may be
34 34 substituted into the command::
35 35
36 36 {first} The 1-based line number of the first line in the modified range
37 37 {last} The 1-based line number of the last line in the modified range
38 38
39 39 Deleted sections of a file will be ignored by :linerange, because there is no
40 40 corresponding line range in the version being fixed.
41 41
42 42 By default, tools that set :linerange will only be executed if there is at least
43 43 one changed line range. This is meant to prevent accidents like running a code
44 44 formatter in such a way that it unexpectedly reformats the whole file. If such a
45 45 tool needs to operate on unchanged files, it should set the :skipclean suboption
46 46 to false.
47 47
48 48 The :pattern suboption determines which files will be passed through each
49 49 configured tool. See :hg:`help patterns` for possible values. However, all
50 50 patterns are relative to the repo root, even if that text says they are relative
51 51 to the current working directory. If there are file arguments to :hg:`fix`, the
52 52 intersection of these patterns is used.
53 53
54 54 There is also a configurable limit for the maximum size of file that will be
55 55 processed by :hg:`fix`::
56 56
57 57 [fix]
58 58 maxfilesize = 2MB
59 59
60 60 Normally, execution of configured tools will continue after a failure (indicated
61 61 by a non-zero exit status). It can also be configured to abort after the first
62 62 such failure, so that no files will be affected if any tool fails. This abort
63 63 will also cause :hg:`fix` to exit with a non-zero status::
64 64
65 65 [fix]
66 66 failure = abort
67 67
68 68 When multiple tools are configured to affect a file, they execute in an order
69 69 defined by the :priority suboption. The priority suboption has a default value
70 70 of zero for each tool. Tools are executed in order of descending priority. The
71 71 execution order of tools with equal priority is unspecified. For example, you
72 72 could use the 'sort' and 'head' utilities to keep only the 10 smallest numbers
73 73 in a text file by ensuring that 'sort' runs before 'head'::
74 74
75 75 [fix]
76 76 sort:command = sort -n
77 77 head:command = head -n 10
78 78 sort:pattern = numbers.txt
79 79 head:pattern = numbers.txt
80 80 sort:priority = 2
81 81 head:priority = 1
82 82
83 83 To account for changes made by each tool, the line numbers used for incremental
84 84 formatting are recomputed before executing the next tool. So, each tool may see
85 85 different values for the arguments added by the :linerange suboption.
86 86
87 87 Each fixer tool is allowed to return some metadata in addition to the fixed file
88 88 content. The metadata must be placed before the file content on stdout,
89 89 separated from the file content by a zero byte. The metadata is parsed as a JSON
90 90 value (so, it should be UTF-8 encoded and contain no zero bytes). A fixer tool
91 91 is expected to produce this metadata encoding if and only if the :metadata
92 92 suboption is true::
93 93
94 94 [fix]
95 95 tool:command = tool --prepend-json-metadata
96 96 tool:metadata = true
97 97
98 98 The metadata values are passed to hooks, which can be used to print summaries or
99 99 perform other post-fixing work. The supported hooks are::
100 100
101 101 "postfixfile"
102 102 Run once for each file in each revision where any fixer tools made changes
103 103 to the file content. Provides "$HG_REV" and "$HG_PATH" to identify the file,
104 104 and "$HG_METADATA" with a map of fixer names to metadata values from fixer
105 105 tools that affected the file. Fixer tools that didn't affect the file have a
106 106 valueof None. Only fixer tools that executed are present in the metadata.
107 107
108 108 "postfix"
109 109 Run once after all files and revisions have been handled. Provides
110 110 "$HG_REPLACEMENTS" with information about what revisions were created and
111 111 made obsolete. Provides a boolean "$HG_WDIRWRITTEN" to indicate whether any
112 112 files in the working copy were updated. Provides a list "$HG_METADATA"
113 113 mapping fixer tool names to lists of metadata values returned from
114 114 executions that modified a file. This aggregates the same metadata
115 115 previously passed to the "postfixfile" hook.
116 116
117 117 Fixer tools are run the in repository's root directory. This allows them to read
118 118 configuration files from the working copy, or even write to the working copy.
119 119 The working copy is not updated to match the revision being fixed. In fact,
120 120 several revisions may be fixed in parallel. Writes to the working copy are not
121 121 amended into the revision being fixed; fixer tools should always write fixed
122 122 file content back to stdout as documented above.
123 123 """
124 124
125 125 from __future__ import absolute_import
126 126
127 127 import collections
128 128 import itertools
129 129 import os
130 130 import re
131 131 import subprocess
132 132
133 133 from mercurial.i18n import _
134 134 from mercurial.node import nullrev
135 135 from mercurial.node import wdirrev
136 136
137 137 from mercurial.utils import procutil
138 138
139 139 from mercurial import (
140 140 cmdutil,
141 141 context,
142 142 copies,
143 143 error,
144 144 match as matchmod,
145 145 mdiff,
146 146 merge,
147 147 obsolete,
148 148 pycompat,
149 149 registrar,
150 150 scmutil,
151 151 util,
152 152 worker,
153 153 )
154 154
155 155 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
156 156 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
157 157 # be specifying the version(s) of Mercurial they are tested with, or
158 158 # leave the attribute unspecified.
159 159 testedwith = b'ships-with-hg-core'
160 160
161 161 cmdtable = {}
162 162 command = registrar.command(cmdtable)
163 163
164 164 configtable = {}
165 165 configitem = registrar.configitem(configtable)
166 166
167 167 # Register the suboptions allowed for each configured fixer, and default values.
168 168 FIXER_ATTRS = {
169 169 b'command': None,
170 170 b'linerange': None,
171 171 b'pattern': None,
172 172 b'priority': 0,
173 173 b'metadata': False,
174 174 b'skipclean': True,
175 175 b'enabled': True,
176 176 }
177 177
178 178 for key, default in FIXER_ATTRS.items():
179 179 configitem(b'fix', b'.*:%s$' % key, default=default, generic=True)
180 180
181 181 # A good default size allows most source code files to be fixed, but avoids
182 182 # letting fixer tools choke on huge inputs, which could be surprising to the
183 183 # user.
184 184 configitem(b'fix', b'maxfilesize', default=b'2MB')
185 185
186 186 # Allow fix commands to exit non-zero if an executed fixer tool exits non-zero.
187 187 # This helps users do shell scripts that stop when a fixer tool signals a
188 188 # problem.
189 189 configitem(b'fix', b'failure', default=b'continue')
190 190
191 191
192 192 def checktoolfailureaction(ui, message, hint=None):
193 193 """Abort with 'message' if fix.failure=abort"""
194 194 action = ui.config(b'fix', b'failure')
195 195 if action not in (b'continue', b'abort'):
196 196 raise error.Abort(
197 197 _(b'unknown fix.failure action: %s') % (action,),
198 198 hint=_(b'use "continue" or "abort"'),
199 199 )
200 200 if action == b'abort':
201 201 raise error.Abort(message, hint=hint)
202 202
203 203
204 204 allopt = (b'', b'all', False, _(b'fix all non-public non-obsolete revisions'))
205 205 baseopt = (
206 206 b'',
207 207 b'base',
208 208 [],
209 209 _(
210 210 b'revisions to diff against (overrides automatic '
211 211 b'selection, and applies to every revision being '
212 212 b'fixed)'
213 213 ),
214 214 _(b'REV'),
215 215 )
216 216 revopt = (b'r', b'rev', [], _(b'revisions to fix'), _(b'REV'))
217 217 wdiropt = (b'w', b'working-dir', False, _(b'fix the working directory'))
218 218 wholeopt = (b'', b'whole', False, _(b'always fix every line of a file'))
219 219 usage = _(b'[OPTION]... [FILE]...')
220 220
221 221
222 222 @command(
223 223 b'fix',
224 224 [allopt, baseopt, revopt, wdiropt, wholeopt],
225 225 usage,
226 226 helpcategory=command.CATEGORY_FILE_CONTENTS,
227 227 )
228 228 def fix(ui, repo, *pats, **opts):
229 229 """rewrite file content in changesets or working directory
230 230
231 231 Runs any configured tools to fix the content of files. Only affects files
232 232 with changes, unless file arguments are provided. Only affects changed lines
233 233 of files, unless the --whole flag is used. Some tools may always affect the
234 234 whole file regardless of --whole.
235 235
236 236 If revisions are specified with --rev, those revisions will be checked, and
237 237 they may be replaced with new revisions that have fixed file content. It is
238 238 desirable to specify all descendants of each specified revision, so that the
239 239 fixes propagate to the descendants. If all descendants are fixed at the same
240 240 time, no merging, rebasing, or evolution will be required.
241 241
242 242 If --working-dir is used, files with uncommitted changes in the working copy
243 243 will be fixed. If the checked-out revision is also fixed, the working
244 244 directory will update to the replacement revision.
245 245
246 246 When determining what lines of each file to fix at each revision, the whole
247 247 set of revisions being fixed is considered, so that fixes to earlier
248 248 revisions are not forgotten in later ones. The --base flag can be used to
249 249 override this default behavior, though it is not usually desirable to do so.
250 250 """
251 251 opts = pycompat.byteskwargs(opts)
252 cmdutil.check_at_most_one_arg(opts, b'all', b'rev')
252 253 if opts[b'all']:
253 if opts[b'rev']:
254 raise error.Abort(_(b'cannot specify both "--rev" and "--all"'))
255 254 opts[b'rev'] = [b'not public() and not obsolete()']
256 255 opts[b'working_dir'] = True
257 256 with repo.wlock(), repo.lock(), repo.transaction(b'fix'):
258 257 revstofix = getrevstofix(ui, repo, opts)
259 258 basectxs = getbasectxs(repo, opts, revstofix)
260 259 workqueue, numitems = getworkqueue(
261 260 ui, repo, pats, opts, revstofix, basectxs
262 261 )
263 262 fixers = getfixers(ui)
264 263
265 264 # There are no data dependencies between the workers fixing each file
266 265 # revision, so we can use all available parallelism.
267 266 def getfixes(items):
268 267 for rev, path in items:
269 268 ctx = repo[rev]
270 269 olddata = ctx[path].data()
271 270 metadata, newdata = fixfile(
272 271 ui, repo, opts, fixers, ctx, path, basectxs[rev]
273 272 )
274 273 # Don't waste memory/time passing unchanged content back, but
275 274 # produce one result per item either way.
276 275 yield (
277 276 rev,
278 277 path,
279 278 metadata,
280 279 newdata if newdata != olddata else None,
281 280 )
282 281
283 282 results = worker.worker(
284 283 ui, 1.0, getfixes, tuple(), workqueue, threadsafe=False
285 284 )
286 285
287 286 # We have to hold on to the data for each successor revision in memory
288 287 # until all its parents are committed. We ensure this by committing and
289 288 # freeing memory for the revisions in some topological order. This
290 289 # leaves a little bit of memory efficiency on the table, but also makes
291 290 # the tests deterministic. It might also be considered a feature since
292 291 # it makes the results more easily reproducible.
293 292 filedata = collections.defaultdict(dict)
294 293 aggregatemetadata = collections.defaultdict(list)
295 294 replacements = {}
296 295 wdirwritten = False
297 296 commitorder = sorted(revstofix, reverse=True)
298 297 with ui.makeprogress(
299 298 topic=_(b'fixing'), unit=_(b'files'), total=sum(numitems.values())
300 299 ) as progress:
301 300 for rev, path, filerevmetadata, newdata in results:
302 301 progress.increment(item=path)
303 302 for fixername, fixermetadata in filerevmetadata.items():
304 303 aggregatemetadata[fixername].append(fixermetadata)
305 304 if newdata is not None:
306 305 filedata[rev][path] = newdata
307 306 hookargs = {
308 307 b'rev': rev,
309 308 b'path': path,
310 309 b'metadata': filerevmetadata,
311 310 }
312 311 repo.hook(
313 312 b'postfixfile',
314 313 throw=False,
315 314 **pycompat.strkwargs(hookargs)
316 315 )
317 316 numitems[rev] -= 1
318 317 # Apply the fixes for this and any other revisions that are
319 318 # ready and sitting at the front of the queue. Using a loop here
320 319 # prevents the queue from being blocked by the first revision to
321 320 # be ready out of order.
322 321 while commitorder and not numitems[commitorder[-1]]:
323 322 rev = commitorder.pop()
324 323 ctx = repo[rev]
325 324 if rev == wdirrev:
326 325 writeworkingdir(repo, ctx, filedata[rev], replacements)
327 326 wdirwritten = bool(filedata[rev])
328 327 else:
329 328 replacerev(ui, repo, ctx, filedata[rev], replacements)
330 329 del filedata[rev]
331 330
332 331 cleanup(repo, replacements, wdirwritten)
333 332 hookargs = {
334 333 b'replacements': replacements,
335 334 b'wdirwritten': wdirwritten,
336 335 b'metadata': aggregatemetadata,
337 336 }
338 337 repo.hook(b'postfix', throw=True, **pycompat.strkwargs(hookargs))
339 338
340 339
341 340 def cleanup(repo, replacements, wdirwritten):
342 341 """Calls scmutil.cleanupnodes() with the given replacements.
343 342
344 343 "replacements" is a dict from nodeid to nodeid, with one key and one value
345 344 for every revision that was affected by fixing. This is slightly different
346 345 from cleanupnodes().
347 346
348 347 "wdirwritten" is a bool which tells whether the working copy was affected by
349 348 fixing, since it has no entry in "replacements".
350 349
351 350 Useful as a hook point for extending "hg fix" with output summarizing the
352 351 effects of the command, though we choose not to output anything here.
353 352 """
354 353 replacements = {
355 354 prec: [succ] for prec, succ in pycompat.iteritems(replacements)
356 355 }
357 356 scmutil.cleanupnodes(repo, replacements, b'fix', fixphase=True)
358 357
359 358
360 359 def getworkqueue(ui, repo, pats, opts, revstofix, basectxs):
361 360 """"Constructs the list of files to be fixed at specific revisions
362 361
363 362 It is up to the caller how to consume the work items, and the only
364 363 dependence between them is that replacement revisions must be committed in
365 364 topological order. Each work item represents a file in the working copy or
366 365 in some revision that should be fixed and written back to the working copy
367 366 or into a replacement revision.
368 367
369 368 Work items for the same revision are grouped together, so that a worker
370 369 pool starting with the first N items in parallel is likely to finish the
371 370 first revision's work before other revisions. This can allow us to write
372 371 the result to disk and reduce memory footprint. At time of writing, the
373 372 partition strategy in worker.py seems favorable to this. We also sort the
374 373 items by ascending revision number to match the order in which we commit
375 374 the fixes later.
376 375 """
377 376 workqueue = []
378 377 numitems = collections.defaultdict(int)
379 378 maxfilesize = ui.configbytes(b'fix', b'maxfilesize')
380 379 for rev in sorted(revstofix):
381 380 fixctx = repo[rev]
382 381 match = scmutil.match(fixctx, pats, opts)
383 382 for path in sorted(
384 383 pathstofix(ui, repo, pats, opts, match, basectxs[rev], fixctx)
385 384 ):
386 385 fctx = fixctx[path]
387 386 if fctx.islink():
388 387 continue
389 388 if fctx.size() > maxfilesize:
390 389 ui.warn(
391 390 _(b'ignoring file larger than %s: %s\n')
392 391 % (util.bytecount(maxfilesize), path)
393 392 )
394 393 continue
395 394 workqueue.append((rev, path))
396 395 numitems[rev] += 1
397 396 return workqueue, numitems
398 397
399 398
400 399 def getrevstofix(ui, repo, opts):
401 400 """Returns the set of revision numbers that should be fixed"""
402 401 revs = set(scmutil.revrange(repo, opts[b'rev']))
403 402 for rev in revs:
404 403 checkfixablectx(ui, repo, repo[rev])
405 404 if revs:
406 405 cmdutil.checkunfinished(repo)
407 406 checknodescendants(repo, revs)
408 407 if opts.get(b'working_dir'):
409 408 revs.add(wdirrev)
410 409 if list(merge.mergestate.read(repo).unresolved()):
411 410 raise error.Abort(b'unresolved conflicts', hint=b"use 'hg resolve'")
412 411 if not revs:
413 412 raise error.Abort(
414 413 b'no changesets specified', hint=b'use --rev or --working-dir'
415 414 )
416 415 return revs
417 416
418 417
419 418 def checknodescendants(repo, revs):
420 419 if not obsolete.isenabled(repo, obsolete.allowunstableopt) and repo.revs(
421 420 b'(%ld::) - (%ld)', revs, revs
422 421 ):
423 422 raise error.Abort(
424 423 _(b'can only fix a changeset together with all its descendants')
425 424 )
426 425
427 426
428 427 def checkfixablectx(ui, repo, ctx):
429 428 """Aborts if the revision shouldn't be replaced with a fixed one."""
430 429 if not ctx.mutable():
431 430 raise error.Abort(
432 431 b'can\'t fix immutable changeset %s'
433 432 % (scmutil.formatchangeid(ctx),)
434 433 )
435 434 if ctx.obsolete():
436 435 # It would be better to actually check if the revision has a successor.
437 436 allowdivergence = ui.configbool(
438 437 b'experimental', b'evolution.allowdivergence'
439 438 )
440 439 if not allowdivergence:
441 440 raise error.Abort(
442 441 b'fixing obsolete revision could cause divergence'
443 442 )
444 443
445 444
446 445 def pathstofix(ui, repo, pats, opts, match, basectxs, fixctx):
447 446 """Returns the set of files that should be fixed in a context
448 447
449 448 The result depends on the base contexts; we include any file that has
450 449 changed relative to any of the base contexts. Base contexts should be
451 450 ancestors of the context being fixed.
452 451 """
453 452 files = set()
454 453 for basectx in basectxs:
455 454 stat = basectx.status(
456 455 fixctx, match=match, listclean=bool(pats), listunknown=bool(pats)
457 456 )
458 457 files.update(
459 458 set(
460 459 itertools.chain(
461 460 stat.added, stat.modified, stat.clean, stat.unknown
462 461 )
463 462 )
464 463 )
465 464 return files
466 465
467 466
468 467 def lineranges(opts, path, basectxs, fixctx, content2):
469 468 """Returns the set of line ranges that should be fixed in a file
470 469
471 470 Of the form [(10, 20), (30, 40)].
472 471
473 472 This depends on the given base contexts; we must consider lines that have
474 473 changed versus any of the base contexts, and whether the file has been
475 474 renamed versus any of them.
476 475
477 476 Another way to understand this is that we exclude line ranges that are
478 477 common to the file in all base contexts.
479 478 """
480 479 if opts.get(b'whole'):
481 480 # Return a range containing all lines. Rely on the diff implementation's
482 481 # idea of how many lines are in the file, instead of reimplementing it.
483 482 return difflineranges(b'', content2)
484 483
485 484 rangeslist = []
486 485 for basectx in basectxs:
487 486 basepath = copies.pathcopies(basectx, fixctx).get(path, path)
488 487 if basepath in basectx:
489 488 content1 = basectx[basepath].data()
490 489 else:
491 490 content1 = b''
492 491 rangeslist.extend(difflineranges(content1, content2))
493 492 return unionranges(rangeslist)
494 493
495 494
496 495 def unionranges(rangeslist):
497 496 """Return the union of some closed intervals
498 497
499 498 >>> unionranges([])
500 499 []
501 500 >>> unionranges([(1, 100)])
502 501 [(1, 100)]
503 502 >>> unionranges([(1, 100), (1, 100)])
504 503 [(1, 100)]
505 504 >>> unionranges([(1, 100), (2, 100)])
506 505 [(1, 100)]
507 506 >>> unionranges([(1, 99), (1, 100)])
508 507 [(1, 100)]
509 508 >>> unionranges([(1, 100), (40, 60)])
510 509 [(1, 100)]
511 510 >>> unionranges([(1, 49), (50, 100)])
512 511 [(1, 100)]
513 512 >>> unionranges([(1, 48), (50, 100)])
514 513 [(1, 48), (50, 100)]
515 514 >>> unionranges([(1, 2), (3, 4), (5, 6)])
516 515 [(1, 6)]
517 516 """
518 517 rangeslist = sorted(set(rangeslist))
519 518 unioned = []
520 519 if rangeslist:
521 520 unioned, rangeslist = [rangeslist[0]], rangeslist[1:]
522 521 for a, b in rangeslist:
523 522 c, d = unioned[-1]
524 523 if a > d + 1:
525 524 unioned.append((a, b))
526 525 else:
527 526 unioned[-1] = (c, max(b, d))
528 527 return unioned
529 528
530 529
531 530 def difflineranges(content1, content2):
532 531 """Return list of line number ranges in content2 that differ from content1.
533 532
534 533 Line numbers are 1-based. The numbers are the first and last line contained
535 534 in the range. Single-line ranges have the same line number for the first and
536 535 last line. Excludes any empty ranges that result from lines that are only
537 536 present in content1. Relies on mdiff's idea of where the line endings are in
538 537 the string.
539 538
540 539 >>> from mercurial import pycompat
541 540 >>> lines = lambda s: b'\\n'.join([c for c in pycompat.iterbytestr(s)])
542 541 >>> difflineranges2 = lambda a, b: difflineranges(lines(a), lines(b))
543 542 >>> difflineranges2(b'', b'')
544 543 []
545 544 >>> difflineranges2(b'a', b'')
546 545 []
547 546 >>> difflineranges2(b'', b'A')
548 547 [(1, 1)]
549 548 >>> difflineranges2(b'a', b'a')
550 549 []
551 550 >>> difflineranges2(b'a', b'A')
552 551 [(1, 1)]
553 552 >>> difflineranges2(b'ab', b'')
554 553 []
555 554 >>> difflineranges2(b'', b'AB')
556 555 [(1, 2)]
557 556 >>> difflineranges2(b'abc', b'ac')
558 557 []
559 558 >>> difflineranges2(b'ab', b'aCb')
560 559 [(2, 2)]
561 560 >>> difflineranges2(b'abc', b'aBc')
562 561 [(2, 2)]
563 562 >>> difflineranges2(b'ab', b'AB')
564 563 [(1, 2)]
565 564 >>> difflineranges2(b'abcde', b'aBcDe')
566 565 [(2, 2), (4, 4)]
567 566 >>> difflineranges2(b'abcde', b'aBCDe')
568 567 [(2, 4)]
569 568 """
570 569 ranges = []
571 570 for lines, kind in mdiff.allblocks(content1, content2):
572 571 firstline, lastline = lines[2:4]
573 572 if kind == b'!' and firstline != lastline:
574 573 ranges.append((firstline + 1, lastline))
575 574 return ranges
576 575
577 576
578 577 def getbasectxs(repo, opts, revstofix):
579 578 """Returns a map of the base contexts for each revision
580 579
581 580 The base contexts determine which lines are considered modified when we
582 581 attempt to fix just the modified lines in a file. It also determines which
583 582 files we attempt to fix, so it is important to compute this even when
584 583 --whole is used.
585 584 """
586 585 # The --base flag overrides the usual logic, and we give every revision
587 586 # exactly the set of baserevs that the user specified.
588 587 if opts.get(b'base'):
589 588 baserevs = set(scmutil.revrange(repo, opts.get(b'base')))
590 589 if not baserevs:
591 590 baserevs = {nullrev}
592 591 basectxs = {repo[rev] for rev in baserevs}
593 592 return {rev: basectxs for rev in revstofix}
594 593
595 594 # Proceed in topological order so that we can easily determine each
596 595 # revision's baserevs by looking at its parents and their baserevs.
597 596 basectxs = collections.defaultdict(set)
598 597 for rev in sorted(revstofix):
599 598 ctx = repo[rev]
600 599 for pctx in ctx.parents():
601 600 if pctx.rev() in basectxs:
602 601 basectxs[rev].update(basectxs[pctx.rev()])
603 602 else:
604 603 basectxs[rev].add(pctx)
605 604 return basectxs
606 605
607 606
608 607 def fixfile(ui, repo, opts, fixers, fixctx, path, basectxs):
609 608 """Run any configured fixers that should affect the file in this context
610 609
611 610 Returns the file content that results from applying the fixers in some order
612 611 starting with the file's content in the fixctx. Fixers that support line
613 612 ranges will affect lines that have changed relative to any of the basectxs
614 613 (i.e. they will only avoid lines that are common to all basectxs).
615 614
616 615 A fixer tool's stdout will become the file's new content if and only if it
617 616 exits with code zero. The fixer tool's working directory is the repository's
618 617 root.
619 618 """
620 619 metadata = {}
621 620 newdata = fixctx[path].data()
622 621 for fixername, fixer in pycompat.iteritems(fixers):
623 622 if fixer.affects(opts, fixctx, path):
624 623 ranges = lineranges(opts, path, basectxs, fixctx, newdata)
625 624 command = fixer.command(ui, path, ranges)
626 625 if command is None:
627 626 continue
628 627 ui.debug(b'subprocess: %s\n' % (command,))
629 628 proc = subprocess.Popen(
630 629 procutil.tonativestr(command),
631 630 shell=True,
632 631 cwd=procutil.tonativestr(repo.root),
633 632 stdin=subprocess.PIPE,
634 633 stdout=subprocess.PIPE,
635 634 stderr=subprocess.PIPE,
636 635 )
637 636 stdout, stderr = proc.communicate(newdata)
638 637 if stderr:
639 638 showstderr(ui, fixctx.rev(), fixername, stderr)
640 639 newerdata = stdout
641 640 if fixer.shouldoutputmetadata():
642 641 try:
643 642 metadatajson, newerdata = stdout.split(b'\0', 1)
644 643 metadata[fixername] = pycompat.json_loads(metadatajson)
645 644 except ValueError:
646 645 ui.warn(
647 646 _(b'ignored invalid output from fixer tool: %s\n')
648 647 % (fixername,)
649 648 )
650 649 continue
651 650 else:
652 651 metadata[fixername] = None
653 652 if proc.returncode == 0:
654 653 newdata = newerdata
655 654 else:
656 655 if not stderr:
657 656 message = _(b'exited with status %d\n') % (proc.returncode,)
658 657 showstderr(ui, fixctx.rev(), fixername, message)
659 658 checktoolfailureaction(
660 659 ui,
661 660 _(b'no fixes will be applied'),
662 661 hint=_(
663 662 b'use --config fix.failure=continue to apply any '
664 663 b'successful fixes anyway'
665 664 ),
666 665 )
667 666 return metadata, newdata
668 667
669 668
670 669 def showstderr(ui, rev, fixername, stderr):
671 670 """Writes the lines of the stderr string as warnings on the ui
672 671
673 672 Uses the revision number and fixername to give more context to each line of
674 673 the error message. Doesn't include file names, since those take up a lot of
675 674 space and would tend to be included in the error message if they were
676 675 relevant.
677 676 """
678 677 for line in re.split(b'[\r\n]+', stderr):
679 678 if line:
680 679 ui.warn(b'[')
681 680 if rev is None:
682 681 ui.warn(_(b'wdir'), label=b'evolve.rev')
683 682 else:
684 683 ui.warn(b'%d' % rev, label=b'evolve.rev')
685 684 ui.warn(b'] %s: %s\n' % (fixername, line))
686 685
687 686
688 687 def writeworkingdir(repo, ctx, filedata, replacements):
689 688 """Write new content to the working copy and check out the new p1 if any
690 689
691 690 We check out a new revision if and only if we fixed something in both the
692 691 working directory and its parent revision. This avoids the need for a full
693 692 update/merge, and means that the working directory simply isn't affected
694 693 unless the --working-dir flag is given.
695 694
696 695 Directly updates the dirstate for the affected files.
697 696 """
698 697 for path, data in pycompat.iteritems(filedata):
699 698 fctx = ctx[path]
700 699 fctx.write(data, fctx.flags())
701 700 if repo.dirstate[path] == b'n':
702 701 repo.dirstate.normallookup(path)
703 702
704 703 oldparentnodes = repo.dirstate.parents()
705 704 newparentnodes = [replacements.get(n, n) for n in oldparentnodes]
706 705 if newparentnodes != oldparentnodes:
707 706 repo.setparents(*newparentnodes)
708 707
709 708
710 709 def replacerev(ui, repo, ctx, filedata, replacements):
711 710 """Commit a new revision like the given one, but with file content changes
712 711
713 712 "ctx" is the original revision to be replaced by a modified one.
714 713
715 714 "filedata" is a dict that maps paths to their new file content. All other
716 715 paths will be recreated from the original revision without changes.
717 716 "filedata" may contain paths that didn't exist in the original revision;
718 717 they will be added.
719 718
720 719 "replacements" is a dict that maps a single node to a single node, and it is
721 720 updated to indicate the original revision is replaced by the newly created
722 721 one. No entry is added if the replacement's node already exists.
723 722
724 723 The new revision has the same parents as the old one, unless those parents
725 724 have already been replaced, in which case those replacements are the parents
726 725 of this new revision. Thus, if revisions are replaced in topological order,
727 726 there is no need to rebase them into the original topology later.
728 727 """
729 728
730 729 p1rev, p2rev = repo.changelog.parentrevs(ctx.rev())
731 730 p1ctx, p2ctx = repo[p1rev], repo[p2rev]
732 731 newp1node = replacements.get(p1ctx.node(), p1ctx.node())
733 732 newp2node = replacements.get(p2ctx.node(), p2ctx.node())
734 733
735 734 # We don't want to create a revision that has no changes from the original,
736 735 # but we should if the original revision's parent has been replaced.
737 736 # Otherwise, we would produce an orphan that needs no actual human
738 737 # intervention to evolve. We can't rely on commit() to avoid creating the
739 738 # un-needed revision because the extra field added below produces a new hash
740 739 # regardless of file content changes.
741 740 if (
742 741 not filedata
743 742 and p1ctx.node() not in replacements
744 743 and p2ctx.node() not in replacements
745 744 ):
746 745 return
747 746
748 747 def filectxfn(repo, memctx, path):
749 748 if path not in ctx:
750 749 return None
751 750 fctx = ctx[path]
752 751 copysource = fctx.copysource()
753 752 return context.memfilectx(
754 753 repo,
755 754 memctx,
756 755 path=fctx.path(),
757 756 data=filedata.get(path, fctx.data()),
758 757 islink=fctx.islink(),
759 758 isexec=fctx.isexec(),
760 759 copysource=copysource,
761 760 )
762 761
763 762 extra = ctx.extra().copy()
764 763 extra[b'fix_source'] = ctx.hex()
765 764
766 765 memctx = context.memctx(
767 766 repo,
768 767 parents=(newp1node, newp2node),
769 768 text=ctx.description(),
770 769 files=set(ctx.files()) | set(filedata.keys()),
771 770 filectxfn=filectxfn,
772 771 user=ctx.user(),
773 772 date=ctx.date(),
774 773 extra=extra,
775 774 branch=ctx.branch(),
776 775 editor=None,
777 776 )
778 777 sucnode = memctx.commit()
779 778 prenode = ctx.node()
780 779 if prenode == sucnode:
781 780 ui.debug(b'node %s already existed\n' % (ctx.hex()))
782 781 else:
783 782 replacements[ctx.node()] = sucnode
784 783
785 784
786 785 def getfixers(ui):
787 786 """Returns a map of configured fixer tools indexed by their names
788 787
789 788 Each value is a Fixer object with methods that implement the behavior of the
790 789 fixer's config suboptions. Does not validate the config values.
791 790 """
792 791 fixers = {}
793 792 for name in fixernames(ui):
794 793 enabled = ui.configbool(b'fix', name + b':enabled')
795 794 command = ui.config(b'fix', name + b':command')
796 795 pattern = ui.config(b'fix', name + b':pattern')
797 796 linerange = ui.config(b'fix', name + b':linerange')
798 797 priority = ui.configint(b'fix', name + b':priority')
799 798 metadata = ui.configbool(b'fix', name + b':metadata')
800 799 skipclean = ui.configbool(b'fix', name + b':skipclean')
801 800 # Don't use a fixer if it has no pattern configured. It would be
802 801 # dangerous to let it affect all files. It would be pointless to let it
803 802 # affect no files. There is no reasonable subset of files to use as the
804 803 # default.
805 804 if command is None:
806 805 ui.warn(
807 806 _(b'fixer tool has no command configuration: %s\n') % (name,)
808 807 )
809 808 elif pattern is None:
810 809 ui.warn(
811 810 _(b'fixer tool has no pattern configuration: %s\n') % (name,)
812 811 )
813 812 elif not enabled:
814 813 ui.debug(b'ignoring disabled fixer tool: %s\n' % (name,))
815 814 else:
816 815 fixers[name] = Fixer(
817 816 command, pattern, linerange, priority, metadata, skipclean
818 817 )
819 818 return collections.OrderedDict(
820 819 sorted(fixers.items(), key=lambda item: item[1]._priority, reverse=True)
821 820 )
822 821
823 822
824 823 def fixernames(ui):
825 824 """Returns the names of [fix] config options that have suboptions"""
826 825 names = set()
827 826 for k, v in ui.configitems(b'fix'):
828 827 if b':' in k:
829 828 names.add(k.split(b':', 1)[0])
830 829 return names
831 830
832 831
833 832 class Fixer(object):
834 833 """Wraps the raw config values for a fixer with methods"""
835 834
836 835 def __init__(
837 836 self, command, pattern, linerange, priority, metadata, skipclean
838 837 ):
839 838 self._command = command
840 839 self._pattern = pattern
841 840 self._linerange = linerange
842 841 self._priority = priority
843 842 self._metadata = metadata
844 843 self._skipclean = skipclean
845 844
846 845 def affects(self, opts, fixctx, path):
847 846 """Should this fixer run on the file at the given path and context?"""
848 847 repo = fixctx.repo()
849 848 matcher = matchmod.match(
850 849 repo.root, repo.root, [self._pattern], ctx=fixctx
851 850 )
852 851 return matcher(path)
853 852
854 853 def shouldoutputmetadata(self):
855 854 """Should the stdout of this fixer start with JSON and a null byte?"""
856 855 return self._metadata
857 856
858 857 def command(self, ui, path, ranges):
859 858 """A shell command to use to invoke this fixer on the given file/lines
860 859
861 860 May return None if there is no appropriate command to run for the given
862 861 parameters.
863 862 """
864 863 expand = cmdutil.rendercommandtemplate
865 864 parts = [
866 865 expand(
867 866 ui,
868 867 self._command,
869 868 {b'rootpath': path, b'basename': os.path.basename(path)},
870 869 )
871 870 ]
872 871 if self._linerange:
873 872 if self._skipclean and not ranges:
874 873 # No line ranges to fix, so don't run the fixer.
875 874 return None
876 875 for first, last in ranges:
877 876 parts.append(
878 877 expand(
879 878 ui, self._linerange, {b'first': first, b'last': last}
880 879 )
881 880 )
882 881 return b' '.join(parts)
General Comments 0
You need to be logged in to leave comments. Login now