--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/fix.py Sat Mar 03 14:08:44 2018 -0800
@@ -0,0 +1,544 @@
+# fix - rewrite file content in changesets and working copy
+#
+# Copyright 2018 Google LLC.
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+"""rewrite file content in changesets or working copy (EXPERIMENTAL)
+
+Provides a command that runs configured tools on the contents of modified files,
+writing back any fixes to the working copy or replacing changesets.
+
+Here is an example configuration that causes :hg:`fix` to apply automatic
+formatting fixes to modified lines in C++ code::
+
+ [fix]
+ clang-format:command=clang-format --assume-filename={rootpath}
+ clang-format:linerange=--lines={first}:{last}
+ clang-format:fileset=set:**.cpp or **.hpp
+
+The :command suboption forms the first part of the shell command that will be
+used to fix a file. The content of the file is passed on standard input, and the
+fixed file content is expected on standard output. If there is any output on
+standard error, the file will not be affected. Some values may be substituted
+into the command::
+
+ {rootpath} The path of the file being fixed, relative to the repo root
+ {basename} The name of the file being fixed, without the directory path
+
+If the :linerange suboption is set, the tool will only be run if there are
+changed lines in a file. The value of this suboption is appended to the shell
+command once for every range of changed lines in the file. Some values may be
+substituted into the command::
+
+ {first} The 1-based line number of the first line in the modified range
+ {last} The 1-based line number of the last line in the modified range
+
+The :fileset suboption determines which files will be passed through each
+configured tool. See :hg:`help fileset` for possible values. If there are file
+arguments to :hg:`fix`, the intersection of these filesets is used.
+
+There is also a configurable limit for the maximum size of file that will be
+processed by :hg:`fix`::
+
+ [fix]
+ maxfilesize=2MB
+
+"""
+
+from __future__ import absolute_import
+
+import collections
+import itertools
+import os
+import re
+import subprocess
+import sys
+
+from mercurial.i18n import _
+from mercurial.node import nullrev
+from mercurial.node import wdirrev
+
+from mercurial import (
+ cmdutil,
+ context,
+ copies,
+ error,
+ match,
+ mdiff,
+ merge,
+ obsolete,
+ posix,
+ registrar,
+ scmutil,
+ util,
+)
+
+# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
+# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
+# be specifying the version(s) of Mercurial they are tested with, or
+# leave the attribute unspecified.
+testedwith = 'ships-with-hg-core'
+
+cmdtable = {}
+command = registrar.command(cmdtable)
+
+configtable = {}
+configitem = registrar.configitem(configtable)
+
+# Register the suboptions allowed for each configured fixer.
+FIXER_ATTRS = ('command', 'linerange', 'fileset')
+
+for key in FIXER_ATTRS:
+ configitem('fix', '.*(:%s)?' % key, default=None, generic=True)
+
+# A good default size allows most source code files to be fixed, but avoids
+# letting fixer tools choke on huge inputs, which could be surprising to the
+# user.
+configitem('fix', 'maxfilesize', default='2MB')
+
+@command('fix',
+ [('', 'base', [], _('revisions to diff against (overrides automatic '
+ 'selection, and applies to every revision being '
+ 'fixed)'), _('REV')),
+ ('r', 'rev', [], _('revisions to fix'), _('REV')),
+ ('w', 'working-dir', False, _('fix the working directory')),
+ ('', 'whole', False, _('always fix every line of a file'))],
+ _('[OPTION]... [FILE]...'))
+def fix(ui, repo, *pats, **opts):
+ """rewrite file content in changesets or working directory
+
+ Runs any configured tools to fix the content of files. Only affects files
+ with changes, unless file arguments are provided. Only affects changed lines
+ of files, unless the --whole flag is used. Some tools may always affect the
+ whole file regardless of --whole.
+
+ If revisions are specified with --rev, those revisions will be checked, and
+ they may be replaced with new revisions that have fixed file content. It is
+ desirable to specify all descendants of each specified revision, so that the
+ fixes propagate to the descendants. If all descendants are fixed at the same
+ time, no merging, rebasing, or evolution will be required.
+
+ If --working-dir is used, files with uncommitted changes in the working copy
+ will be fixed. If the checked-out revision is also fixed, the working
+ directory will update to the replacement revision.
+
+ When determining what lines of each file to fix at each revision, the whole
+ set of revisions being fixed is considered, so that fixes to earlier
+ revisions are not forgotten in later ones. The --base flag can be used to
+ override this default behavior, though it is not usually desirable to do so.
+ """
+ with repo.wlock(), repo.lock():
+ revstofix = getrevstofix(ui, repo, opts)
+ basectxs = getbasectxs(repo, opts, revstofix)
+ workqueue, numitems = getworkqueue(ui, repo, pats, opts, revstofix,
+ basectxs)
+ filedata = collections.defaultdict(dict)
+ replacements = {}
+ fixers = getfixers(ui)
+ # Some day this loop can become a worker pool, but for now it's easier
+ # to fix everything serially in topological order.
+ for rev, path in sorted(workqueue):
+ ctx = repo[rev]
+ olddata = ctx[path].data()
+ newdata = fixfile(ui, opts, fixers, ctx, path, basectxs[rev])
+ if newdata != olddata:
+ filedata[rev][path] = newdata
+ numitems[rev] -= 1
+ if not numitems[rev]:
+ if rev == wdirrev:
+ writeworkingdir(repo, ctx, filedata[rev], replacements)
+ else:
+ replacerev(ui, repo, ctx, filedata[rev], replacements)
+ del filedata[rev]
+
+ replacements = {prec: [succ] for prec, succ in replacements.iteritems()}
+ scmutil.cleanupnodes(repo, replacements, 'fix')
+
+def getworkqueue(ui, repo, pats, opts, revstofix, basectxs):
+ """"Constructs the list of files to be fixed at specific revisions
+
+ It is up to the caller how to consume the work items, and the only
+ dependence between them is that replacement revisions must be committed in
+ topological order. Each work item represents a file in the working copy or
+ in some revision that should be fixed and written back to the working copy
+ or into a replacement revision.
+ """
+ workqueue = []
+ numitems = collections.defaultdict(int)
+ maxfilesize = ui.configbytes('fix', 'maxfilesize')
+ for rev in revstofix:
+ fixctx = repo[rev]
+ match = scmutil.match(fixctx, pats, opts)
+ for path in pathstofix(ui, repo, pats, opts, match, basectxs[rev],
+ fixctx):
+ if path not in fixctx:
+ continue
+ fctx = fixctx[path]
+ if fctx.islink():
+ continue
+ if fctx.size() > maxfilesize:
+ ui.warn(_('ignoring file larger than %s: %s\n') %
+ (util.bytecount(maxfilesize), path))
+ continue
+ workqueue.append((rev, path))
+ numitems[rev] += 1
+ return workqueue, numitems
+
+def getrevstofix(ui, repo, opts):
+ """Returns the set of revision numbers that should be fixed"""
+ revs = set(scmutil.revrange(repo, opts['rev']))
+ for rev in revs:
+ checkfixablectx(ui, repo, repo[rev])
+ if revs:
+ cmdutil.checkunfinished(repo)
+ checknodescendants(repo, revs)
+ if opts.get('working_dir'):
+ revs.add(wdirrev)
+ if list(merge.mergestate.read(repo).unresolved()):
+ raise error.Abort('unresolved conflicts', hint="use 'hg resolve'")
+ if not revs:
+ raise error.Abort(
+ 'no changesets specified', hint='use --rev or --working-dir')
+ return revs
+
+def checknodescendants(repo, revs):
+ if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
+ repo.revs('(%ld::) - (%ld)', revs, revs)):
+ raise error.Abort(_('can only fix a changeset together '
+ 'with all its descendants'))
+
+def checkfixablectx(ui, repo, ctx):
+ """Aborts if the revision shouldn't be replaced with a fixed one."""
+ if not ctx.mutable():
+ raise error.Abort('can\'t fix immutable changeset %s' %
+ (scmutil.formatchangeid(ctx),))
+ if ctx.obsolete():
+ # It would be better to actually check if the revision has a successor.
+ allowdivergence = ui.configbool('experimental',
+ 'evolution.allowdivergence')
+ if not allowdivergence:
+ raise error.Abort('fixing obsolete revision could cause divergence')
+
+def pathstofix(ui, repo, pats, opts, match, basectxs, fixctx):
+ """Returns the set of files that should be fixed in a context
+
+ The result depends on the base contexts; we include any file that has
+ changed relative to any of the base contexts. Base contexts should be
+ ancestors of the context being fixed.
+ """
+ files = set()
+ for basectx in basectxs:
+ stat = repo.status(
+ basectx, fixctx, match=match, clean=bool(pats), unknown=bool(pats))
+ files.update(
+ set(itertools.chain(stat.added, stat.modified, stat.clean,
+ stat.unknown)))
+ return files
+
+def lineranges(opts, path, basectxs, fixctx, content2):
+ """Returns the set of line ranges that should be fixed in a file
+
+ Of the form [(10, 20), (30, 40)].
+
+ This depends on the given base contexts; we must consider lines that have
+ changed versus any of the base contexts, and whether the file has been
+ renamed versus any of them.
+
+ Another way to understand this is that we exclude line ranges that are
+ common to the file in all base contexts.
+ """
+ if opts.get('whole'):
+ # Return a range containing all lines. Rely on the diff implementation's
+ # idea of how many lines are in the file, instead of reimplementing it.
+ return difflineranges('', content2)
+
+ rangeslist = []
+ for basectx in basectxs:
+ basepath = copies.pathcopies(basectx, fixctx).get(path, path)
+ if basepath in basectx:
+ content1 = basectx[basepath].data()
+ else:
+ content1 = ''
+ rangeslist.extend(difflineranges(content1, content2))
+ return unionranges(rangeslist)
+
+def unionranges(rangeslist):
+ """Return the union of some closed intervals
+
+ >>> unionranges([])
+ []
+ >>> unionranges([(1, 100)])
+ [(1, 100)]
+ >>> unionranges([(1, 100), (1, 100)])
+ [(1, 100)]
+ >>> unionranges([(1, 100), (2, 100)])
+ [(1, 100)]
+ >>> unionranges([(1, 99), (1, 100)])
+ [(1, 100)]
+ >>> unionranges([(1, 100), (40, 60)])
+ [(1, 100)]
+ >>> unionranges([(1, 49), (50, 100)])
+ [(1, 100)]
+ >>> unionranges([(1, 48), (50, 100)])
+ [(1, 48), (50, 100)]
+ >>> unionranges([(1, 2), (3, 4), (5, 6)])
+ [(1, 6)]
+ """
+ rangeslist = sorted(set(rangeslist))
+ unioned = []
+ if rangeslist:
+ unioned, rangeslist = [rangeslist[0]], rangeslist[1:]
+ for a, b in rangeslist:
+ c, d = unioned[-1]
+ if a > d + 1:
+ unioned.append((a, b))
+ else:
+ unioned[-1] = (c, max(b, d))
+ return unioned
+
+def difflineranges(content1, content2):
+ """Return list of line number ranges in content2 that differ from content1.
+
+ Line numbers are 1-based. The numbers are the first and last line contained
+ in the range. Single-line ranges have the same line number for the first and
+ last line. Excludes any empty ranges that result from lines that are only
+ present in content1. Relies on mdiff's idea of where the line endings are in
+ the string.
+
+ >>> lines = lambda s: '\\n'.join([c for c in s])
+ >>> difflineranges2 = lambda a, b: difflineranges(lines(a), lines(b))
+ >>> difflineranges2('', '')
+ []
+ >>> difflineranges2('a', '')
+ []
+ >>> difflineranges2('', 'A')
+ [(1, 1)]
+ >>> difflineranges2('a', 'a')
+ []
+ >>> difflineranges2('a', 'A')
+ [(1, 1)]
+ >>> difflineranges2('ab', '')
+ []
+ >>> difflineranges2('', 'AB')
+ [(1, 2)]
+ >>> difflineranges2('abc', 'ac')
+ []
+ >>> difflineranges2('ab', 'aCb')
+ [(2, 2)]
+ >>> difflineranges2('abc', 'aBc')
+ [(2, 2)]
+ >>> difflineranges2('ab', 'AB')
+ [(1, 2)]
+ >>> difflineranges2('abcde', 'aBcDe')
+ [(2, 2), (4, 4)]
+ >>> difflineranges2('abcde', 'aBCDe')
+ [(2, 4)]
+ """
+ ranges = []
+ for lines, kind in mdiff.allblocks(content1, content2):
+ firstline, lastline = lines[2:4]
+ if kind == '!' and firstline != lastline:
+ ranges.append((firstline + 1, lastline))
+ return ranges
+
+def getbasectxs(repo, opts, revstofix):
+ """Returns a map of the base contexts for each revision
+
+ The base contexts determine which lines are considered modified when we
+ attempt to fix just the modified lines in a file.
+ """
+ # The --base flag overrides the usual logic, and we give every revision
+ # exactly the set of baserevs that the user specified.
+ if opts.get('base'):
+ baserevs = set(scmutil.revrange(repo, opts.get('base')))
+ if not baserevs:
+ baserevs = {nullrev}
+ basectxs = {repo[rev] for rev in baserevs}
+ return {rev: basectxs for rev in revstofix}
+
+ # Proceed in topological order so that we can easily determine each
+ # revision's baserevs by looking at its parents and their baserevs.
+ basectxs = collections.defaultdict(set)
+ for rev in sorted(revstofix):
+ ctx = repo[rev]
+ for pctx in ctx.parents():
+ if pctx.rev() in basectxs:
+ basectxs[rev].update(basectxs[pctx.rev()])
+ else:
+ basectxs[rev].add(pctx)
+ return basectxs
+
+def fixfile(ui, opts, fixers, fixctx, path, basectxs):
+ """Run any configured fixers that should affect the file in this context
+
+ Returns the file content that results from applying the fixers in some order
+ starting with the file's content in the fixctx. Fixers that support line
+ ranges will affect lines that have changed relative to any of the basectxs
+ (i.e. they will only avoid lines that are common to all basectxs).
+ """
+ newdata = fixctx[path].data()
+ for fixername, fixer in fixers.iteritems():
+ if fixer.affects(opts, fixctx, path):
+ ranges = lineranges(opts, path, basectxs, fixctx, newdata)
+ command = fixer.command(path, ranges)
+ if command is None:
+ continue
+ ui.debug('subprocess: %s\n' % (command,))
+ proc = subprocess.Popen(
+ command,
+ shell=True,
+ cwd='/',
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ newerdata, stderr = proc.communicate(newdata)
+ if stderr:
+ showstderr(ui, fixctx.rev(), fixername, stderr)
+ else:
+ newdata = newerdata
+ return newdata
+
+def showstderr(ui, rev, fixername, stderr):
+ """Writes the lines of the stderr string as warnings on the ui
+
+ Uses the revision number and fixername to give more context to each line of
+ the error message. Doesn't include file names, since those take up a lot of
+ space and would tend to be included in the error message if they were
+ relevant.
+ """
+ for line in re.split('[\r\n]+', stderr):
+ if line:
+ ui.warn(('['))
+ if rev is None:
+ ui.warn(_('wdir'), label='evolve.rev')
+ else:
+ ui.warn((str(rev)), label='evolve.rev')
+ ui.warn(('] %s: %s\n') % (fixername, line))
+
+def writeworkingdir(repo, ctx, filedata, replacements):
+ """Write new content to the working copy and check out the new p1 if any
+
+ We check out a new revision if and only if we fixed something in both the
+ working directory and its parent revision. This avoids the need for a full
+ update/merge, and means that the working directory simply isn't affected
+ unless the --working-dir flag is given.
+
+ Directly updates the dirstate for the affected files.
+ """
+ for path, data in filedata.iteritems():
+ fctx = ctx[path]
+ fctx.write(data, fctx.flags())
+ if repo.dirstate[path] == 'n':
+ repo.dirstate.normallookup(path)
+
+ oldparentnodes = repo.dirstate.parents()
+ newparentnodes = [replacements.get(n, n) for n in oldparentnodes]
+ if newparentnodes != oldparentnodes:
+ repo.setparents(*newparentnodes)
+
+def replacerev(ui, repo, ctx, filedata, replacements):
+ """Commit a new revision like the given one, but with file content changes
+
+ "ctx" is the original revision to be replaced by a modified one.
+
+ "filedata" is a dict that maps paths to their new file content. All other
+ paths will be recreated from the original revision without changes.
+ "filedata" may contain paths that didn't exist in the original revision;
+ they will be added.
+
+ "replacements" is a dict that maps a single node to a single node, and it is
+ updated to indicate the original revision is replaced by the newly created
+ one. No entry is added if the replacement's node already exists.
+
+ The new revision has the same parents as the old one, unless those parents
+ have already been replaced, in which case those replacements are the parents
+ of this new revision. Thus, if revisions are replaced in topological order,
+ there is no need to rebase them into the original topology later.
+ """
+
+ p1rev, p2rev = repo.changelog.parentrevs(ctx.rev())
+ p1ctx, p2ctx = repo[p1rev], repo[p2rev]
+ newp1node = replacements.get(p1ctx.node(), p1ctx.node())
+ newp2node = replacements.get(p2ctx.node(), p2ctx.node())
+
+ def filectxfn(repo, memctx, path):
+ if path not in ctx:
+ return None
+ fctx = ctx[path]
+ copied = fctx.renamed()
+ if copied:
+ copied = copied[0]
+ return context.memfilectx(
+ repo,
+ memctx,
+ path=fctx.path(),
+ data=filedata.get(path, fctx.data()),
+ islink=fctx.islink(),
+ isexec=fctx.isexec(),
+ copied=copied)
+
+ overrides = {('phases', 'new-commit'): ctx.phase()}
+ with ui.configoverride(overrides, source='fix'):
+ memctx = context.memctx(
+ repo,
+ parents=(newp1node, newp2node),
+ text=ctx.description(),
+ files=set(ctx.files()) | set(filedata.keys()),
+ filectxfn=filectxfn,
+ user=ctx.user(),
+ date=ctx.date(),
+ extra=ctx.extra(),
+ branch=ctx.branch(),
+ editor=None)
+ sucnode = memctx.commit()
+ prenode = ctx.node()
+ if prenode == sucnode:
+ ui.debug('node %s already existed\n' % (ctx.hex()))
+ else:
+ replacements[ctx.node()] = sucnode
+
+def getfixers(ui):
+ """Returns a map of configured fixer tools indexed by their names
+
+ Each value is a Fixer object with methods that implement the behavior of the
+ fixer's config suboptions. Does not validate the config values.
+ """
+ result = {}
+ for name in fixernames(ui):
+ result[name] = Fixer()
+ attrs = ui.configsuboptions('fix', name)[1]
+ for key in FIXER_ATTRS:
+ setattr(result[name], '_' + key, attrs.get(key, ''))
+ return result
+
+def fixernames(ui):
+ """Returns the names of [fix] config options that have suboptions"""
+ names = set()
+ for k, v in ui.configitems('fix'):
+ if ':' in k:
+ names.add(k.split(':', 1)[0])
+ return names
+
+class Fixer(object):
+ """Wraps the raw config values for a fixer with methods"""
+
+ def affects(self, opts, fixctx, path):
+ """Should this fixer run on the file at the given path and context?"""
+ return scmutil.match(fixctx, [self._fileset], opts)(path)
+
+ def command(self, path, ranges):
+ """A shell command to use to invoke this fixer on the given file/lines
+
+ May return None if there is no appropriate command to run for the given
+ parameters.
+ """
+ parts = [self._command.format(rootpath=path,
+ basename=os.path.basename(path))]
+ if self._linerange:
+ if not ranges:
+ # No line ranges to fix, so don't run the fixer.
+ return None
+ for first, last in ranges:
+ parts.append(self._linerange.format(first=first, last=last))
+ return ' '.join(parts)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-fix.t Sat Mar 03 14:08:44 2018 -0800
@@ -0,0 +1,969 @@
+Set up the config with two simple fixers: one that fixes specific line ranges,
+and one that always fixes the whole file. They both "fix" files by converting
+letters to uppercase. They use different file extensions, so each test case can
+choose which behavior to use by naming files.
+
+ $ cat >> $HGRCPATH <<EOF
+ > [extensions]
+ > fix =
+ > [experimental]
+ > evolution.createmarkers=True
+ > evolution.allowunstable=True
+ > [fix]
+ > uppercase-whole-file:command=sed -e 's/.*/\U&/'
+ > uppercase-whole-file:fileset=set:**.whole
+ > uppercase-changed-lines:command=sed
+ > uppercase-changed-lines:linerange=-e '{first},{last} s/.*/\U&/'
+ > uppercase-changed-lines:fileset=set:**.changed
+ > EOF
+
+Help text for fix.
+
+ $ hg help fix
+ hg fix [OPTION]... [FILE]...
+
+ rewrite file content in changesets or working directory
+
+ Runs any configured tools to fix the content of files. Only affects files
+ with changes, unless file arguments are provided. Only affects changed
+ lines of files, unless the --whole flag is used. Some tools may always
+ affect the whole file regardless of --whole.
+
+ If revisions are specified with --rev, those revisions will be checked,
+ and they may be replaced with new revisions that have fixed file content.
+ It is desirable to specify all descendants of each specified revision, so
+ that the fixes propagate to the descendants. If all descendants are fixed
+ at the same time, no merging, rebasing, or evolution will be required.
+
+ If --working-dir is used, files with uncommitted changes in the working
+ copy will be fixed. If the checked-out revision is also fixed, the working
+ directory will update to the replacement revision.
+
+ When determining what lines of each file to fix at each revision, the
+ whole set of revisions being fixed is considered, so that fixes to earlier
+ revisions are not forgotten in later ones. The --base flag can be used to
+ override this default behavior, though it is not usually desirable to do
+ so.
+
+ (use 'hg help -e fix' to show help for the fix extension)
+
+ options ([+] can be repeated):
+
+ --base REV [+] revisions to diff against (overrides automatic selection,
+ and applies to every revision being fixed)
+ -r --rev REV [+] revisions to fix
+ -w --working-dir fix the working directory
+ --whole always fix every line of a file
+
+ (some details hidden, use --verbose to show complete help)
+
+ $ hg help -e fix
+ fix extension - rewrite file content in changesets or working copy
+ (EXPERIMENTAL)
+
+ Provides a command that runs configured tools on the contents of modified
+ files, writing back any fixes to the working copy or replacing changesets.
+
+ Here is an example configuration that causes 'hg fix' to apply automatic
+ formatting fixes to modified lines in C++ code:
+
+ [fix]
+ clang-format:command=clang-format --assume-filename={rootpath}
+ clang-format:linerange=--lines={first}:{last}
+ clang-format:fileset=set:**.cpp or **.hpp
+
+ The :command suboption forms the first part of the shell command that will be
+ used to fix a file. The content of the file is passed on standard input, and
+ the fixed file content is expected on standard output. If there is any output
+ on standard error, the file will not be affected. Some values may be
+ substituted into the command:
+
+ {rootpath} The path of the file being fixed, relative to the repo root
+ {basename} The name of the file being fixed, without the directory path
+
+ If the :linerange suboption is set, the tool will only be run if there are
+ changed lines in a file. The value of this suboption is appended to the shell
+ command once for every range of changed lines in the file. Some values may be
+ substituted into the command:
+
+ {first} The 1-based line number of the first line in the modified range
+ {last} The 1-based line number of the last line in the modified range
+
+ The :fileset suboption determines which files will be passed through each
+ configured tool. See 'hg help fileset' for possible values. If there are file
+ arguments to 'hg fix', the intersection of these filesets is used.
+
+ There is also a configurable limit for the maximum size of file that will be
+ processed by 'hg fix':
+
+ [fix]
+ maxfilesize=2MB
+
+ list of commands:
+
+ fix rewrite file content in changesets or working directory
+
+ (use 'hg help -v -e fix' to show built-in aliases and global options)
+
+There is no default behavior in the absence of --rev and --working-dir.
+
+ $ hg init badusage
+ $ cd badusage
+
+ $ hg fix
+ abort: no changesets specified
+ (use --rev or --working-dir)
+ [255]
+ $ hg fix --whole
+ abort: no changesets specified
+ (use --rev or --working-dir)
+ [255]
+ $ hg fix --base 0
+ abort: no changesets specified
+ (use --rev or --working-dir)
+ [255]
+
+Fixing a public revision isn't allowed. It should abort early enough that
+nothing happens, even to the working directory.
+
+ $ printf "hello\n" > hello.whole
+ $ hg commit -Aqm "hello"
+ $ hg phase -r 0 --public
+ $ hg fix -r 0
+ abort: can't fix immutable changeset 0:6470986d2e7b
+ [255]
+ $ hg fix -r 0 --working-dir
+ abort: can't fix immutable changeset 0:6470986d2e7b
+ [255]
+ $ hg cat -r tip hello.whole
+ hello
+ $ cat hello.whole
+ hello
+
+ $ cd ..
+
+Fixing a clean working directory should do nothing. Even the --whole flag
+shouldn't cause any clean files to be fixed. Specifying a clean file explicitly
+should only fix it if the fixer always fixes the whole file. The combination of
+an explicit filename and --whole should format the entire file regardless.
+
+ $ hg init fixcleanwdir
+ $ cd fixcleanwdir
+
+ $ printf "hello\n" > hello.changed
+ $ printf "world\n" > hello.whole
+ $ hg commit -Aqm "foo"
+ $ hg fix --working-dir
+ $ hg diff
+ $ hg fix --working-dir --whole
+ $ hg diff
+ $ hg fix --working-dir *
+ $ cat *
+ hello
+ WORLD
+ $ hg revert --all --no-backup
+ reverting hello.whole
+ $ hg fix --working-dir * --whole
+ $ cat *
+ HELLO
+ WORLD
+
+The same ideas apply to fixing a revision, so we create a revision that doesn't
+modify either of the files in question and try fixing it. This also tests that
+we ignore a file that doesn't match any configured fixer.
+
+ $ hg revert --all --no-backup
+ reverting hello.changed
+ reverting hello.whole
+ $ printf "unimportant\n" > some.file
+ $ hg commit -Aqm "some other file"
+
+ $ hg fix -r .
+ $ hg cat -r tip *
+ hello
+ world
+ unimportant
+ $ hg fix -r . --whole
+ $ hg cat -r tip *
+ hello
+ world
+ unimportant
+ $ hg fix -r . *
+ $ hg cat -r tip *
+ hello
+ WORLD
+ unimportant
+ $ hg fix -r . * --whole --config experimental.evolution.allowdivergence=true
+ 2 new content-divergent changesets
+ $ hg cat -r tip *
+ HELLO
+ WORLD
+ unimportant
+
+ $ cd ..
+
+Fixing the working directory should still work if there are no revisions.
+
+ $ hg init norevisions
+ $ cd norevisions
+
+ $ printf "something\n" > something.whole
+ $ hg add
+ adding something.whole
+ $ hg fix --working-dir
+ $ cat something.whole
+ SOMETHING
+
+ $ cd ..
+
+Test the effect of fixing the working directory for each possible status, with
+and without providing explicit file arguments.
+
+ $ hg init implicitlyfixstatus
+ $ cd implicitlyfixstatus
+
+ $ printf "modified\n" > modified.whole
+ $ printf "removed\n" > removed.whole
+ $ printf "deleted\n" > deleted.whole
+ $ printf "clean\n" > clean.whole
+ $ printf "ignored.whole" > .hgignore
+ $ hg commit -Aqm "stuff"
+
+ $ printf "modified!!!\n" > modified.whole
+ $ printf "unknown\n" > unknown.whole
+ $ printf "ignored\n" > ignored.whole
+ $ printf "added\n" > added.whole
+ $ hg add added.whole
+ $ hg remove removed.whole
+ $ rm deleted.whole
+
+ $ hg status --all
+ M modified.whole
+ A added.whole
+ R removed.whole
+ ! deleted.whole
+ ? unknown.whole
+ I ignored.whole
+ C .hgignore
+ C clean.whole
+
+ $ hg fix --working-dir
+
+ $ hg status --all
+ M modified.whole
+ A added.whole
+ R removed.whole
+ ! deleted.whole
+ ? unknown.whole
+ I ignored.whole
+ C .hgignore
+ C clean.whole
+
+ $ cat *.whole
+ ADDED
+ clean
+ ignored
+ MODIFIED!!!
+ unknown
+
+ $ printf "modified!!!\n" > modified.whole
+ $ printf "added\n" > added.whole
+ $ hg fix --working-dir *.whole
+
+ $ hg status --all
+ M clean.whole
+ M modified.whole
+ A added.whole
+ R removed.whole
+ ! deleted.whole
+ ? unknown.whole
+ I ignored.whole
+ C .hgignore
+
+It would be better if this also fixed the unknown file.
+ $ cat *.whole
+ ADDED
+ CLEAN
+ ignored
+ MODIFIED!!!
+ unknown
+
+ $ cd ..
+
+Test that incremental fixing works on files with additions, deletions, and
+changes in multiple line ranges. Note that deletions do not generally cause
+neighboring lines to be fixed, so we don't return a line range for purely
+deleted sections. In the future we should support a :deletion config that
+allows fixers to know where deletions are located.
+
+ $ hg init incrementalfixedlines
+ $ cd incrementalfixedlines
+
+ $ printf "a\nb\nc\nd\ne\nf\ng\n" > foo.txt
+ $ hg commit -Aqm "foo"
+ $ printf "zz\na\nc\ndd\nee\nff\nf\ngg\n" > foo.txt
+
+ $ hg --config "fix.fail:command=echo" \
+ > --config "fix.fail:linerange={first}:{last}" \
+ > --config "fix.fail:fileset=foo.txt" \
+ > fix --working-dir
+ $ cat foo.txt
+ 1:1 4:6 8:8
+
+ $ cd ..
+
+Test that --whole fixes all lines regardless of the diffs present.
+
+ $ hg init wholeignoresdiffs
+ $ cd wholeignoresdiffs
+
+ $ printf "a\nb\nc\nd\ne\nf\ng\n" > foo.changed
+ $ hg commit -Aqm "foo"
+ $ printf "zz\na\nc\ndd\nee\nff\nf\ngg\n" > foo.changed
+ $ hg fix --working-dir --whole
+ $ cat foo.changed
+ ZZ
+ A
+ C
+ DD
+ EE
+ FF
+ F
+ GG
+
+ $ cd ..
+
+We should do nothing with symlinks, and their targets should be unaffected. Any
+other behavior would be more complicated to implement and harder to document.
+
+#if symlink
+ $ hg init dontmesswithsymlinks
+ $ cd dontmesswithsymlinks
+
+ $ printf "hello\n" > hello.whole
+ $ ln -s hello.whole hellolink
+ $ hg add
+ adding hello.whole
+ adding hellolink
+ $ hg fix --working-dir hellolink
+ $ hg status
+ A hello.whole
+ A hellolink
+
+ $ cd ..
+#endif
+
+We should allow fixers to run on binary files, even though this doesn't sound
+like a common use case. There's not much benefit to disallowing it, and users
+can add "and not binary()" to their filesets if needed. The Mercurial
+philosophy is generally to not handle binary files specially anyway.
+
+ $ hg init cantouchbinaryfiles
+ $ cd cantouchbinaryfiles
+
+ $ printf "hello\0\n" > hello.whole
+ $ hg add
+ adding hello.whole
+ $ hg fix --working-dir 'set:binary()'
+ $ cat hello.whole
+ HELLO\x00 (esc)
+
+ $ cd ..
+
+We have a config for the maximum size of file we will attempt to fix. This can
+be helpful to avoid running unsuspecting fixer tools on huge inputs, which
+could happen by accident without a well considered configuration. A more
+precise configuration could use the size() fileset function if one global limit
+is undesired.
+
+ $ hg init maxfilesize
+ $ cd maxfilesize
+
+ $ printf "this file is huge\n" > hello.whole
+ $ hg add
+ adding hello.whole
+ $ hg --config fix.maxfilesize=10 fix --working-dir
+ ignoring file larger than 10 bytes: hello.whole
+ $ cat hello.whole
+ this file is huge
+
+ $ cd ..
+
+If we specify a file to fix, other files should be left alone, even if they
+have changes.
+
+ $ hg init fixonlywhatitellyouto
+ $ cd fixonlywhatitellyouto
+
+ $ printf "fix me!\n" > fixme.whole
+ $ printf "not me.\n" > notme.whole
+ $ hg add
+ adding fixme.whole
+ adding notme.whole
+ $ hg fix --working-dir fixme.whole
+ $ cat *.whole
+ FIX ME!
+ not me.
+
+ $ cd ..
+
+Specifying a directory name should fix all its files and subdirectories.
+
+ $ hg init fixdirectory
+ $ cd fixdirectory
+
+ $ mkdir -p dir1/dir2
+ $ printf "foo\n" > foo.whole
+ $ printf "bar\n" > dir1/bar.whole
+ $ printf "baz\n" > dir1/dir2/baz.whole
+ $ hg add
+ adding dir1/bar.whole
+ adding dir1/dir2/baz.whole
+ adding foo.whole
+ $ hg fix --working-dir dir1
+ $ cat foo.whole dir1/bar.whole dir1/dir2/baz.whole
+ foo
+ BAR
+ BAZ
+
+ $ cd ..
+
+Fixing a file in the working directory that needs no fixes should not actually
+write back to the file, so for example the mtime shouldn't change.
+
+ $ hg init donttouchunfixedfiles
+ $ cd donttouchunfixedfiles
+
+ $ printf "NO FIX NEEDED\n" > foo.whole
+ $ hg add
+ adding foo.whole
+ $ OLD_MTIME=`stat -c %Y foo.whole`
+ $ sleep 1 # mtime has a resolution of one second.
+ $ hg fix --working-dir
+ $ NEW_MTIME=`stat -c %Y foo.whole`
+ $ test $OLD_MTIME = $NEW_MTIME
+
+ $ cd ..
+
+When a fixer prints to stderr, we assume that it has failed. We should show the
+error messages to the user, and we should not let the failing fixer affect the
+file it was fixing (many code formatters might emit error messages on stderr
+and nothing on stdout, which would cause us the clear the file). We show the
+user which fixer failed and which revision, but we assume that the fixer will
+print the filename if it is relevant.
+
+ $ hg init showstderr
+ $ cd showstderr
+
+ $ printf "hello\n" > hello.txt
+ $ hg add
+ adding hello.txt
+ $ hg --config "fix.fail:command=printf 'HELLO\n' ; \
+ > printf '{rootpath}: some\nerror' >&2" \
+ > --config "fix.fail:fileset=hello.txt" \
+ > fix --working-dir
+ [wdir] fail: hello.txt: some
+ [wdir] fail: error
+ $ cat hello.txt
+ hello
+
+ $ cd ..
+
+Fixing the working directory and its parent revision at the same time should
+check out the replacement revision for the parent. This prevents any new
+uncommitted changes from appearing. We test this for a clean working directory
+and a dirty one. In both cases, all lines/files changed since the grandparent
+will be fixed. The grandparent is the "baserev" for both the parent and the
+working copy.
+
+ $ hg init fixdotandcleanwdir
+ $ cd fixdotandcleanwdir
+
+ $ printf "hello\n" > hello.whole
+ $ printf "world\n" > world.whole
+ $ hg commit -Aqm "the parent commit"
+
+ $ hg parents --template '{rev} {desc}\n'
+ 0 the parent commit
+ $ hg fix --working-dir -r .
+ $ hg parents --template '{rev} {desc}\n'
+ 1 the parent commit
+ $ hg cat -r . *.whole
+ HELLO
+ WORLD
+ $ cat *.whole
+ HELLO
+ WORLD
+ $ hg status
+
+ $ cd ..
+
+Same test with a dirty working copy.
+
+ $ hg init fixdotanddirtywdir
+ $ cd fixdotanddirtywdir
+
+ $ printf "hello\n" > hello.whole
+ $ printf "world\n" > world.whole
+ $ hg commit -Aqm "the parent commit"
+
+ $ printf "hello,\n" > hello.whole
+ $ printf "world!\n" > world.whole
+
+ $ hg parents --template '{rev} {desc}\n'
+ 0 the parent commit
+ $ hg fix --working-dir -r .
+ $ hg parents --template '{rev} {desc}\n'
+ 1 the parent commit
+ $ hg cat -r . *.whole
+ HELLO
+ WORLD
+ $ cat *.whole
+ HELLO,
+ WORLD!
+ $ hg status
+ M hello.whole
+ M world.whole
+
+ $ cd ..
+
+When we have a chain of commits that change mutually exclusive lines of code,
+we should be able to do incremental fixing that causes each commit in the chain
+to include fixes made to the previous commits. This prevents children from
+backing out the fixes made in their parents. A dirty working directory is
+conceptually similar to another commit in the chain.
+
+ $ hg init incrementallyfixchain
+ $ cd incrementallyfixchain
+
+ $ cat > file.changed <<EOF
+ > first
+ > second
+ > third
+ > fourth
+ > fifth
+ > EOF
+ $ hg commit -Aqm "the common ancestor (the baserev)"
+ $ cat > file.changed <<EOF
+ > first (changed)
+ > second
+ > third
+ > fourth
+ > fifth
+ > EOF
+ $ hg commit -Aqm "the first commit to fix"
+ $ cat > file.changed <<EOF
+ > first (changed)
+ > second
+ > third (changed)
+ > fourth
+ > fifth
+ > EOF
+ $ hg commit -Aqm "the second commit to fix"
+ $ cat > file.changed <<EOF
+ > first (changed)
+ > second
+ > third (changed)
+ > fourth
+ > fifth (changed)
+ > EOF
+
+ $ hg fix -r . -r '.^' --working-dir
+
+ $ hg parents --template '{rev}\n'
+ 4
+ $ hg cat -r '.^^' file.changed
+ first
+ second
+ third
+ fourth
+ fifth
+ $ hg cat -r '.^' file.changed
+ FIRST (CHANGED)
+ second
+ third
+ fourth
+ fifth
+ $ hg cat -r . file.changed
+ FIRST (CHANGED)
+ second
+ THIRD (CHANGED)
+ fourth
+ fifth
+ $ cat file.changed
+ FIRST (CHANGED)
+ second
+ THIRD (CHANGED)
+ fourth
+ FIFTH (CHANGED)
+
+ $ cd ..
+
+If we incrementally fix a merge commit, we should fix any lines that changed
+versus either parent. You could imagine only fixing the intersection or some
+other subset, but this is necessary if either parent is being fixed. It
+prevents us from forgetting fixes made in either parent.
+
+ $ hg init incrementallyfixmergecommit
+ $ cd incrementallyfixmergecommit
+
+ $ printf "a\nb\nc\n" > file.changed
+ $ hg commit -Aqm "ancestor"
+
+ $ printf "aa\nb\nc\n" > file.changed
+ $ hg commit -m "change a"
+
+ $ hg checkout '.^'
+ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ $ printf "a\nb\ncc\n" > file.changed
+ $ hg commit -m "change c"
+ created new head
+
+ $ hg merge
+ merging file.changed
+ 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+ (branch merge, don't forget to commit)
+ $ hg commit -m "merge"
+ $ hg cat -r . file.changed
+ aa
+ b
+ cc
+
+ $ hg fix -r . --working-dir
+ $ hg cat -r . file.changed
+ AA
+ b
+ CC
+
+ $ cd ..
+
+Abort fixing revisions if there is an unfinished operation. We don't want to
+make things worse by editing files or stripping/obsoleting things. Also abort
+fixing the working directory if there are unresolved merge conflicts.
+
+ $ hg init abortunresolved
+ $ cd abortunresolved
+
+ $ echo "foo1" > foo.whole
+ $ hg commit -Aqm "foo 1"
+
+ $ hg update null
+ 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+ $ echo "foo2" > foo.whole
+ $ hg commit -Aqm "foo 2"
+
+ $ hg --config extensions.rebase= rebase -r 1 -d 0
+ rebasing 1:c3b6dc0e177a "foo 2" (tip)
+ merging foo.whole
+ warning: conflicts while merging foo.whole! (edit, then use 'hg resolve --mark')
+ unresolved conflicts (see hg resolve, then hg rebase --continue)
+ [1]
+
+ $ hg --config extensions.rebase= fix --working-dir
+ abort: unresolved conflicts
+ (use 'hg resolve')
+ [255]
+
+ $ hg --config extensions.rebase= fix -r .
+ abort: rebase in progress
+ (use 'hg rebase --continue' or 'hg rebase --abort')
+ [255]
+
+When fixing a file that was renamed, we should diff against the source of the
+rename for incremental fixing and we should correctly reproduce the rename in
+the replacement revision.
+
+ $ hg init fixrenamecommit
+ $ cd fixrenamecommit
+
+ $ printf "a\nb\nc\n" > source.changed
+ $ hg commit -Aqm "source revision"
+ $ hg move source.changed dest.changed
+ $ printf "a\nb\ncc\n" > dest.changed
+ $ hg commit -m "dest revision"
+
+ $ hg fix -r .
+ $ hg log -r tip --copies --template "{file_copies}\n"
+ dest.changed (source.changed)
+ $ hg cat -r tip dest.changed
+ a
+ b
+ CC
+
+ $ cd ..
+
+When fixing revisions that remove files we must ensure that the replacement
+actually removes the file, whereas it could accidentally leave it unchanged or
+write an empty string to it.
+
+ $ hg init fixremovedfile
+ $ cd fixremovedfile
+
+ $ printf "foo\n" > foo.whole
+ $ printf "bar\n" > bar.whole
+ $ hg commit -Aqm "add files"
+ $ hg remove bar.whole
+ $ hg commit -m "remove file"
+ $ hg status --change .
+ R bar.whole
+ $ hg fix -r . foo.whole
+ $ hg status --change tip
+ M foo.whole
+ R bar.whole
+
+ $ cd ..
+
+If fixing a revision finds no fixes to make, no replacement revision should be
+created.
+
+ $ hg init nofixesneeded
+ $ cd nofixesneeded
+
+ $ printf "FOO\n" > foo.whole
+ $ hg commit -Aqm "add file"
+ $ hg log --template '{rev}\n'
+ 0
+ $ hg fix -r .
+ $ hg log --template '{rev}\n'
+ 0
+
+ $ cd ..
+
+If fixing a commit reverts all the changes in the commit, we replace it with a
+commit that changes no files.
+
+ $ hg init nochangesleft
+ $ cd nochangesleft
+
+ $ printf "FOO\n" > foo.whole
+ $ hg commit -Aqm "add file"
+ $ printf "foo\n" > foo.whole
+ $ hg commit -m "edit file"
+ $ hg status --change .
+ M foo.whole
+ $ hg fix -r .
+ $ hg status --change tip
+
+ $ cd ..
+
+If we fix a parent and child revision together, the child revision must be
+replaced if the parent is replaced, even if the diffs of the child needed no
+fixes. However, we're free to not replace revisions that need no fixes and have
+no ancestors that are replaced.
+
+ $ hg init mustreplacechild
+ $ cd mustreplacechild
+
+ $ printf "FOO\n" > foo.whole
+ $ hg commit -Aqm "add foo"
+ $ printf "foo\n" > foo.whole
+ $ hg commit -m "edit foo"
+ $ printf "BAR\n" > bar.whole
+ $ hg commit -Aqm "add bar"
+
+ $ hg log --graph --template '{node|shortest} {files}'
+ @ bc05 bar.whole
+ |
+ o 4fd2 foo.whole
+ |
+ o f9ac foo.whole
+
+ $ hg fix -r 0:2
+ $ hg log --graph --template '{node|shortest} {files}'
+ o 3801 bar.whole
+ |
+ o 38cc
+ |
+ | @ bc05 bar.whole
+ | |
+ | x 4fd2 foo.whole
+ |/
+ o f9ac foo.whole
+
+
+ $ cd ..
+
+It's also possible that the child needs absolutely no changes, but we still
+need to replace it to update its parent. If we skipped replacing the child
+because it had no file content changes, it would become an orphan for no good
+reason.
+
+ $ hg init mustreplacechildevenifnop
+ $ cd mustreplacechildevenifnop
+
+ $ printf "Foo\n" > foo.whole
+ $ hg commit -Aqm "add a bad foo"
+ $ printf "FOO\n" > foo.whole
+ $ hg commit -m "add a good foo"
+ $ hg fix -r . -r '.^'
+ $ hg log --graph --template '{rev} {desc}'
+ o 3 add a good foo
+ |
+ o 2 add a bad foo
+
+ @ 1 add a good foo
+ |
+ x 0 add a bad foo
+
+
+ $ cd ..
+
+Similar to the case above, the child revision may become empty as a result of
+fixing its parent. We should still create an empty replacement child.
+TODO: determine how this should interact with ui.allowemptycommit given that
+the empty replacement could have children.
+
+ $ hg init mustreplacechildevenifempty
+ $ cd mustreplacechildevenifempty
+
+ $ printf "foo\n" > foo.whole
+ $ hg commit -Aqm "add foo"
+ $ printf "Foo\n" > foo.whole
+ $ hg commit -m "edit foo"
+ $ hg fix -r . -r '.^'
+ $ hg log --graph --template '{rev} {desc}\n' --stat
+ o 3 edit foo
+ |
+ o 2 add foo
+ foo.whole | 1 +
+ 1 files changed, 1 insertions(+), 0 deletions(-)
+
+ @ 1 edit foo
+ | foo.whole | 2 +-
+ | 1 files changed, 1 insertions(+), 1 deletions(-)
+ |
+ x 0 add foo
+ foo.whole | 1 +
+ 1 files changed, 1 insertions(+), 0 deletions(-)
+
+
+ $ cd ..
+
+Fixing a secret commit should replace it with another secret commit.
+
+ $ hg init fixsecretcommit
+ $ cd fixsecretcommit
+
+ $ printf "foo\n" > foo.whole
+ $ hg commit -Aqm "add foo" --secret
+ $ hg fix -r .
+ $ hg log --template '{rev} {phase}\n'
+ 1 secret
+ 0 secret
+
+ $ cd ..
+
+We should also preserve phase when fixing a draft commit while the user has
+their default set to secret.
+
+ $ hg init respectphasesnewcommit
+ $ cd respectphasesnewcommit
+
+ $ printf "foo\n" > foo.whole
+ $ hg commit -Aqm "add foo"
+ $ hg --config phases.newcommit=secret fix -r .
+ $ hg log --template '{rev} {phase}\n'
+ 1 draft
+ 0 draft
+
+ $ cd ..
+
+Debug output should show what fixer commands are being subprocessed, which is
+useful for anyone trying to set up a new config.
+
+ $ hg init debugoutput
+ $ cd debugoutput
+
+ $ printf "foo\nbar\nbaz\n" > foo.changed
+ $ hg commit -Aqm "foo"
+ $ printf "Foo\nbar\nBaz\n" > foo.changed
+ $ hg --debug fix --working-dir
+ subprocess: sed -e '1,1 s/.*/\U&/' -e '3,3 s/.*/\U&/'
+
+ $ cd ..
+
+Fixing an obsolete revision can cause divergence, so we abort unless the user
+configures to allow it. This is not yet smart enough to know whether there is a
+successor, but even then it is not likely intentional or idiomatic to fix an
+obsolete revision.
+
+ $ hg init abortobsoleterev
+ $ cd abortobsoleterev
+
+ $ printf "foo\n" > foo.changed
+ $ hg commit -Aqm "foo"
+ $ hg debugobsolete `hg parents --template '{node}'`
+ obsoleted 1 changesets
+ $ hg --hidden fix -r 0
+ abort: fixing obsolete revision could cause divergence
+ [255]
+
+ $ hg --hidden fix -r 0 --config experimental.evolution.allowdivergence=true
+ $ hg cat -r tip foo.changed
+ FOO
+
+ $ cd ..
+
+Test all of the available substitution values for fixer commands.
+
+ $ hg init substitution
+ $ cd substitution
+
+ $ mkdir foo
+ $ printf "hello\ngoodbye\n" > foo/bar
+ $ hg add
+ adding foo/bar
+ $ hg --config "fix.fail:command=printf '%s\n' '{rootpath}' '{basename}'" \
+ > --config "fix.fail:linerange='{first}' '{last}'" \
+ > --config "fix.fail:fileset=foo/bar" \
+ > fix --working-dir
+ $ cat foo/bar
+ foo/bar
+ bar
+ 1
+ 2
+
+ $ cd ..
+
+The --base flag should allow picking the revisions to diff against for changed
+files and incremental line formatting.
+
+ $ hg init baseflag
+ $ cd baseflag
+
+ $ printf "one\ntwo\n" > foo.changed
+ $ printf "bar\n" > bar.changed
+ $ hg commit -Aqm "first"
+ $ printf "one\nTwo\n" > foo.changed
+ $ hg commit -m "second"
+ $ hg fix -w --base .
+ $ hg status
+ $ hg fix -w --base null
+ $ cat foo.changed
+ ONE
+ TWO
+ $ cat bar.changed
+ BAR
+
+ $ cd ..
+
+If the user asks to fix the parent of another commit, they are asking to create
+an orphan. We must respect experimental.evolution.allowunstable.
+
+ $ hg init allowunstable
+ $ cd allowunstable
+
+ $ printf "one\n" > foo.whole
+ $ hg commit -Aqm "first"
+ $ printf "two\n" > foo.whole
+ $ hg commit -m "second"
+ $ hg --config experimental.evolution.allowunstable=False fix -r '.^'
+ abort: can only fix a changeset together with all its descendants
+ [255]
+ $ hg fix -r '.^'
+ 1 new orphan changesets
+ $ hg cat -r 2 foo.whole
+ ONE
+
+ $ cd ..
+