Converts Mercurial commits into a changelog in RST format.

def build_changelog(docs_path, package_name="yacms"):
    """
    Converts Mercurial commits into a changelog in RST format.
    """

    project_path = os.path.join(docs_path, "..")
    version_file = os.path.join(package_name, "__init__.py")
    version_var = "__version__"
    changelog_filename = "CHANGELOG"
    changelog_file = os.path.join(project_path, changelog_filename)
    versions = OrderedDict()
    repo = None
    ignore = ("AUTHORS", "formatting", "typo", "pep8", "pep 8",
              "whitespace", "README", "trans", "print debug",
              "debugging", "tabs", "style", "sites", "ignore",
              "tweak", "cleanup", "minor", "for changeset",
              ".com``", "oops", "syntax")
    hotfixes = {
        "40cbc47b8d8a": "1.0.9",
        "a25749986abc": "1.0.10",
    }

    # Load the repo.
    try:
        from mercurial import ui, hg, error
        from mercurial.commands import tag
    except ImportError:
        pass
    else:
        try:
            ui = ui.ui()
            repo = hg.repository(ui, project_path)
        except error.RepoError:
            return
    if repo is None:
        return

    # Go through each changeset and assign it to the versions dict.
    changesets = [repo.changectx(changeset) for changeset in repo.changelog]
    for cs in sorted(changesets, reverse=True, key=_changeset_date):
        # Check if the file with the version number is in this changeset
        # and if it is, pull it out and assign it as a variable.
        files = cs.files()
        new_version = False
        # Commit message cleanup hacks.
        description = cs.description().decode("utf-8")
        description = description.rstrip(".").replace("\n", ". ")
        while "  " in description:
            description = description.replace("  ", " ")
        description = description.replace(". . ", ". ").replace("...", ",")
        while ".." in description:
            description = description.replace("..", ".")
        description = description.replace(":.", ":").replace("n'. t", "n't")
        words = description.split()
        # Format var names in commit.
        for i, word in enumerate(words):
            if (set("._") & set(word[:-1]) and set(letters) & set(word) and
                    "`" not in word and not word[0].isdigit()):
                last = ""
                if word[-1] in ",.":
                    last, word = word[-1], word[:-1]
                words[i] = "``%s``%s" % (word, last)
        description = " ".join(words)
        if version_file in files:
            for line in cs[version_file].data().split("\n"):
                if line.startswith(version_var):
                    exec(line)
                    if locals()[version_var] == "0.1.0":
                        locals()[version_var] = "1.0.0"
                        break
                    versions[locals()[version_var]] = {
                        "changes": [],
                        "date": _changeset_date(cs).strftime("%b %d, %Y")
                    }
                    new_version = len(files) == 1

        # Tag new versions.
        hotfix = hotfixes.get(cs.hex()[:12])
        if hotfix or new_version:
            if hotfix:
                version_tag = hotfix
            else:
                try:
                    version_tag = locals()[version_var]
                except KeyError:
                    version_tag = None
            if version_tag and version_tag not in cs.tags():
                try:
                    tag(ui, repo, version_tag, rev=cs.hex())
                    print("Tagging version %s" % version_tag)
                except:
                    pass

        # Ignore changesets that are merges, bumped the version, closed
        # a branch, regenerated the changelog itself, contain an ignore
        # word, or contain too few words to be meaningful.
        merge = len(cs.parents()) > 1
        branch_closed = len(files) == 0
        changelog_update = changelog_filename in files
        ignored = [w for w in ignore if w.lower() in description.lower()]
        too_few_words = len(description.split()) <= 3
        if (merge or new_version or branch_closed or changelog_update or
                ignored or too_few_words):
            continue
        # Ensure we have a current version and if so, add this changeset's
        # description to it.
        version = None
        try:
            version = locals()[version_var]
        except KeyError:
            if not hotfix:
                continue
        user = cs.user().decode("utf-8").split("<")[0].strip()
        entry = "%s - %s" % (description, user)
        if hotfix or entry not in versions[version]["changes"]:
            if hotfix:
                versions[hotfix] = {
                    "changes": [entry],
                    "date": _changeset_date(cs).strftime("%b %d, %Y"),
                }
            else:
                versions[version]["changes"].insert(0, entry)

    # Write out the changelog.
    with open(changelog_file, "w") as f:
        for version, version_info in versions.items():
            header = "Version %s (%s)" % (version, version_info["date"])
            f.write("%s\n" % header)
            f.write("%s\n" % ("-" * len(header)))
            f.write("\n")
            if version_info["changes"]:
                for change in version_info["changes"]:
                    f.write("  * %s\n" % change)
            else:
                f.write("  * No changes listed.\n")
            f.write("\n")