##// END OF EJS Templates
fix: move handling of --all into getrevstofix() for consistency...
Martin von Zweigbergk -
r45063:9f5e94bb default
parent child Browse files
Show More
@@ -1,863 +1,864 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 value of 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 in the 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 pycompat,
148 148 registrar,
149 149 rewriteutil,
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 252 cmdutil.check_at_most_one_arg(opts, b'all', b'rev')
253 253 cmdutil.check_incompatible_arguments(opts, b'working_dir', [b'all'])
254 if opts[b'all']:
255 opts[b'rev'] = [b'not public() and not obsolete()']
256 opts[b'working_dir'] = True
254
257 255 with repo.wlock(), repo.lock(), repo.transaction(b'fix'):
258 256 revstofix = getrevstofix(ui, repo, opts)
259 257 basectxs = getbasectxs(repo, opts, revstofix)
260 258 workqueue, numitems = getworkqueue(
261 259 ui, repo, pats, opts, revstofix, basectxs
262 260 )
263 261 fixers = getfixers(ui)
264 262
265 263 # There are no data dependencies between the workers fixing each file
266 264 # revision, so we can use all available parallelism.
267 265 def getfixes(items):
268 266 for rev, path in items:
269 267 ctx = repo[rev]
270 268 olddata = ctx[path].data()
271 269 metadata, newdata = fixfile(
272 270 ui, repo, opts, fixers, ctx, path, basectxs[rev]
273 271 )
274 272 # Don't waste memory/time passing unchanged content back, but
275 273 # produce one result per item either way.
276 274 yield (
277 275 rev,
278 276 path,
279 277 metadata,
280 278 newdata if newdata != olddata else None,
281 279 )
282 280
283 281 results = worker.worker(
284 282 ui, 1.0, getfixes, tuple(), workqueue, threadsafe=False
285 283 )
286 284
287 285 # We have to hold on to the data for each successor revision in memory
288 286 # until all its parents are committed. We ensure this by committing and
289 287 # freeing memory for the revisions in some topological order. This
290 288 # leaves a little bit of memory efficiency on the table, but also makes
291 289 # the tests deterministic. It might also be considered a feature since
292 290 # it makes the results more easily reproducible.
293 291 filedata = collections.defaultdict(dict)
294 292 aggregatemetadata = collections.defaultdict(list)
295 293 replacements = {}
296 294 wdirwritten = False
297 295 commitorder = sorted(revstofix, reverse=True)
298 296 with ui.makeprogress(
299 297 topic=_(b'fixing'), unit=_(b'files'), total=sum(numitems.values())
300 298 ) as progress:
301 299 for rev, path, filerevmetadata, newdata in results:
302 300 progress.increment(item=path)
303 301 for fixername, fixermetadata in filerevmetadata.items():
304 302 aggregatemetadata[fixername].append(fixermetadata)
305 303 if newdata is not None:
306 304 filedata[rev][path] = newdata
307 305 hookargs = {
308 306 b'rev': rev,
309 307 b'path': path,
310 308 b'metadata': filerevmetadata,
311 309 }
312 310 repo.hook(
313 311 b'postfixfile',
314 312 throw=False,
315 313 **pycompat.strkwargs(hookargs)
316 314 )
317 315 numitems[rev] -= 1
318 316 # Apply the fixes for this and any other revisions that are
319 317 # ready and sitting at the front of the queue. Using a loop here
320 318 # prevents the queue from being blocked by the first revision to
321 319 # be ready out of order.
322 320 while commitorder and not numitems[commitorder[-1]]:
323 321 rev = commitorder.pop()
324 322 ctx = repo[rev]
325 323 if rev == wdirrev:
326 324 writeworkingdir(repo, ctx, filedata[rev], replacements)
327 325 wdirwritten = bool(filedata[rev])
328 326 else:
329 327 replacerev(ui, repo, ctx, filedata[rev], replacements)
330 328 del filedata[rev]
331 329
332 330 cleanup(repo, replacements, wdirwritten)
333 331 hookargs = {
334 332 b'replacements': replacements,
335 333 b'wdirwritten': wdirwritten,
336 334 b'metadata': aggregatemetadata,
337 335 }
338 336 repo.hook(b'postfix', throw=True, **pycompat.strkwargs(hookargs))
339 337
340 338
341 339 def cleanup(repo, replacements, wdirwritten):
342 340 """Calls scmutil.cleanupnodes() with the given replacements.
343 341
344 342 "replacements" is a dict from nodeid to nodeid, with one key and one value
345 343 for every revision that was affected by fixing. This is slightly different
346 344 from cleanupnodes().
347 345
348 346 "wdirwritten" is a bool which tells whether the working copy was affected by
349 347 fixing, since it has no entry in "replacements".
350 348
351 349 Useful as a hook point for extending "hg fix" with output summarizing the
352 350 effects of the command, though we choose not to output anything here.
353 351 """
354 352 replacements = {
355 353 prec: [succ] for prec, succ in pycompat.iteritems(replacements)
356 354 }
357 355 scmutil.cleanupnodes(repo, replacements, b'fix', fixphase=True)
358 356
359 357
360 358 def getworkqueue(ui, repo, pats, opts, revstofix, basectxs):
361 359 """"Constructs the list of files to be fixed at specific revisions
362 360
363 361 It is up to the caller how to consume the work items, and the only
364 362 dependence between them is that replacement revisions must be committed in
365 363 topological order. Each work item represents a file in the working copy or
366 364 in some revision that should be fixed and written back to the working copy
367 365 or into a replacement revision.
368 366
369 367 Work items for the same revision are grouped together, so that a worker
370 368 pool starting with the first N items in parallel is likely to finish the
371 369 first revision's work before other revisions. This can allow us to write
372 370 the result to disk and reduce memory footprint. At time of writing, the
373 371 partition strategy in worker.py seems favorable to this. We also sort the
374 372 items by ascending revision number to match the order in which we commit
375 373 the fixes later.
376 374 """
377 375 workqueue = []
378 376 numitems = collections.defaultdict(int)
379 377 maxfilesize = ui.configbytes(b'fix', b'maxfilesize')
380 378 for rev in sorted(revstofix):
381 379 fixctx = repo[rev]
382 380 match = scmutil.match(fixctx, pats, opts)
383 381 for path in sorted(
384 382 pathstofix(ui, repo, pats, opts, match, basectxs[rev], fixctx)
385 383 ):
386 384 fctx = fixctx[path]
387 385 if fctx.islink():
388 386 continue
389 387 if fctx.size() > maxfilesize:
390 388 ui.warn(
391 389 _(b'ignoring file larger than %s: %s\n')
392 390 % (util.bytecount(maxfilesize), path)
393 391 )
394 392 continue
395 393 workqueue.append((rev, path))
396 394 numitems[rev] += 1
397 395 return workqueue, numitems
398 396
399 397
400 398 def getrevstofix(ui, repo, opts):
401 399 """Returns the set of revision numbers that should be fixed"""
402 revs = set(scmutil.revrange(repo, opts[b'rev']))
403 if opts.get(b'working_dir'):
404 revs.add(wdirrev)
400 if opts[b'all']:
401 revs = repo.revs(b'(not public() and not obsolete()) or wdir()')
402 else:
403 revs = set(scmutil.revrange(repo, opts[b'rev']))
404 if opts.get(b'working_dir'):
405 revs.add(wdirrev)
405 406 for rev in revs:
406 407 checkfixablectx(ui, repo, repo[rev])
407 408 # Allow fixing only wdir() even if there's an unfinished operation
408 409 if not (len(revs) == 1 and wdirrev in revs):
409 410 cmdutil.checkunfinished(repo)
410 411 rewriteutil.precheck(repo, revs, b'fix')
411 412 if wdirrev in revs and list(merge.mergestate.read(repo).unresolved()):
412 413 raise error.Abort(b'unresolved conflicts', hint=b"use 'hg resolve'")
413 414 if not revs:
414 415 raise error.Abort(
415 416 b'no changesets specified', hint=b'use --rev or --working-dir'
416 417 )
417 418 return revs
418 419
419 420
420 421 def checkfixablectx(ui, repo, ctx):
421 422 """Aborts if the revision shouldn't be replaced with a fixed one."""
422 423 if ctx.obsolete():
423 424 # It would be better to actually check if the revision has a successor.
424 425 allowdivergence = ui.configbool(
425 426 b'experimental', b'evolution.allowdivergence'
426 427 )
427 428 if not allowdivergence:
428 429 raise error.Abort(
429 430 b'fixing obsolete revision could cause divergence'
430 431 )
431 432
432 433
433 434 def pathstofix(ui, repo, pats, opts, match, basectxs, fixctx):
434 435 """Returns the set of files that should be fixed in a context
435 436
436 437 The result depends on the base contexts; we include any file that has
437 438 changed relative to any of the base contexts. Base contexts should be
438 439 ancestors of the context being fixed.
439 440 """
440 441 files = set()
441 442 for basectx in basectxs:
442 443 stat = basectx.status(
443 444 fixctx, match=match, listclean=bool(pats), listunknown=bool(pats)
444 445 )
445 446 files.update(
446 447 set(
447 448 itertools.chain(
448 449 stat.added, stat.modified, stat.clean, stat.unknown
449 450 )
450 451 )
451 452 )
452 453 return files
453 454
454 455
455 456 def lineranges(opts, path, basectxs, fixctx, content2):
456 457 """Returns the set of line ranges that should be fixed in a file
457 458
458 459 Of the form [(10, 20), (30, 40)].
459 460
460 461 This depends on the given base contexts; we must consider lines that have
461 462 changed versus any of the base contexts, and whether the file has been
462 463 renamed versus any of them.
463 464
464 465 Another way to understand this is that we exclude line ranges that are
465 466 common to the file in all base contexts.
466 467 """
467 468 if opts.get(b'whole'):
468 469 # Return a range containing all lines. Rely on the diff implementation's
469 470 # idea of how many lines are in the file, instead of reimplementing it.
470 471 return difflineranges(b'', content2)
471 472
472 473 rangeslist = []
473 474 for basectx in basectxs:
474 475 basepath = copies.pathcopies(basectx, fixctx).get(path, path)
475 476 if basepath in basectx:
476 477 content1 = basectx[basepath].data()
477 478 else:
478 479 content1 = b''
479 480 rangeslist.extend(difflineranges(content1, content2))
480 481 return unionranges(rangeslist)
481 482
482 483
483 484 def unionranges(rangeslist):
484 485 """Return the union of some closed intervals
485 486
486 487 >>> unionranges([])
487 488 []
488 489 >>> unionranges([(1, 100)])
489 490 [(1, 100)]
490 491 >>> unionranges([(1, 100), (1, 100)])
491 492 [(1, 100)]
492 493 >>> unionranges([(1, 100), (2, 100)])
493 494 [(1, 100)]
494 495 >>> unionranges([(1, 99), (1, 100)])
495 496 [(1, 100)]
496 497 >>> unionranges([(1, 100), (40, 60)])
497 498 [(1, 100)]
498 499 >>> unionranges([(1, 49), (50, 100)])
499 500 [(1, 100)]
500 501 >>> unionranges([(1, 48), (50, 100)])
501 502 [(1, 48), (50, 100)]
502 503 >>> unionranges([(1, 2), (3, 4), (5, 6)])
503 504 [(1, 6)]
504 505 """
505 506 rangeslist = sorted(set(rangeslist))
506 507 unioned = []
507 508 if rangeslist:
508 509 unioned, rangeslist = [rangeslist[0]], rangeslist[1:]
509 510 for a, b in rangeslist:
510 511 c, d = unioned[-1]
511 512 if a > d + 1:
512 513 unioned.append((a, b))
513 514 else:
514 515 unioned[-1] = (c, max(b, d))
515 516 return unioned
516 517
517 518
518 519 def difflineranges(content1, content2):
519 520 """Return list of line number ranges in content2 that differ from content1.
520 521
521 522 Line numbers are 1-based. The numbers are the first and last line contained
522 523 in the range. Single-line ranges have the same line number for the first and
523 524 last line. Excludes any empty ranges that result from lines that are only
524 525 present in content1. Relies on mdiff's idea of where the line endings are in
525 526 the string.
526 527
527 528 >>> from mercurial import pycompat
528 529 >>> lines = lambda s: b'\\n'.join([c for c in pycompat.iterbytestr(s)])
529 530 >>> difflineranges2 = lambda a, b: difflineranges(lines(a), lines(b))
530 531 >>> difflineranges2(b'', b'')
531 532 []
532 533 >>> difflineranges2(b'a', b'')
533 534 []
534 535 >>> difflineranges2(b'', b'A')
535 536 [(1, 1)]
536 537 >>> difflineranges2(b'a', b'a')
537 538 []
538 539 >>> difflineranges2(b'a', b'A')
539 540 [(1, 1)]
540 541 >>> difflineranges2(b'ab', b'')
541 542 []
542 543 >>> difflineranges2(b'', b'AB')
543 544 [(1, 2)]
544 545 >>> difflineranges2(b'abc', b'ac')
545 546 []
546 547 >>> difflineranges2(b'ab', b'aCb')
547 548 [(2, 2)]
548 549 >>> difflineranges2(b'abc', b'aBc')
549 550 [(2, 2)]
550 551 >>> difflineranges2(b'ab', b'AB')
551 552 [(1, 2)]
552 553 >>> difflineranges2(b'abcde', b'aBcDe')
553 554 [(2, 2), (4, 4)]
554 555 >>> difflineranges2(b'abcde', b'aBCDe')
555 556 [(2, 4)]
556 557 """
557 558 ranges = []
558 559 for lines, kind in mdiff.allblocks(content1, content2):
559 560 firstline, lastline = lines[2:4]
560 561 if kind == b'!' and firstline != lastline:
561 562 ranges.append((firstline + 1, lastline))
562 563 return ranges
563 564
564 565
565 566 def getbasectxs(repo, opts, revstofix):
566 567 """Returns a map of the base contexts for each revision
567 568
568 569 The base contexts determine which lines are considered modified when we
569 570 attempt to fix just the modified lines in a file. It also determines which
570 571 files we attempt to fix, so it is important to compute this even when
571 572 --whole is used.
572 573 """
573 574 # The --base flag overrides the usual logic, and we give every revision
574 575 # exactly the set of baserevs that the user specified.
575 576 if opts.get(b'base'):
576 577 baserevs = set(scmutil.revrange(repo, opts.get(b'base')))
577 578 if not baserevs:
578 579 baserevs = {nullrev}
579 580 basectxs = {repo[rev] for rev in baserevs}
580 581 return {rev: basectxs for rev in revstofix}
581 582
582 583 # Proceed in topological order so that we can easily determine each
583 584 # revision's baserevs by looking at its parents and their baserevs.
584 585 basectxs = collections.defaultdict(set)
585 586 for rev in sorted(revstofix):
586 587 ctx = repo[rev]
587 588 for pctx in ctx.parents():
588 589 if pctx.rev() in basectxs:
589 590 basectxs[rev].update(basectxs[pctx.rev()])
590 591 else:
591 592 basectxs[rev].add(pctx)
592 593 return basectxs
593 594
594 595
595 596 def fixfile(ui, repo, opts, fixers, fixctx, path, basectxs):
596 597 """Run any configured fixers that should affect the file in this context
597 598
598 599 Returns the file content that results from applying the fixers in some order
599 600 starting with the file's content in the fixctx. Fixers that support line
600 601 ranges will affect lines that have changed relative to any of the basectxs
601 602 (i.e. they will only avoid lines that are common to all basectxs).
602 603
603 604 A fixer tool's stdout will become the file's new content if and only if it
604 605 exits with code zero. The fixer tool's working directory is the repository's
605 606 root.
606 607 """
607 608 metadata = {}
608 609 newdata = fixctx[path].data()
609 610 for fixername, fixer in pycompat.iteritems(fixers):
610 611 if fixer.affects(opts, fixctx, path):
611 612 ranges = lineranges(opts, path, basectxs, fixctx, newdata)
612 613 command = fixer.command(ui, path, ranges)
613 614 if command is None:
614 615 continue
615 616 ui.debug(b'subprocess: %s\n' % (command,))
616 617 proc = subprocess.Popen(
617 618 procutil.tonativestr(command),
618 619 shell=True,
619 620 cwd=procutil.tonativestr(repo.root),
620 621 stdin=subprocess.PIPE,
621 622 stdout=subprocess.PIPE,
622 623 stderr=subprocess.PIPE,
623 624 )
624 625 stdout, stderr = proc.communicate(newdata)
625 626 if stderr:
626 627 showstderr(ui, fixctx.rev(), fixername, stderr)
627 628 newerdata = stdout
628 629 if fixer.shouldoutputmetadata():
629 630 try:
630 631 metadatajson, newerdata = stdout.split(b'\0', 1)
631 632 metadata[fixername] = pycompat.json_loads(metadatajson)
632 633 except ValueError:
633 634 ui.warn(
634 635 _(b'ignored invalid output from fixer tool: %s\n')
635 636 % (fixername,)
636 637 )
637 638 continue
638 639 else:
639 640 metadata[fixername] = None
640 641 if proc.returncode == 0:
641 642 newdata = newerdata
642 643 else:
643 644 if not stderr:
644 645 message = _(b'exited with status %d\n') % (proc.returncode,)
645 646 showstderr(ui, fixctx.rev(), fixername, message)
646 647 checktoolfailureaction(
647 648 ui,
648 649 _(b'no fixes will be applied'),
649 650 hint=_(
650 651 b'use --config fix.failure=continue to apply any '
651 652 b'successful fixes anyway'
652 653 ),
653 654 )
654 655 return metadata, newdata
655 656
656 657
657 658 def showstderr(ui, rev, fixername, stderr):
658 659 """Writes the lines of the stderr string as warnings on the ui
659 660
660 661 Uses the revision number and fixername to give more context to each line of
661 662 the error message. Doesn't include file names, since those take up a lot of
662 663 space and would tend to be included in the error message if they were
663 664 relevant.
664 665 """
665 666 for line in re.split(b'[\r\n]+', stderr):
666 667 if line:
667 668 ui.warn(b'[')
668 669 if rev is None:
669 670 ui.warn(_(b'wdir'), label=b'evolve.rev')
670 671 else:
671 672 ui.warn(b'%d' % rev, label=b'evolve.rev')
672 673 ui.warn(b'] %s: %s\n' % (fixername, line))
673 674
674 675
675 676 def writeworkingdir(repo, ctx, filedata, replacements):
676 677 """Write new content to the working copy and check out the new p1 if any
677 678
678 679 We check out a new revision if and only if we fixed something in both the
679 680 working directory and its parent revision. This avoids the need for a full
680 681 update/merge, and means that the working directory simply isn't affected
681 682 unless the --working-dir flag is given.
682 683
683 684 Directly updates the dirstate for the affected files.
684 685 """
685 686 for path, data in pycompat.iteritems(filedata):
686 687 fctx = ctx[path]
687 688 fctx.write(data, fctx.flags())
688 689 if repo.dirstate[path] == b'n':
689 690 repo.dirstate.normallookup(path)
690 691
691 692 oldparentnodes = repo.dirstate.parents()
692 693 newparentnodes = [replacements.get(n, n) for n in oldparentnodes]
693 694 if newparentnodes != oldparentnodes:
694 695 repo.setparents(*newparentnodes)
695 696
696 697
697 698 def replacerev(ui, repo, ctx, filedata, replacements):
698 699 """Commit a new revision like the given one, but with file content changes
699 700
700 701 "ctx" is the original revision to be replaced by a modified one.
701 702
702 703 "filedata" is a dict that maps paths to their new file content. All other
703 704 paths will be recreated from the original revision without changes.
704 705 "filedata" may contain paths that didn't exist in the original revision;
705 706 they will be added.
706 707
707 708 "replacements" is a dict that maps a single node to a single node, and it is
708 709 updated to indicate the original revision is replaced by the newly created
709 710 one. No entry is added if the replacement's node already exists.
710 711
711 712 The new revision has the same parents as the old one, unless those parents
712 713 have already been replaced, in which case those replacements are the parents
713 714 of this new revision. Thus, if revisions are replaced in topological order,
714 715 there is no need to rebase them into the original topology later.
715 716 """
716 717
717 718 p1rev, p2rev = repo.changelog.parentrevs(ctx.rev())
718 719 p1ctx, p2ctx = repo[p1rev], repo[p2rev]
719 720 newp1node = replacements.get(p1ctx.node(), p1ctx.node())
720 721 newp2node = replacements.get(p2ctx.node(), p2ctx.node())
721 722
722 723 # We don't want to create a revision that has no changes from the original,
723 724 # but we should if the original revision's parent has been replaced.
724 725 # Otherwise, we would produce an orphan that needs no actual human
725 726 # intervention to evolve. We can't rely on commit() to avoid creating the
726 727 # un-needed revision because the extra field added below produces a new hash
727 728 # regardless of file content changes.
728 729 if (
729 730 not filedata
730 731 and p1ctx.node() not in replacements
731 732 and p2ctx.node() not in replacements
732 733 ):
733 734 return
734 735
735 736 extra = ctx.extra().copy()
736 737 extra[b'fix_source'] = ctx.hex()
737 738
738 739 wctx = context.overlayworkingctx(repo)
739 740 wctx.setbase(repo[newp1node])
740 741 merge.revert_to(ctx, wc=wctx)
741 742 copies.graftcopies(wctx, ctx, ctx.p1())
742 743
743 744 for path in filedata.keys():
744 745 fctx = ctx[path]
745 746 copysource = fctx.copysource()
746 747 wctx.write(path, filedata[path], flags=fctx.flags())
747 748 if copysource:
748 749 wctx.markcopied(path, copysource)
749 750
750 751 memctx = wctx.tomemctx(
751 752 text=ctx.description(),
752 753 branch=ctx.branch(),
753 754 extra=extra,
754 755 date=ctx.date(),
755 756 parents=(newp1node, newp2node),
756 757 user=ctx.user(),
757 758 )
758 759
759 760 sucnode = memctx.commit()
760 761 prenode = ctx.node()
761 762 if prenode == sucnode:
762 763 ui.debug(b'node %s already existed\n' % (ctx.hex()))
763 764 else:
764 765 replacements[ctx.node()] = sucnode
765 766
766 767
767 768 def getfixers(ui):
768 769 """Returns a map of configured fixer tools indexed by their names
769 770
770 771 Each value is a Fixer object with methods that implement the behavior of the
771 772 fixer's config suboptions. Does not validate the config values.
772 773 """
773 774 fixers = {}
774 775 for name in fixernames(ui):
775 776 enabled = ui.configbool(b'fix', name + b':enabled')
776 777 command = ui.config(b'fix', name + b':command')
777 778 pattern = ui.config(b'fix', name + b':pattern')
778 779 linerange = ui.config(b'fix', name + b':linerange')
779 780 priority = ui.configint(b'fix', name + b':priority')
780 781 metadata = ui.configbool(b'fix', name + b':metadata')
781 782 skipclean = ui.configbool(b'fix', name + b':skipclean')
782 783 # Don't use a fixer if it has no pattern configured. It would be
783 784 # dangerous to let it affect all files. It would be pointless to let it
784 785 # affect no files. There is no reasonable subset of files to use as the
785 786 # default.
786 787 if command is None:
787 788 ui.warn(
788 789 _(b'fixer tool has no command configuration: %s\n') % (name,)
789 790 )
790 791 elif pattern is None:
791 792 ui.warn(
792 793 _(b'fixer tool has no pattern configuration: %s\n') % (name,)
793 794 )
794 795 elif not enabled:
795 796 ui.debug(b'ignoring disabled fixer tool: %s\n' % (name,))
796 797 else:
797 798 fixers[name] = Fixer(
798 799 command, pattern, linerange, priority, metadata, skipclean
799 800 )
800 801 return collections.OrderedDict(
801 802 sorted(fixers.items(), key=lambda item: item[1]._priority, reverse=True)
802 803 )
803 804
804 805
805 806 def fixernames(ui):
806 807 """Returns the names of [fix] config options that have suboptions"""
807 808 names = set()
808 809 for k, v in ui.configitems(b'fix'):
809 810 if b':' in k:
810 811 names.add(k.split(b':', 1)[0])
811 812 return names
812 813
813 814
814 815 class Fixer(object):
815 816 """Wraps the raw config values for a fixer with methods"""
816 817
817 818 def __init__(
818 819 self, command, pattern, linerange, priority, metadata, skipclean
819 820 ):
820 821 self._command = command
821 822 self._pattern = pattern
822 823 self._linerange = linerange
823 824 self._priority = priority
824 825 self._metadata = metadata
825 826 self._skipclean = skipclean
826 827
827 828 def affects(self, opts, fixctx, path):
828 829 """Should this fixer run on the file at the given path and context?"""
829 830 repo = fixctx.repo()
830 831 matcher = matchmod.match(
831 832 repo.root, repo.root, [self._pattern], ctx=fixctx
832 833 )
833 834 return matcher(path)
834 835
835 836 def shouldoutputmetadata(self):
836 837 """Should the stdout of this fixer start with JSON and a null byte?"""
837 838 return self._metadata
838 839
839 840 def command(self, ui, path, ranges):
840 841 """A shell command to use to invoke this fixer on the given file/lines
841 842
842 843 May return None if there is no appropriate command to run for the given
843 844 parameters.
844 845 """
845 846 expand = cmdutil.rendercommandtemplate
846 847 parts = [
847 848 expand(
848 849 ui,
849 850 self._command,
850 851 {b'rootpath': path, b'basename': os.path.basename(path)},
851 852 )
852 853 ]
853 854 if self._linerange:
854 855 if self._skipclean and not ranges:
855 856 # No line ranges to fix, so don't run the fixer.
856 857 return None
857 858 for first, last in ranges:
858 859 parts.append(
859 860 expand(
860 861 ui, self._linerange, {b'first': first, b'last': last}
861 862 )
862 863 )
863 864 return b' '.join(parts)
General Comments 0
You need to be logged in to leave comments. Login now