Cut a release.

    @param project_name {str}
    @param version_files {list} List of paths to files holding the version
        info for this project.

        If none are given it attempts to guess the version file:
        package.json or VERSION.txt or VERSION or $project_name.py
        or lib/$project_name.py or $project_name.js or lib/$project_name.js.

        The version file can be in one of the following forms:

        - A .py file, in which case the file is expect to have a top-level
          global called "__version_info__" as follows. [1]

            __version_info__ = (0, 7, 6)

          Note that I typically follow that with the following to get a
          string version attribute on my modules:

            __version__ = '.'.join(map(str, __version_info__))

        - A .js file, in which case the file is expected to have a top-level
          global called "VERSION" as follows:

            ver VERSION = "1.2.3";

        - A "package.json" file, typical of a node.js npm-using project.
          The package.json file must have a "version" field.

        - TODO: A simple version file whose only content is a "1.2.3"-style version
          string.

    [1]: This is a convention I tend to follow in my projects.
        Granted it might not be your cup of tea. I should add support for
        just `__version__ = "1.2.3"`. I'm open to other suggestions too.

def cutarelease(project_name, version_files, dry_run=False):
    """Cut a release.

    @param project_name {str}
    @param version_files {list} List of paths to files holding the version
        info for this project.

        If none are given it attempts to guess the version file:
        package.json or VERSION.txt or VERSION or $project_name.py
        or lib/$project_name.py or $project_name.js or lib/$project_name.js.

        The version file can be in one of the following forms:

        - A .py file, in which case the file is expect to have a top-level
          global called "__version_info__" as follows. [1]

            __version_info__ = (0, 7, 6)

          Note that I typically follow that with the following to get a
          string version attribute on my modules:

            __version__ = '.'.join(map(str, __version_info__))

        - A .js file, in which case the file is expected to have a top-level
          global called "VERSION" as follows:

            ver VERSION = "1.2.3";

        - A "package.json" file, typical of a node.js npm-using project.
          The package.json file must have a "version" field.

        - TODO: A simple version file whose only content is a "1.2.3"-style version
          string.

    [1]: This is a convention I tend to follow in my projects.
        Granted it might not be your cup of tea. I should add support for
        just `__version__ = "1.2.3"`. I'm open to other suggestions too.
    """
    dry_run_str = dry_run and " (dry-run)" or ""

    if not version_files:
        log.info("guessing version file")
        candidates = [
            "package.json",
            "VERSION.txt",
            "VERSION",
            "%s.py" % project_name,
            "lib/%s.py" % project_name,
            "%s.js" % project_name,
            "lib/%s.js" % project_name,
        ]
        for candidate in candidates:
            if exists(candidate):
                version_files = [candidate]
                break
        else:
            raise Error("could not find a version file: specify its path or "
                "add one of the following to your project: '%s'"
                % "', '".join(candidates))
        log.info("using '%s' as version file", version_files[0])

    parsed_version_files = [_parse_version_file(f) for f in version_files]
    version_file_type, version_info = parsed_version_files[0]
    version = _version_from_version_info(version_info)

    # Confirm
    if not dry_run:
        answer = query_yes_no("* * *\n"
            "Are you sure you want cut a %s release?\n"
            "This will involved commits and a push." % version,
            default="no")
        print "* * *"
        if answer != "yes":
            log.info("user abort")
            return
    log.info("cutting a %s release%s", version, dry_run_str)

    # Checks: Ensure there is a section in changes for this version.



    changes_path = "CHANGES.md"
    changes_txt, changes, nyr = parse_changelog(changes_path)
    #pprint(changes)
    top_ver = changes[0]["version"]
    if top_ver != version:
        raise Error("changelog '%s' top section says "
            "version %r, expected version %r: aborting"
            % (changes_path, top_ver, version))
    top_verline = changes[0]["verline"]
    if not top_verline.endswith(nyr):
        answer = query_yes_no("\n* * *\n"
            "The changelog '%s' top section doesn't have the expected\n"
            "'%s' marker. Has this been released already?"
            % (changes_path, nyr), default="yes")
        print "* * *"
        if answer != "no":
            log.info("abort")
            return
    top_body = changes[0]["body"]
    if top_body.strip() == "(nothing yet)":
        raise Error("top section body is `(nothing yet)': it looks like "
            "nothing has been added to this release")

    # Commits to prepare release.
    changes_txt_before = changes_txt
    changes_txt = changes_txt.replace(" (not yet released)", "", 1)
    if not dry_run and changes_txt != changes_txt_before:
        log.info("prepare `%s' for release", changes_path)
        f = codecs.open(changes_path, 'w', 'utf-8')
        f.write(changes_txt)
        f.close()
        run('git commit %s -m "prepare for %s release"'
            % (changes_path, version))

    # Tag version and push.
    curr_tags = set(t for t in _capture_stdout(["git", "tag", "-l"]).split('\n') if t)
    if not dry_run and version not in curr_tags:
        log.info("tag the release")
        run('git tag -a "%s" -m "version %s"' % (version, version))
        run('git push --tags')

    # Optionally release.
    if exists("package.json"):
        answer = query_yes_no("\n* * *\nPublish to npm?", default="yes")
        print "* * *"
        if answer == "yes":
            if dry_run:
                log.info("skipping npm publish (dry-run)")
            else:
                run('npm publish')
    elif exists("setup.py"):
        answer = query_yes_no("\n* * *\nPublish to pypi?", default="yes")
        print "* * *"
        if answer == "yes":
            if dry_run:
                log.info("skipping pypi publish (dry-run)")
            else:
                run("%spython setup.py sdist --formats zip upload"
                    % _setup_command_prefix())

    # Commits to prepare for future dev and push.
    # - update changelog file
    next_version_info = _get_next_version_info(version_info)
    next_version = _version_from_version_info(next_version_info)
    log.info("prepare for future dev (version %s)", next_version)
    marker = "## " + changes[0]["verline"]
    if marker.endswith(nyr):
        marker = marker[0:-len(nyr)]
    if marker not in changes_txt:
        raise Error("couldn't find `%s' marker in `%s' "
            "content: can't prep for subsequent dev" % (marker, changes_path))
    next_verline = "%s %s%s" % (marker.rsplit(None, 1)[0], next_version, nyr)
    changes_txt = changes_txt.replace(marker + '\n',
        "%s\n\n(nothing yet)\n\n\n%s\n" % (next_verline, marker))
    if not dry_run:
        f = codecs.open(changes_path, 'w', 'utf-8')
        f.write(changes_txt)
        f.close()

    # - update version file
    next_version_tuple = _tuple_from_version(next_version)
    for i, ver_file in enumerate(version_files):
        ver_content = codecs.open(ver_file, 'r', 'utf-8').read()
        ver_file_type, ver_info = parsed_version_files[i]
        if ver_file_type == "json":
            marker = '"version": "%s"' % version
            if marker not in ver_content:
                raise Error("couldn't find `%s' version marker in `%s' "
                    "content: can't prep for subsequent dev" % (marker, ver_file))
            ver_content = ver_content.replace(marker,
                '"version": "%s"' % next_version)
        elif ver_file_type == "javascript":
            candidates = [
                ("single", "var VERSION = '%s';" % version),
                ("double", 'var VERSION = "%s";' % version),
            ]
            for quote_type, marker in candidates:
                if marker in ver_content:
                    break
            else:
                raise Error("couldn't find any candidate version marker in "
                    "`%s' content: can't prep for subsequent dev: %r"
                    % (ver_file, candidates))
            if quote_type == "single":
                ver_content = ver_content.replace(marker,
                    "var VERSION = '%s';" % next_version)
            else:
                ver_content = ver_content.replace(marker,
                    'var VERSION = "%s";' % next_version)
        elif ver_file_type == "python":
            marker = "__version_info__ = %r" % (version_info,)
            if marker not in ver_content:
                raise Error("couldn't find `%s' version marker in `%s' "
                    "content: can't prep for subsequent dev" % (marker, ver_file))
            ver_content = ver_content.replace(marker,
                "__version_info__ = %r" % (next_version_tuple,))
        elif ver_file_type == "version":
            ver_content = next_version
        else:
            raise Error("unknown ver_file_type: %r" % ver_file_type)
        if not dry_run:
            log.info("update version to '%s' in '%s'", next_version, ver_file)
            f = codecs.open(ver_file, 'w', 'utf-8')
            f.write(ver_content)
            f.close()

    if not dry_run:
        run('git commit %s %s -m "prep for future dev"' % (
            changes_path, ' '.join(version_files)))
        run('git push')