##// END OF EJS Templates
fix: add test case that shows why --whole with --base is useful...
Danny Hooper -
r38609:f068495a @71 default
parent child Browse files
Show More
@@ -1,584 +1,586
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 replacements = {prec: [succ] for prec, succ in replacements.iteritems()}
186 186 scmutil.cleanupnodes(repo, replacements, 'fix', fixphase=True)
187 187
188 188 def getworkqueue(ui, repo, pats, opts, revstofix, basectxs):
189 189 """"Constructs the list of files to be fixed at specific revisions
190 190
191 191 It is up to the caller how to consume the work items, and the only
192 192 dependence between them is that replacement revisions must be committed in
193 193 topological order. Each work item represents a file in the working copy or
194 194 in some revision that should be fixed and written back to the working copy
195 195 or into a replacement revision.
196 196
197 197 Work items for the same revision are grouped together, so that a worker
198 198 pool starting with the first N items in parallel is likely to finish the
199 199 first revision's work before other revisions. This can allow us to write
200 200 the result to disk and reduce memory footprint. At time of writing, the
201 201 partition strategy in worker.py seems favorable to this. We also sort the
202 202 items by ascending revision number to match the order in which we commit
203 203 the fixes later.
204 204 """
205 205 workqueue = []
206 206 numitems = collections.defaultdict(int)
207 207 maxfilesize = ui.configbytes('fix', 'maxfilesize')
208 208 for rev in sorted(revstofix):
209 209 fixctx = repo[rev]
210 210 match = scmutil.match(fixctx, pats, opts)
211 211 for path in pathstofix(ui, repo, pats, opts, match, basectxs[rev],
212 212 fixctx):
213 213 if path not in fixctx:
214 214 continue
215 215 fctx = fixctx[path]
216 216 if fctx.islink():
217 217 continue
218 218 if fctx.size() > maxfilesize:
219 219 ui.warn(_('ignoring file larger than %s: %s\n') %
220 220 (util.bytecount(maxfilesize), path))
221 221 continue
222 222 workqueue.append((rev, path))
223 223 numitems[rev] += 1
224 224 return workqueue, numitems
225 225
226 226 def getrevstofix(ui, repo, opts):
227 227 """Returns the set of revision numbers that should be fixed"""
228 228 revs = set(scmutil.revrange(repo, opts['rev']))
229 229 for rev in revs:
230 230 checkfixablectx(ui, repo, repo[rev])
231 231 if revs:
232 232 cmdutil.checkunfinished(repo)
233 233 checknodescendants(repo, revs)
234 234 if opts.get('working_dir'):
235 235 revs.add(wdirrev)
236 236 if list(merge.mergestate.read(repo).unresolved()):
237 237 raise error.Abort('unresolved conflicts', hint="use 'hg resolve'")
238 238 if not revs:
239 239 raise error.Abort(
240 240 'no changesets specified', hint='use --rev or --working-dir')
241 241 return revs
242 242
243 243 def checknodescendants(repo, revs):
244 244 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
245 245 repo.revs('(%ld::) - (%ld)', revs, revs)):
246 246 raise error.Abort(_('can only fix a changeset together '
247 247 'with all its descendants'))
248 248
249 249 def checkfixablectx(ui, repo, ctx):
250 250 """Aborts if the revision shouldn't be replaced with a fixed one."""
251 251 if not ctx.mutable():
252 252 raise error.Abort('can\'t fix immutable changeset %s' %
253 253 (scmutil.formatchangeid(ctx),))
254 254 if ctx.obsolete():
255 255 # It would be better to actually check if the revision has a successor.
256 256 allowdivergence = ui.configbool('experimental',
257 257 'evolution.allowdivergence')
258 258 if not allowdivergence:
259 259 raise error.Abort('fixing obsolete revision could cause divergence')
260 260
261 261 def pathstofix(ui, repo, pats, opts, match, basectxs, fixctx):
262 262 """Returns the set of files that should be fixed in a context
263 263
264 264 The result depends on the base contexts; we include any file that has
265 265 changed relative to any of the base contexts. Base contexts should be
266 266 ancestors of the context being fixed.
267 267 """
268 268 files = set()
269 269 for basectx in basectxs:
270 270 stat = repo.status(
271 271 basectx, fixctx, match=match, clean=bool(pats), unknown=bool(pats))
272 272 files.update(
273 273 set(itertools.chain(stat.added, stat.modified, stat.clean,
274 274 stat.unknown)))
275 275 return files
276 276
277 277 def lineranges(opts, path, basectxs, fixctx, content2):
278 278 """Returns the set of line ranges that should be fixed in a file
279 279
280 280 Of the form [(10, 20), (30, 40)].
281 281
282 282 This depends on the given base contexts; we must consider lines that have
283 283 changed versus any of the base contexts, and whether the file has been
284 284 renamed versus any of them.
285 285
286 286 Another way to understand this is that we exclude line ranges that are
287 287 common to the file in all base contexts.
288 288 """
289 289 if opts.get('whole'):
290 290 # Return a range containing all lines. Rely on the diff implementation's
291 291 # idea of how many lines are in the file, instead of reimplementing it.
292 292 return difflineranges('', content2)
293 293
294 294 rangeslist = []
295 295 for basectx in basectxs:
296 296 basepath = copies.pathcopies(basectx, fixctx).get(path, path)
297 297 if basepath in basectx:
298 298 content1 = basectx[basepath].data()
299 299 else:
300 300 content1 = ''
301 301 rangeslist.extend(difflineranges(content1, content2))
302 302 return unionranges(rangeslist)
303 303
304 304 def unionranges(rangeslist):
305 305 """Return the union of some closed intervals
306 306
307 307 >>> unionranges([])
308 308 []
309 309 >>> unionranges([(1, 100)])
310 310 [(1, 100)]
311 311 >>> unionranges([(1, 100), (1, 100)])
312 312 [(1, 100)]
313 313 >>> unionranges([(1, 100), (2, 100)])
314 314 [(1, 100)]
315 315 >>> unionranges([(1, 99), (1, 100)])
316 316 [(1, 100)]
317 317 >>> unionranges([(1, 100), (40, 60)])
318 318 [(1, 100)]
319 319 >>> unionranges([(1, 49), (50, 100)])
320 320 [(1, 100)]
321 321 >>> unionranges([(1, 48), (50, 100)])
322 322 [(1, 48), (50, 100)]
323 323 >>> unionranges([(1, 2), (3, 4), (5, 6)])
324 324 [(1, 6)]
325 325 """
326 326 rangeslist = sorted(set(rangeslist))
327 327 unioned = []
328 328 if rangeslist:
329 329 unioned, rangeslist = [rangeslist[0]], rangeslist[1:]
330 330 for a, b in rangeslist:
331 331 c, d = unioned[-1]
332 332 if a > d + 1:
333 333 unioned.append((a, b))
334 334 else:
335 335 unioned[-1] = (c, max(b, d))
336 336 return unioned
337 337
338 338 def difflineranges(content1, content2):
339 339 """Return list of line number ranges in content2 that differ from content1.
340 340
341 341 Line numbers are 1-based. The numbers are the first and last line contained
342 342 in the range. Single-line ranges have the same line number for the first and
343 343 last line. Excludes any empty ranges that result from lines that are only
344 344 present in content1. Relies on mdiff's idea of where the line endings are in
345 345 the string.
346 346
347 347 >>> from mercurial import pycompat
348 348 >>> lines = lambda s: b'\\n'.join([c for c in pycompat.iterbytestr(s)])
349 349 >>> difflineranges2 = lambda a, b: difflineranges(lines(a), lines(b))
350 350 >>> difflineranges2(b'', b'')
351 351 []
352 352 >>> difflineranges2(b'a', b'')
353 353 []
354 354 >>> difflineranges2(b'', b'A')
355 355 [(1, 1)]
356 356 >>> difflineranges2(b'a', b'a')
357 357 []
358 358 >>> difflineranges2(b'a', b'A')
359 359 [(1, 1)]
360 360 >>> difflineranges2(b'ab', b'')
361 361 []
362 362 >>> difflineranges2(b'', b'AB')
363 363 [(1, 2)]
364 364 >>> difflineranges2(b'abc', b'ac')
365 365 []
366 366 >>> difflineranges2(b'ab', b'aCb')
367 367 [(2, 2)]
368 368 >>> difflineranges2(b'abc', b'aBc')
369 369 [(2, 2)]
370 370 >>> difflineranges2(b'ab', b'AB')
371 371 [(1, 2)]
372 372 >>> difflineranges2(b'abcde', b'aBcDe')
373 373 [(2, 2), (4, 4)]
374 374 >>> difflineranges2(b'abcde', b'aBCDe')
375 375 [(2, 4)]
376 376 """
377 377 ranges = []
378 378 for lines, kind in mdiff.allblocks(content1, content2):
379 379 firstline, lastline = lines[2:4]
380 380 if kind == '!' and firstline != lastline:
381 381 ranges.append((firstline + 1, lastline))
382 382 return ranges
383 383
384 384 def getbasectxs(repo, opts, revstofix):
385 385 """Returns a map of the base contexts for each revision
386 386
387 387 The base contexts determine which lines are considered modified when we
388 attempt to fix just the modified lines in a file.
388 attempt to fix just the modified lines in a file. It also determines which
389 files we attempt to fix, so it is important to compute this even when
390 --whole is used.
389 391 """
390 392 # The --base flag overrides the usual logic, and we give every revision
391 393 # exactly the set of baserevs that the user specified.
392 394 if opts.get('base'):
393 395 baserevs = set(scmutil.revrange(repo, opts.get('base')))
394 396 if not baserevs:
395 397 baserevs = {nullrev}
396 398 basectxs = {repo[rev] for rev in baserevs}
397 399 return {rev: basectxs for rev in revstofix}
398 400
399 401 # Proceed in topological order so that we can easily determine each
400 402 # revision's baserevs by looking at its parents and their baserevs.
401 403 basectxs = collections.defaultdict(set)
402 404 for rev in sorted(revstofix):
403 405 ctx = repo[rev]
404 406 for pctx in ctx.parents():
405 407 if pctx.rev() in basectxs:
406 408 basectxs[rev].update(basectxs[pctx.rev()])
407 409 else:
408 410 basectxs[rev].add(pctx)
409 411 return basectxs
410 412
411 413 def fixfile(ui, opts, fixers, fixctx, path, basectxs):
412 414 """Run any configured fixers that should affect the file in this context
413 415
414 416 Returns the file content that results from applying the fixers in some order
415 417 starting with the file's content in the fixctx. Fixers that support line
416 418 ranges will affect lines that have changed relative to any of the basectxs
417 419 (i.e. they will only avoid lines that are common to all basectxs).
418 420 """
419 421 newdata = fixctx[path].data()
420 422 for fixername, fixer in fixers.iteritems():
421 423 if fixer.affects(opts, fixctx, path):
422 424 ranges = lineranges(opts, path, basectxs, fixctx, newdata)
423 425 command = fixer.command(ui, path, ranges)
424 426 if command is None:
425 427 continue
426 428 ui.debug('subprocess: %s\n' % (command,))
427 429 proc = subprocess.Popen(
428 430 command,
429 431 shell=True,
430 432 cwd='/',
431 433 stdin=subprocess.PIPE,
432 434 stdout=subprocess.PIPE,
433 435 stderr=subprocess.PIPE)
434 436 newerdata, stderr = proc.communicate(newdata)
435 437 if stderr:
436 438 showstderr(ui, fixctx.rev(), fixername, stderr)
437 439 else:
438 440 newdata = newerdata
439 441 return newdata
440 442
441 443 def showstderr(ui, rev, fixername, stderr):
442 444 """Writes the lines of the stderr string as warnings on the ui
443 445
444 446 Uses the revision number and fixername to give more context to each line of
445 447 the error message. Doesn't include file names, since those take up a lot of
446 448 space and would tend to be included in the error message if they were
447 449 relevant.
448 450 """
449 451 for line in re.split('[\r\n]+', stderr):
450 452 if line:
451 453 ui.warn(('['))
452 454 if rev is None:
453 455 ui.warn(_('wdir'), label='evolve.rev')
454 456 else:
455 457 ui.warn((str(rev)), label='evolve.rev')
456 458 ui.warn(('] %s: %s\n') % (fixername, line))
457 459
458 460 def writeworkingdir(repo, ctx, filedata, replacements):
459 461 """Write new content to the working copy and check out the new p1 if any
460 462
461 463 We check out a new revision if and only if we fixed something in both the
462 464 working directory and its parent revision. This avoids the need for a full
463 465 update/merge, and means that the working directory simply isn't affected
464 466 unless the --working-dir flag is given.
465 467
466 468 Directly updates the dirstate for the affected files.
467 469 """
468 470 for path, data in filedata.iteritems():
469 471 fctx = ctx[path]
470 472 fctx.write(data, fctx.flags())
471 473 if repo.dirstate[path] == 'n':
472 474 repo.dirstate.normallookup(path)
473 475
474 476 oldparentnodes = repo.dirstate.parents()
475 477 newparentnodes = [replacements.get(n, n) for n in oldparentnodes]
476 478 if newparentnodes != oldparentnodes:
477 479 repo.setparents(*newparentnodes)
478 480
479 481 def replacerev(ui, repo, ctx, filedata, replacements):
480 482 """Commit a new revision like the given one, but with file content changes
481 483
482 484 "ctx" is the original revision to be replaced by a modified one.
483 485
484 486 "filedata" is a dict that maps paths to their new file content. All other
485 487 paths will be recreated from the original revision without changes.
486 488 "filedata" may contain paths that didn't exist in the original revision;
487 489 they will be added.
488 490
489 491 "replacements" is a dict that maps a single node to a single node, and it is
490 492 updated to indicate the original revision is replaced by the newly created
491 493 one. No entry is added if the replacement's node already exists.
492 494
493 495 The new revision has the same parents as the old one, unless those parents
494 496 have already been replaced, in which case those replacements are the parents
495 497 of this new revision. Thus, if revisions are replaced in topological order,
496 498 there is no need to rebase them into the original topology later.
497 499 """
498 500
499 501 p1rev, p2rev = repo.changelog.parentrevs(ctx.rev())
500 502 p1ctx, p2ctx = repo[p1rev], repo[p2rev]
501 503 newp1node = replacements.get(p1ctx.node(), p1ctx.node())
502 504 newp2node = replacements.get(p2ctx.node(), p2ctx.node())
503 505
504 506 def filectxfn(repo, memctx, path):
505 507 if path not in ctx:
506 508 return None
507 509 fctx = ctx[path]
508 510 copied = fctx.renamed()
509 511 if copied:
510 512 copied = copied[0]
511 513 return context.memfilectx(
512 514 repo,
513 515 memctx,
514 516 path=fctx.path(),
515 517 data=filedata.get(path, fctx.data()),
516 518 islink=fctx.islink(),
517 519 isexec=fctx.isexec(),
518 520 copied=copied)
519 521
520 522 memctx = context.memctx(
521 523 repo,
522 524 parents=(newp1node, newp2node),
523 525 text=ctx.description(),
524 526 files=set(ctx.files()) | set(filedata.keys()),
525 527 filectxfn=filectxfn,
526 528 user=ctx.user(),
527 529 date=ctx.date(),
528 530 extra=ctx.extra(),
529 531 branch=ctx.branch(),
530 532 editor=None)
531 533 sucnode = memctx.commit()
532 534 prenode = ctx.node()
533 535 if prenode == sucnode:
534 536 ui.debug('node %s already existed\n' % (ctx.hex()))
535 537 else:
536 538 replacements[ctx.node()] = sucnode
537 539
538 540 def getfixers(ui):
539 541 """Returns a map of configured fixer tools indexed by their names
540 542
541 543 Each value is a Fixer object with methods that implement the behavior of the
542 544 fixer's config suboptions. Does not validate the config values.
543 545 """
544 546 result = {}
545 547 for name in fixernames(ui):
546 548 result[name] = Fixer()
547 549 attrs = ui.configsuboptions('fix', name)[1]
548 550 for key in FIXER_ATTRS:
549 551 setattr(result[name], pycompat.sysstr('_' + key),
550 552 attrs.get(key, ''))
551 553 return result
552 554
553 555 def fixernames(ui):
554 556 """Returns the names of [fix] config options that have suboptions"""
555 557 names = set()
556 558 for k, v in ui.configitems('fix'):
557 559 if ':' in k:
558 560 names.add(k.split(':', 1)[0])
559 561 return names
560 562
561 563 class Fixer(object):
562 564 """Wraps the raw config values for a fixer with methods"""
563 565
564 566 def affects(self, opts, fixctx, path):
565 567 """Should this fixer run on the file at the given path and context?"""
566 568 return scmutil.match(fixctx, [self._fileset], opts)(path)
567 569
568 570 def command(self, ui, path, ranges):
569 571 """A shell command to use to invoke this fixer on the given file/lines
570 572
571 573 May return None if there is no appropriate command to run for the given
572 574 parameters.
573 575 """
574 576 expand = cmdutil.rendercommandtemplate
575 577 parts = [expand(ui, self._command,
576 578 {'rootpath': path, 'basename': os.path.basename(path)})]
577 579 if self._linerange:
578 580 if not ranges:
579 581 # No line ranges to fix, so don't run the fixer.
580 582 return None
581 583 for first, last in ranges:
582 584 parts.append(expand(ui, self._linerange,
583 585 {'first': first, 'last': last}))
584 586 return ' '.join(parts)
@@ -1,1029 +1,1055
1 1 A script that implements uppercasing of specific lines in a file. This
2 2 approximates the behavior of code formatters well enough for our tests.
3 3
4 4 $ UPPERCASEPY="$TESTTMP/uppercase.py"
5 5 $ cat > $UPPERCASEPY <<EOF
6 6 > import sys
7 7 > from mercurial.utils.procutil import setbinary
8 8 > setbinary(sys.stdin)
9 9 > setbinary(sys.stdout)
10 10 > lines = set()
11 11 > for arg in sys.argv[1:]:
12 12 > if arg == 'all':
13 13 > sys.stdout.write(sys.stdin.read().upper())
14 14 > sys.exit(0)
15 15 > else:
16 16 > first, last = arg.split('-')
17 17 > lines.update(range(int(first), int(last) + 1))
18 18 > for i, line in enumerate(sys.stdin.readlines()):
19 19 > if i + 1 in lines:
20 20 > sys.stdout.write(line.upper())
21 21 > else:
22 22 > sys.stdout.write(line)
23 23 > EOF
24 24 $ TESTLINES="foo\nbar\nbaz\nqux\n"
25 25 $ printf $TESTLINES | $PYTHON $UPPERCASEPY
26 26 foo
27 27 bar
28 28 baz
29 29 qux
30 30 $ printf $TESTLINES | $PYTHON $UPPERCASEPY all
31 31 FOO
32 32 BAR
33 33 BAZ
34 34 QUX
35 35 $ printf $TESTLINES | $PYTHON $UPPERCASEPY 1-1
36 36 FOO
37 37 bar
38 38 baz
39 39 qux
40 40 $ printf $TESTLINES | $PYTHON $UPPERCASEPY 1-2
41 41 FOO
42 42 BAR
43 43 baz
44 44 qux
45 45 $ printf $TESTLINES | $PYTHON $UPPERCASEPY 2-3
46 46 foo
47 47 BAR
48 48 BAZ
49 49 qux
50 50 $ printf $TESTLINES | $PYTHON $UPPERCASEPY 2-2 4-4
51 51 foo
52 52 BAR
53 53 baz
54 54 QUX
55 55
56 56 Set up the config with two simple fixers: one that fixes specific line ranges,
57 57 and one that always fixes the whole file. They both "fix" files by converting
58 58 letters to uppercase. They use different file extensions, so each test case can
59 59 choose which behavior to use by naming files.
60 60
61 61 $ cat >> $HGRCPATH <<EOF
62 62 > [extensions]
63 63 > fix =
64 64 > [experimental]
65 65 > evolution.createmarkers=True
66 66 > evolution.allowunstable=True
67 67 > [fix]
68 68 > uppercase-whole-file:command=$PYTHON $UPPERCASEPY all
69 69 > uppercase-whole-file:fileset=set:**.whole
70 70 > uppercase-changed-lines:command=$PYTHON $UPPERCASEPY
71 71 > uppercase-changed-lines:linerange={first}-{last}
72 72 > uppercase-changed-lines:fileset=set:**.changed
73 73 > EOF
74 74
75 75 Help text for fix.
76 76
77 77 $ hg help fix
78 78 hg fix [OPTION]... [FILE]...
79 79
80 80 rewrite file content in changesets or working directory
81 81
82 82 Runs any configured tools to fix the content of files. Only affects files
83 83 with changes, unless file arguments are provided. Only affects changed
84 84 lines of files, unless the --whole flag is used. Some tools may always
85 85 affect the whole file regardless of --whole.
86 86
87 87 If revisions are specified with --rev, those revisions will be checked,
88 88 and they may be replaced with new revisions that have fixed file content.
89 89 It is desirable to specify all descendants of each specified revision, so
90 90 that the fixes propagate to the descendants. If all descendants are fixed
91 91 at the same time, no merging, rebasing, or evolution will be required.
92 92
93 93 If --working-dir is used, files with uncommitted changes in the working
94 94 copy will be fixed. If the checked-out revision is also fixed, the working
95 95 directory will update to the replacement revision.
96 96
97 97 When determining what lines of each file to fix at each revision, the
98 98 whole set of revisions being fixed is considered, so that fixes to earlier
99 99 revisions are not forgotten in later ones. The --base flag can be used to
100 100 override this default behavior, though it is not usually desirable to do
101 101 so.
102 102
103 103 (use 'hg help -e fix' to show help for the fix extension)
104 104
105 105 options ([+] can be repeated):
106 106
107 107 --all fix all non-public non-obsolete revisions
108 108 --base REV [+] revisions to diff against (overrides automatic selection,
109 109 and applies to every revision being fixed)
110 110 -r --rev REV [+] revisions to fix
111 111 -w --working-dir fix the working directory
112 112 --whole always fix every line of a file
113 113
114 114 (some details hidden, use --verbose to show complete help)
115 115
116 116 $ hg help -e fix
117 117 fix extension - rewrite file content in changesets or working copy
118 118 (EXPERIMENTAL)
119 119
120 120 Provides a command that runs configured tools on the contents of modified
121 121 files, writing back any fixes to the working copy or replacing changesets.
122 122
123 123 Here is an example configuration that causes 'hg fix' to apply automatic
124 124 formatting fixes to modified lines in C++ code:
125 125
126 126 [fix]
127 127 clang-format:command=clang-format --assume-filename={rootpath}
128 128 clang-format:linerange=--lines={first}:{last}
129 129 clang-format:fileset=set:**.cpp or **.hpp
130 130
131 131 The :command suboption forms the first part of the shell command that will be
132 132 used to fix a file. The content of the file is passed on standard input, and
133 133 the fixed file content is expected on standard output. If there is any output
134 134 on standard error, the file will not be affected. Some values may be
135 135 substituted into the command:
136 136
137 137 {rootpath} The path of the file being fixed, relative to the repo root
138 138 {basename} The name of the file being fixed, without the directory path
139 139
140 140 If the :linerange suboption is set, the tool will only be run if there are
141 141 changed lines in a file. The value of this suboption is appended to the shell
142 142 command once for every range of changed lines in the file. Some values may be
143 143 substituted into the command:
144 144
145 145 {first} The 1-based line number of the first line in the modified range
146 146 {last} The 1-based line number of the last line in the modified range
147 147
148 148 The :fileset suboption determines which files will be passed through each
149 149 configured tool. See 'hg help fileset' for possible values. If there are file
150 150 arguments to 'hg fix', the intersection of these filesets is used.
151 151
152 152 There is also a configurable limit for the maximum size of file that will be
153 153 processed by 'hg fix':
154 154
155 155 [fix]
156 156 maxfilesize=2MB
157 157
158 158 list of commands:
159 159
160 160 fix rewrite file content in changesets or working directory
161 161
162 162 (use 'hg help -v -e fix' to show built-in aliases and global options)
163 163
164 164 There is no default behavior in the absence of --rev and --working-dir.
165 165
166 166 $ hg init badusage
167 167 $ cd badusage
168 168
169 169 $ hg fix
170 170 abort: no changesets specified
171 171 (use --rev or --working-dir)
172 172 [255]
173 173 $ hg fix --whole
174 174 abort: no changesets specified
175 175 (use --rev or --working-dir)
176 176 [255]
177 177 $ hg fix --base 0
178 178 abort: no changesets specified
179 179 (use --rev or --working-dir)
180 180 [255]
181 181
182 182 Fixing a public revision isn't allowed. It should abort early enough that
183 183 nothing happens, even to the working directory.
184 184
185 185 $ printf "hello\n" > hello.whole
186 186 $ hg commit -Aqm "hello"
187 187 $ hg phase -r 0 --public
188 188 $ hg fix -r 0
189 189 abort: can't fix immutable changeset 0:6470986d2e7b
190 190 [255]
191 191 $ hg fix -r 0 --working-dir
192 192 abort: can't fix immutable changeset 0:6470986d2e7b
193 193 [255]
194 194 $ hg cat -r tip hello.whole
195 195 hello
196 196 $ cat hello.whole
197 197 hello
198 198
199 199 $ cd ..
200 200
201 201 Fixing a clean working directory should do nothing. Even the --whole flag
202 202 shouldn't cause any clean files to be fixed. Specifying a clean file explicitly
203 203 should only fix it if the fixer always fixes the whole file. The combination of
204 204 an explicit filename and --whole should format the entire file regardless.
205 205
206 206 $ hg init fixcleanwdir
207 207 $ cd fixcleanwdir
208 208
209 209 $ printf "hello\n" > hello.changed
210 210 $ printf "world\n" > hello.whole
211 211 $ hg commit -Aqm "foo"
212 212 $ hg fix --working-dir
213 213 $ hg diff
214 214 $ hg fix --working-dir --whole
215 215 $ hg diff
216 216 $ hg fix --working-dir *
217 217 $ cat *
218 218 hello
219 219 WORLD
220 220 $ hg revert --all --no-backup
221 221 reverting hello.whole
222 222 $ hg fix --working-dir * --whole
223 223 $ cat *
224 224 HELLO
225 225 WORLD
226 226
227 227 The same ideas apply to fixing a revision, so we create a revision that doesn't
228 228 modify either of the files in question and try fixing it. This also tests that
229 229 we ignore a file that doesn't match any configured fixer.
230 230
231 231 $ hg revert --all --no-backup
232 232 reverting hello.changed
233 233 reverting hello.whole
234 234 $ printf "unimportant\n" > some.file
235 235 $ hg commit -Aqm "some other file"
236 236
237 237 $ hg fix -r .
238 238 $ hg cat -r tip *
239 239 hello
240 240 world
241 241 unimportant
242 242 $ hg fix -r . --whole
243 243 $ hg cat -r tip *
244 244 hello
245 245 world
246 246 unimportant
247 247 $ hg fix -r . *
248 248 $ hg cat -r tip *
249 249 hello
250 250 WORLD
251 251 unimportant
252 252 $ hg fix -r . * --whole --config experimental.evolution.allowdivergence=true
253 253 2 new content-divergent changesets
254 254 $ hg cat -r tip *
255 255 HELLO
256 256 WORLD
257 257 unimportant
258 258
259 259 $ cd ..
260 260
261 261 Fixing the working directory should still work if there are no revisions.
262 262
263 263 $ hg init norevisions
264 264 $ cd norevisions
265 265
266 266 $ printf "something\n" > something.whole
267 267 $ hg add
268 268 adding something.whole
269 269 $ hg fix --working-dir
270 270 $ cat something.whole
271 271 SOMETHING
272 272
273 273 $ cd ..
274 274
275 275 Test the effect of fixing the working directory for each possible status, with
276 276 and without providing explicit file arguments.
277 277
278 278 $ hg init implicitlyfixstatus
279 279 $ cd implicitlyfixstatus
280 280
281 281 $ printf "modified\n" > modified.whole
282 282 $ printf "removed\n" > removed.whole
283 283 $ printf "deleted\n" > deleted.whole
284 284 $ printf "clean\n" > clean.whole
285 285 $ printf "ignored.whole" > .hgignore
286 286 $ hg commit -Aqm "stuff"
287 287
288 288 $ printf "modified!!!\n" > modified.whole
289 289 $ printf "unknown\n" > unknown.whole
290 290 $ printf "ignored\n" > ignored.whole
291 291 $ printf "added\n" > added.whole
292 292 $ hg add added.whole
293 293 $ hg remove removed.whole
294 294 $ rm deleted.whole
295 295
296 296 $ hg status --all
297 297 M modified.whole
298 298 A added.whole
299 299 R removed.whole
300 300 ! deleted.whole
301 301 ? unknown.whole
302 302 I ignored.whole
303 303 C .hgignore
304 304 C clean.whole
305 305
306 306 $ hg fix --working-dir
307 307
308 308 $ hg status --all
309 309 M modified.whole
310 310 A added.whole
311 311 R removed.whole
312 312 ! deleted.whole
313 313 ? unknown.whole
314 314 I ignored.whole
315 315 C .hgignore
316 316 C clean.whole
317 317
318 318 $ cat *.whole
319 319 ADDED
320 320 clean
321 321 ignored
322 322 MODIFIED!!!
323 323 unknown
324 324
325 325 $ printf "modified!!!\n" > modified.whole
326 326 $ printf "added\n" > added.whole
327 327 $ hg fix --working-dir *.whole
328 328
329 329 $ hg status --all
330 330 M clean.whole
331 331 M modified.whole
332 332 A added.whole
333 333 R removed.whole
334 334 ! deleted.whole
335 335 ? unknown.whole
336 336 I ignored.whole
337 337 C .hgignore
338 338
339 339 It would be better if this also fixed the unknown file.
340 340 $ cat *.whole
341 341 ADDED
342 342 CLEAN
343 343 ignored
344 344 MODIFIED!!!
345 345 unknown
346 346
347 347 $ cd ..
348 348
349 349 Test that incremental fixing works on files with additions, deletions, and
350 350 changes in multiple line ranges. Note that deletions do not generally cause
351 351 neighboring lines to be fixed, so we don't return a line range for purely
352 352 deleted sections. In the future we should support a :deletion config that
353 353 allows fixers to know where deletions are located.
354 354
355 355 $ hg init incrementalfixedlines
356 356 $ cd incrementalfixedlines
357 357
358 358 $ printf "a\nb\nc\nd\ne\nf\ng\n" > foo.txt
359 359 $ hg commit -Aqm "foo"
360 360 $ printf "zz\na\nc\ndd\nee\nff\nf\ngg\n" > foo.txt
361 361
362 362 $ hg --config "fix.fail:command=echo" \
363 363 > --config "fix.fail:linerange={first}:{last}" \
364 364 > --config "fix.fail:fileset=foo.txt" \
365 365 > fix --working-dir
366 366 $ cat foo.txt
367 367 1:1 4:6 8:8
368 368
369 369 $ cd ..
370 370
371 371 Test that --whole fixes all lines regardless of the diffs present.
372 372
373 373 $ hg init wholeignoresdiffs
374 374 $ cd wholeignoresdiffs
375 375
376 376 $ printf "a\nb\nc\nd\ne\nf\ng\n" > foo.changed
377 377 $ hg commit -Aqm "foo"
378 378 $ printf "zz\na\nc\ndd\nee\nff\nf\ngg\n" > foo.changed
379 379 $ hg fix --working-dir --whole
380 380 $ cat foo.changed
381 381 ZZ
382 382 A
383 383 C
384 384 DD
385 385 EE
386 386 FF
387 387 F
388 388 GG
389 389
390 390 $ cd ..
391 391
392 392 We should do nothing with symlinks, and their targets should be unaffected. Any
393 393 other behavior would be more complicated to implement and harder to document.
394 394
395 395 #if symlink
396 396 $ hg init dontmesswithsymlinks
397 397 $ cd dontmesswithsymlinks
398 398
399 399 $ printf "hello\n" > hello.whole
400 400 $ ln -s hello.whole hellolink
401 401 $ hg add
402 402 adding hello.whole
403 403 adding hellolink
404 404 $ hg fix --working-dir hellolink
405 405 $ hg status
406 406 A hello.whole
407 407 A hellolink
408 408
409 409 $ cd ..
410 410 #endif
411 411
412 412 We should allow fixers to run on binary files, even though this doesn't sound
413 413 like a common use case. There's not much benefit to disallowing it, and users
414 414 can add "and not binary()" to their filesets if needed. The Mercurial
415 415 philosophy is generally to not handle binary files specially anyway.
416 416
417 417 $ hg init cantouchbinaryfiles
418 418 $ cd cantouchbinaryfiles
419 419
420 420 $ printf "hello\0\n" > hello.whole
421 421 $ hg add
422 422 adding hello.whole
423 423 $ hg fix --working-dir 'set:binary()'
424 424 $ cat hello.whole
425 425 HELLO\x00 (esc)
426 426
427 427 $ cd ..
428 428
429 429 We have a config for the maximum size of file we will attempt to fix. This can
430 430 be helpful to avoid running unsuspecting fixer tools on huge inputs, which
431 431 could happen by accident without a well considered configuration. A more
432 432 precise configuration could use the size() fileset function if one global limit
433 433 is undesired.
434 434
435 435 $ hg init maxfilesize
436 436 $ cd maxfilesize
437 437
438 438 $ printf "this file is huge\n" > hello.whole
439 439 $ hg add
440 440 adding hello.whole
441 441 $ hg --config fix.maxfilesize=10 fix --working-dir
442 442 ignoring file larger than 10 bytes: hello.whole
443 443 $ cat hello.whole
444 444 this file is huge
445 445
446 446 $ cd ..
447 447
448 448 If we specify a file to fix, other files should be left alone, even if they
449 449 have changes.
450 450
451 451 $ hg init fixonlywhatitellyouto
452 452 $ cd fixonlywhatitellyouto
453 453
454 454 $ printf "fix me!\n" > fixme.whole
455 455 $ printf "not me.\n" > notme.whole
456 456 $ hg add
457 457 adding fixme.whole
458 458 adding notme.whole
459 459 $ hg fix --working-dir fixme.whole
460 460 $ cat *.whole
461 461 FIX ME!
462 462 not me.
463 463
464 464 $ cd ..
465 465
466 466 Specifying a directory name should fix all its files and subdirectories.
467 467
468 468 $ hg init fixdirectory
469 469 $ cd fixdirectory
470 470
471 471 $ mkdir -p dir1/dir2
472 472 $ printf "foo\n" > foo.whole
473 473 $ printf "bar\n" > dir1/bar.whole
474 474 $ printf "baz\n" > dir1/dir2/baz.whole
475 475 $ hg add
476 476 adding dir1/bar.whole
477 477 adding dir1/dir2/baz.whole
478 478 adding foo.whole
479 479 $ hg fix --working-dir dir1
480 480 $ cat foo.whole dir1/bar.whole dir1/dir2/baz.whole
481 481 foo
482 482 BAR
483 483 BAZ
484 484
485 485 $ cd ..
486 486
487 487 Fixing a file in the working directory that needs no fixes should not actually
488 488 write back to the file, so for example the mtime shouldn't change.
489 489
490 490 $ hg init donttouchunfixedfiles
491 491 $ cd donttouchunfixedfiles
492 492
493 493 $ printf "NO FIX NEEDED\n" > foo.whole
494 494 $ hg add
495 495 adding foo.whole
496 496 $ cp -p foo.whole foo.whole.orig
497 497 $ cp -p foo.whole.orig foo.whole
498 498 $ sleep 2 # mtime has a resolution of one or two seconds.
499 499 $ hg fix --working-dir
500 500 $ f foo.whole.orig --newer foo.whole
501 501 foo.whole.orig: newer than foo.whole
502 502
503 503 $ cd ..
504 504
505 505 When a fixer prints to stderr, we assume that it has failed. We should show the
506 506 error messages to the user, and we should not let the failing fixer affect the
507 507 file it was fixing (many code formatters might emit error messages on stderr
508 508 and nothing on stdout, which would cause us the clear the file). We show the
509 509 user which fixer failed and which revision, but we assume that the fixer will
510 510 print the filename if it is relevant.
511 511
512 512 $ hg init showstderr
513 513 $ cd showstderr
514 514
515 515 $ printf "hello\n" > hello.txt
516 516 $ hg add
517 517 adding hello.txt
518 518 $ cat >> $TESTTMP/cmd.sh <<'EOF'
519 519 > printf 'HELLO\n'
520 520 > printf "$@: some\nerror" >&2
521 521 > EOF
522 522 $ hg --config "fix.fail:command=sh $TESTTMP/cmd.sh {rootpath}" \
523 523 > --config "fix.fail:fileset=hello.txt" \
524 524 > fix --working-dir
525 525 [wdir] fail: hello.txt: some
526 526 [wdir] fail: error
527 527 $ cat hello.txt
528 528 hello
529 529
530 530 $ cd ..
531 531
532 532 Fixing the working directory and its parent revision at the same time should
533 533 check out the replacement revision for the parent. This prevents any new
534 534 uncommitted changes from appearing. We test this for a clean working directory
535 535 and a dirty one. In both cases, all lines/files changed since the grandparent
536 536 will be fixed. The grandparent is the "baserev" for both the parent and the
537 537 working copy.
538 538
539 539 $ hg init fixdotandcleanwdir
540 540 $ cd fixdotandcleanwdir
541 541
542 542 $ printf "hello\n" > hello.whole
543 543 $ printf "world\n" > world.whole
544 544 $ hg commit -Aqm "the parent commit"
545 545
546 546 $ hg parents --template '{rev} {desc}\n'
547 547 0 the parent commit
548 548 $ hg fix --working-dir -r .
549 549 $ hg parents --template '{rev} {desc}\n'
550 550 1 the parent commit
551 551 $ hg cat -r . *.whole
552 552 HELLO
553 553 WORLD
554 554 $ cat *.whole
555 555 HELLO
556 556 WORLD
557 557 $ hg status
558 558
559 559 $ cd ..
560 560
561 561 Same test with a dirty working copy.
562 562
563 563 $ hg init fixdotanddirtywdir
564 564 $ cd fixdotanddirtywdir
565 565
566 566 $ printf "hello\n" > hello.whole
567 567 $ printf "world\n" > world.whole
568 568 $ hg commit -Aqm "the parent commit"
569 569
570 570 $ printf "hello,\n" > hello.whole
571 571 $ printf "world!\n" > world.whole
572 572
573 573 $ hg parents --template '{rev} {desc}\n'
574 574 0 the parent commit
575 575 $ hg fix --working-dir -r .
576 576 $ hg parents --template '{rev} {desc}\n'
577 577 1 the parent commit
578 578 $ hg cat -r . *.whole
579 579 HELLO
580 580 WORLD
581 581 $ cat *.whole
582 582 HELLO,
583 583 WORLD!
584 584 $ hg status
585 585 M hello.whole
586 586 M world.whole
587 587
588 588 $ cd ..
589 589
590 590 When we have a chain of commits that change mutually exclusive lines of code,
591 591 we should be able to do incremental fixing that causes each commit in the chain
592 592 to include fixes made to the previous commits. This prevents children from
593 593 backing out the fixes made in their parents. A dirty working directory is
594 594 conceptually similar to another commit in the chain.
595 595
596 596 $ hg init incrementallyfixchain
597 597 $ cd incrementallyfixchain
598 598
599 599 $ cat > file.changed <<EOF
600 600 > first
601 601 > second
602 602 > third
603 603 > fourth
604 604 > fifth
605 605 > EOF
606 606 $ hg commit -Aqm "the common ancestor (the baserev)"
607 607 $ cat > file.changed <<EOF
608 608 > first (changed)
609 609 > second
610 610 > third
611 611 > fourth
612 612 > fifth
613 613 > EOF
614 614 $ hg commit -Aqm "the first commit to fix"
615 615 $ cat > file.changed <<EOF
616 616 > first (changed)
617 617 > second
618 618 > third (changed)
619 619 > fourth
620 620 > fifth
621 621 > EOF
622 622 $ hg commit -Aqm "the second commit to fix"
623 623 $ cat > file.changed <<EOF
624 624 > first (changed)
625 625 > second
626 626 > third (changed)
627 627 > fourth
628 628 > fifth (changed)
629 629 > EOF
630 630
631 631 $ hg fix -r . -r '.^' --working-dir
632 632
633 633 $ hg parents --template '{rev}\n'
634 634 4
635 635 $ hg cat -r '.^^' file.changed
636 636 first
637 637 second
638 638 third
639 639 fourth
640 640 fifth
641 641 $ hg cat -r '.^' file.changed
642 642 FIRST (CHANGED)
643 643 second
644 644 third
645 645 fourth
646 646 fifth
647 647 $ hg cat -r . file.changed
648 648 FIRST (CHANGED)
649 649 second
650 650 THIRD (CHANGED)
651 651 fourth
652 652 fifth
653 653 $ cat file.changed
654 654 FIRST (CHANGED)
655 655 second
656 656 THIRD (CHANGED)
657 657 fourth
658 658 FIFTH (CHANGED)
659 659
660 660 $ cd ..
661 661
662 662 If we incrementally fix a merge commit, we should fix any lines that changed
663 663 versus either parent. You could imagine only fixing the intersection or some
664 664 other subset, but this is necessary if either parent is being fixed. It
665 665 prevents us from forgetting fixes made in either parent.
666 666
667 667 $ hg init incrementallyfixmergecommit
668 668 $ cd incrementallyfixmergecommit
669 669
670 670 $ printf "a\nb\nc\n" > file.changed
671 671 $ hg commit -Aqm "ancestor"
672 672
673 673 $ printf "aa\nb\nc\n" > file.changed
674 674 $ hg commit -m "change a"
675 675
676 676 $ hg checkout '.^'
677 677 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
678 678 $ printf "a\nb\ncc\n" > file.changed
679 679 $ hg commit -m "change c"
680 680 created new head
681 681
682 682 $ hg merge
683 683 merging file.changed
684 684 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
685 685 (branch merge, don't forget to commit)
686 686 $ hg commit -m "merge"
687 687 $ hg cat -r . file.changed
688 688 aa
689 689 b
690 690 cc
691 691
692 692 $ hg fix -r . --working-dir
693 693 $ hg cat -r . file.changed
694 694 AA
695 695 b
696 696 CC
697 697
698 698 $ cd ..
699 699
700 700 Abort fixing revisions if there is an unfinished operation. We don't want to
701 701 make things worse by editing files or stripping/obsoleting things. Also abort
702 702 fixing the working directory if there are unresolved merge conflicts.
703 703
704 704 $ hg init abortunresolved
705 705 $ cd abortunresolved
706 706
707 707 $ echo "foo1" > foo.whole
708 708 $ hg commit -Aqm "foo 1"
709 709
710 710 $ hg update null
711 711 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
712 712 $ echo "foo2" > foo.whole
713 713 $ hg commit -Aqm "foo 2"
714 714
715 715 $ hg --config extensions.rebase= rebase -r 1 -d 0
716 716 rebasing 1:c3b6dc0e177a "foo 2" (tip)
717 717 merging foo.whole
718 718 warning: conflicts while merging foo.whole! (edit, then use 'hg resolve --mark')
719 719 unresolved conflicts (see hg resolve, then hg rebase --continue)
720 720 [1]
721 721
722 722 $ hg --config extensions.rebase= fix --working-dir
723 723 abort: unresolved conflicts
724 724 (use 'hg resolve')
725 725 [255]
726 726
727 727 $ hg --config extensions.rebase= fix -r .
728 728 abort: rebase in progress
729 729 (use 'hg rebase --continue' or 'hg rebase --abort')
730 730 [255]
731 731
732 732 When fixing a file that was renamed, we should diff against the source of the
733 733 rename for incremental fixing and we should correctly reproduce the rename in
734 734 the replacement revision.
735 735
736 736 $ hg init fixrenamecommit
737 737 $ cd fixrenamecommit
738 738
739 739 $ printf "a\nb\nc\n" > source.changed
740 740 $ hg commit -Aqm "source revision"
741 741 $ hg move source.changed dest.changed
742 742 $ printf "a\nb\ncc\n" > dest.changed
743 743 $ hg commit -m "dest revision"
744 744
745 745 $ hg fix -r .
746 746 $ hg log -r tip --copies --template "{file_copies}\n"
747 747 dest.changed (source.changed)
748 748 $ hg cat -r tip dest.changed
749 749 a
750 750 b
751 751 CC
752 752
753 753 $ cd ..
754 754
755 755 When fixing revisions that remove files we must ensure that the replacement
756 756 actually removes the file, whereas it could accidentally leave it unchanged or
757 757 write an empty string to it.
758 758
759 759 $ hg init fixremovedfile
760 760 $ cd fixremovedfile
761 761
762 762 $ printf "foo\n" > foo.whole
763 763 $ printf "bar\n" > bar.whole
764 764 $ hg commit -Aqm "add files"
765 765 $ hg remove bar.whole
766 766 $ hg commit -m "remove file"
767 767 $ hg status --change .
768 768 R bar.whole
769 769 $ hg fix -r . foo.whole
770 770 $ hg status --change tip
771 771 M foo.whole
772 772 R bar.whole
773 773
774 774 $ cd ..
775 775
776 776 If fixing a revision finds no fixes to make, no replacement revision should be
777 777 created.
778 778
779 779 $ hg init nofixesneeded
780 780 $ cd nofixesneeded
781 781
782 782 $ printf "FOO\n" > foo.whole
783 783 $ hg commit -Aqm "add file"
784 784 $ hg log --template '{rev}\n'
785 785 0
786 786 $ hg fix -r .
787 787 $ hg log --template '{rev}\n'
788 788 0
789 789
790 790 $ cd ..
791 791
792 792 If fixing a commit reverts all the changes in the commit, we replace it with a
793 793 commit that changes no files.
794 794
795 795 $ hg init nochangesleft
796 796 $ cd nochangesleft
797 797
798 798 $ printf "FOO\n" > foo.whole
799 799 $ hg commit -Aqm "add file"
800 800 $ printf "foo\n" > foo.whole
801 801 $ hg commit -m "edit file"
802 802 $ hg status --change .
803 803 M foo.whole
804 804 $ hg fix -r .
805 805 $ hg status --change tip
806 806
807 807 $ cd ..
808 808
809 809 If we fix a parent and child revision together, the child revision must be
810 810 replaced if the parent is replaced, even if the diffs of the child needed no
811 811 fixes. However, we're free to not replace revisions that need no fixes and have
812 812 no ancestors that are replaced.
813 813
814 814 $ hg init mustreplacechild
815 815 $ cd mustreplacechild
816 816
817 817 $ printf "FOO\n" > foo.whole
818 818 $ hg commit -Aqm "add foo"
819 819 $ printf "foo\n" > foo.whole
820 820 $ hg commit -m "edit foo"
821 821 $ printf "BAR\n" > bar.whole
822 822 $ hg commit -Aqm "add bar"
823 823
824 824 $ hg log --graph --template '{node|shortest} {files}'
825 825 @ bc05 bar.whole
826 826 |
827 827 o 4fd2 foo.whole
828 828 |
829 829 o f9ac foo.whole
830 830
831 831 $ hg fix -r 0:2
832 832 $ hg log --graph --template '{node|shortest} {files}'
833 833 o 3801 bar.whole
834 834 |
835 835 o 38cc
836 836 |
837 837 | @ bc05 bar.whole
838 838 | |
839 839 | x 4fd2 foo.whole
840 840 |/
841 841 o f9ac foo.whole
842 842
843 843
844 844 $ cd ..
845 845
846 846 It's also possible that the child needs absolutely no changes, but we still
847 847 need to replace it to update its parent. If we skipped replacing the child
848 848 because it had no file content changes, it would become an orphan for no good
849 849 reason.
850 850
851 851 $ hg init mustreplacechildevenifnop
852 852 $ cd mustreplacechildevenifnop
853 853
854 854 $ printf "Foo\n" > foo.whole
855 855 $ hg commit -Aqm "add a bad foo"
856 856 $ printf "FOO\n" > foo.whole
857 857 $ hg commit -m "add a good foo"
858 858 $ hg fix -r . -r '.^'
859 859 $ hg log --graph --template '{rev} {desc}'
860 860 o 3 add a good foo
861 861 |
862 862 o 2 add a bad foo
863 863
864 864 @ 1 add a good foo
865 865 |
866 866 x 0 add a bad foo
867 867
868 868
869 869 $ cd ..
870 870
871 871 Similar to the case above, the child revision may become empty as a result of
872 872 fixing its parent. We should still create an empty replacement child.
873 873 TODO: determine how this should interact with ui.allowemptycommit given that
874 874 the empty replacement could have children.
875 875
876 876 $ hg init mustreplacechildevenifempty
877 877 $ cd mustreplacechildevenifempty
878 878
879 879 $ printf "foo\n" > foo.whole
880 880 $ hg commit -Aqm "add foo"
881 881 $ printf "Foo\n" > foo.whole
882 882 $ hg commit -m "edit foo"
883 883 $ hg fix -r . -r '.^'
884 884 $ hg log --graph --template '{rev} {desc}\n' --stat
885 885 o 3 edit foo
886 886 |
887 887 o 2 add foo
888 888 foo.whole | 1 +
889 889 1 files changed, 1 insertions(+), 0 deletions(-)
890 890
891 891 @ 1 edit foo
892 892 | foo.whole | 2 +-
893 893 | 1 files changed, 1 insertions(+), 1 deletions(-)
894 894 |
895 895 x 0 add foo
896 896 foo.whole | 1 +
897 897 1 files changed, 1 insertions(+), 0 deletions(-)
898 898
899 899
900 900 $ cd ..
901 901
902 902 Fixing a secret commit should replace it with another secret commit.
903 903
904 904 $ hg init fixsecretcommit
905 905 $ cd fixsecretcommit
906 906
907 907 $ printf "foo\n" > foo.whole
908 908 $ hg commit -Aqm "add foo" --secret
909 909 $ hg fix -r .
910 910 $ hg log --template '{rev} {phase}\n'
911 911 1 secret
912 912 0 secret
913 913
914 914 $ cd ..
915 915
916 916 We should also preserve phase when fixing a draft commit while the user has
917 917 their default set to secret.
918 918
919 919 $ hg init respectphasesnewcommit
920 920 $ cd respectphasesnewcommit
921 921
922 922 $ printf "foo\n" > foo.whole
923 923 $ hg commit -Aqm "add foo"
924 924 $ hg --config phases.newcommit=secret fix -r .
925 925 $ hg log --template '{rev} {phase}\n'
926 926 1 draft
927 927 0 draft
928 928
929 929 $ cd ..
930 930
931 931 Debug output should show what fixer commands are being subprocessed, which is
932 932 useful for anyone trying to set up a new config.
933 933
934 934 $ hg init debugoutput
935 935 $ cd debugoutput
936 936
937 937 $ printf "foo\nbar\nbaz\n" > foo.changed
938 938 $ hg commit -Aqm "foo"
939 939 $ printf "Foo\nbar\nBaz\n" > foo.changed
940 940 $ hg --debug fix --working-dir
941 941 subprocess: * $TESTTMP/uppercase.py 1-1 3-3 (glob)
942 942
943 943 $ cd ..
944 944
945 945 Fixing an obsolete revision can cause divergence, so we abort unless the user
946 946 configures to allow it. This is not yet smart enough to know whether there is a
947 947 successor, but even then it is not likely intentional or idiomatic to fix an
948 948 obsolete revision.
949 949
950 950 $ hg init abortobsoleterev
951 951 $ cd abortobsoleterev
952 952
953 953 $ printf "foo\n" > foo.changed
954 954 $ hg commit -Aqm "foo"
955 955 $ hg debugobsolete `hg parents --template '{node}'`
956 956 obsoleted 1 changesets
957 957 $ hg --hidden fix -r 0
958 958 abort: fixing obsolete revision could cause divergence
959 959 [255]
960 960
961 961 $ hg --hidden fix -r 0 --config experimental.evolution.allowdivergence=true
962 962 $ hg cat -r tip foo.changed
963 963 FOO
964 964
965 965 $ cd ..
966 966
967 967 Test all of the available substitution values for fixer commands.
968 968
969 969 $ hg init substitution
970 970 $ cd substitution
971 971
972 972 $ mkdir foo
973 973 $ printf "hello\ngoodbye\n" > foo/bar
974 974 $ hg add
975 975 adding foo/bar
976 976 $ hg --config "fix.fail:command=printf '%s\n' '{rootpath}' '{basename}'" \
977 977 > --config "fix.fail:linerange='{first}' '{last}'" \
978 978 > --config "fix.fail:fileset=foo/bar" \
979 979 > fix --working-dir
980 980 $ cat foo/bar
981 981 foo/bar
982 982 bar
983 983 1
984 984 2
985 985
986 986 $ cd ..
987 987
988 988 The --base flag should allow picking the revisions to diff against for changed
989 989 files and incremental line formatting.
990 990
991 991 $ hg init baseflag
992 992 $ cd baseflag
993 993
994 994 $ printf "one\ntwo\n" > foo.changed
995 995 $ printf "bar\n" > bar.changed
996 996 $ hg commit -Aqm "first"
997 997 $ printf "one\nTwo\n" > foo.changed
998 998 $ hg commit -m "second"
999 999 $ hg fix -w --base .
1000 1000 $ hg status
1001 1001 $ hg fix -w --base null
1002 1002 $ cat foo.changed
1003 1003 ONE
1004 1004 TWO
1005 1005 $ cat bar.changed
1006 1006 BAR
1007 1007
1008 1008 $ cd ..
1009 1009
1010 1010 If the user asks to fix the parent of another commit, they are asking to create
1011 1011 an orphan. We must respect experimental.evolution.allowunstable.
1012 1012
1013 1013 $ hg init allowunstable
1014 1014 $ cd allowunstable
1015 1015
1016 1016 $ printf "one\n" > foo.whole
1017 1017 $ hg commit -Aqm "first"
1018 1018 $ printf "two\n" > foo.whole
1019 1019 $ hg commit -m "second"
1020 1020 $ hg --config experimental.evolution.allowunstable=False fix -r '.^'
1021 1021 abort: can only fix a changeset together with all its descendants
1022 1022 [255]
1023 1023 $ hg fix -r '.^'
1024 1024 1 new orphan changesets
1025 1025 $ hg cat -r 2 foo.whole
1026 1026 ONE
1027 1027
1028 1028 $ cd ..
1029 1029
1030 The --base flag affects the set of files being fixed. So while the --whole flag
1031 makes the base irrelevant for changed line ranges, it still changes the
1032 meaning and effect of the command. In this example, no files or lines are fixed
1033 until we specify the base, but then we do fix unchanged lines.
1034
1035 $ hg init basewhole
1036 $ cd basewhole
1037 $ printf "foo1\n" > foo.changed
1038 $ hg commit -Aqm "first"
1039 $ printf "foo2\n" >> foo.changed
1040 $ printf "bar\n" > bar.changed
1041 $ hg commit -Aqm "second"
1042
1043 $ hg fix --working-dir --whole
1044 $ cat *.changed
1045 bar
1046 foo1
1047 foo2
1048
1049 $ hg fix --working-dir --base 0 --whole
1050 $ cat *.changed
1051 BAR
1052 FOO1
1053 FOO2
1054
1055 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now