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