topic2k/pygcgen

View on GitHub
pygcgen/options_parser.py

Summary

Maintainability
C
1 day
Test Coverage
# -*- coding: utf-8 -*-

from __future__ import print_function

import argparse
import os
import re
import subprocess
import sys
if sys.version_info.major == 3:
    from builtins import object
from collections import OrderedDict

from .optionsfile_parser import OptionsFileParser
from .version import __version__


DEFAULT_OPTIONS = {
    "date_format": "%Y-%m-%d",
    "exclude_labels": [],
    "git_remote": "origin",
    "github_api": "api.github.com",
    "github_site": "github.com",
    "header": "# Change Log",
    "issue_prefix": "**Closed issues:**",
    "max_issues": sys.maxsize,
    "max_simultaneous_requests": 10,
    "merge_prefix": "**Merged pull requests:**",
    "options_file": ".pygcgen",
    "output": "CHANGELOG.md",
    "unreleased_label": "Unreleased",
}


class DebugHelp(argparse.Action):
    # def __init__(self, option_strings, dest, nargs=None, **kwargs):
    #     super().__init__(option_strings, dest, **kwargs)

    def __call__(self, parser, namespace, values, option_string=None):
        import platform
        print(self.help)
        print("\n{} v{}".format(parser.prog, __version__))
        print("Python", sys.version)
        print(platform.platform())
        exit()


class OptionsParser(object):
    def __init__(self, options=None):
        self.options = self.parse_options(options)

    def parse_options(self, options):
        parser = argparse.ArgumentParser(
            description='Fully automate changelog generation.',
        )

        parser.add_argument(
            "-u", "--user",
            help="Username of the owner of target GitHub repo"
        )
        parser.add_argument(
            "-p", "--project",
            help="Name of project on GitHub"
        )
        parser.add_argument(
            "-t", "--token",
            help="To make more than 50 requests per hour your GitHub token "
                 "is required. You can generate it at: "
                 "https://github.com/settings/tokens/new"
        )
        parser.add_argument(
            "--options-file", metavar="FILE",
            default=DEFAULT_OPTIONS["options_file"],
            help="Read options from file. Those will overwrite the ones from "
                 "the command line."
        )
        parser.add_argument(
            "-f", "--date-format",
            default=DEFAULT_OPTIONS["date_format"],
            help="The date format to use in changelog. Default is: %%Y-%%m-%%d"
        )
        parser.add_argument(
            "-o", "--output", metavar="FILE",
            default=DEFAULT_OPTIONS["output"],
            help="Output file. Default is CHANGELOG.md"
        )
        parser.add_argument(
            "--no-overwrite", action='store_true',
            help="Don't overwrite the output file if it exists "
                 "(add a number instead)."
        )
        parser.add_argument(
            "-b", "--base", metavar="FILE",
            help="Optional base file to append to generated changelog."
        )
        parser.add_argument(
            "-s", "--section", action="append", nargs="*",
            metavar=('PREFIX', 'LABEL'),
            help="Add a new section to the changelog with the prefix "
                 "'PREFIX'. All issues that match one of the LABEL's "
                 "will be listed in this section."
        )
        lst = [
            ["--header-label", "header", "Setup custom header label."],
            ["--issues-label", "issue_prefix",
             "Setup custom label for closed-issues section."],
            ["--pr-label", "merge_prefix",
             "Setup custom label for pull requests section."]
        ]
        for opt, dest, hlp in lst:
            parser.add_argument(
                opt, dest=dest,
                default=DEFAULT_OPTIONS[dest],
                help="{} Default is: {}".format(hlp, DEFAULT_OPTIONS[dest])
            )
        parser.add_argument(
            "--front-matter", metavar="JSON", dest="frontmatter",
            help="Add YAML front matter. Formatted as JSON because it's "
                 "easier to add on the command line."
        )
        parser.add_argument(
            "--no-issues", action="store_false", dest='issues',
            help="Don't include closed issues in changelog."
        )
        parser.add_argument(
            "--no-issues-wo-labels",
            action="store_true", dest="add_issues_wo_labels",
            help="Don't include closed issues without labels in changelog."
        )
        parser.add_argument(
            "--no-pr-wo-labels",
            action="store_false", dest='add_pr_wo_labels',
            help="Don't include pull requests without labels in changelog."
        )
        parser.add_argument(
            "--no-pull-requests",
            action="store_false", dest='include_pull_request',
            help="Don't include pull-requests in changelog."
        )
        parser.add_argument(
            "--no-filter-by-milestone",
            action="store_false", dest="filter_issues_by_milestone",
            help="Don't use milestone to detect when issue was resolved."
        )
        parser.add_argument(
            "--no-author",
            action="store_false", dest="author",
            help="Don't add author of pull-request in the end."
        )
        parser.add_argument(
            "--author-link-as-tag",
            action='store_true', dest="username_as_tag",
            help="Use GitHub tags instead of Markdown links for the "
                 "author of an issue or pull-request."
        )
        parser.add_argument(
            "--with-unreleased",
            action='store_true', dest="with_unreleased",
            help="Include unreleased closed issues in log."
        )
        parser.add_argument(
            "--unreleased-only",
            action='store_true', dest="unreleased_only",
            help="Generate log from unreleased closed issues only."
        )
        parser.add_argument(
            "--unreleased-label",
            default=DEFAULT_OPTIONS["unreleased_label"],
            help="Label for unreleased closed issues. "
                 "Default is: {0}".format(DEFAULT_OPTIONS["unreleased_label"])
        )
        parser.add_argument(
            "--unreleased-with-date", action='store_true',
            help="Add actual date to unreleased label."
        )
        parser.add_argument(
            "--no-compare-link", action='store_false', dest="compare_link",
            help="Don't include compare link (Full Changelog) between older "
                 "version and newer version."
        )
        parser.add_argument(
            "--include-labels", metavar="LABEL", nargs='*',
            help="Only issues with the specified labels will be "
                 "included in the changelog."
        )
        parser.add_argument(
            "--exclude-labels", metavar="LABEL",
            nargs='*', default=DEFAULT_OPTIONS["exclude_labels"],
            help="Issues with the specified labels will always be "
                 "excluded from changelog. "
                 "Default labels: {0}".format(
                DEFAULT_OPTIONS["exclude_labels"]
            )
        )
        parser.add_argument(
            "--tag-separator",  metavar="SEPARATOR",
            help="The SEPARATOR will be inserted in the log between tags."
        )
        parser.add_argument(
            "--between-tags",  metavar="TAG", nargs='*',
            help="Changelog will be filled only between specified tags."
        )
        parser.add_argument(
            "--exclude-tags", metavar="TAG", nargs='*',
            help="Change log will exclude specified tags."
        )
        parser.add_argument(
            "--exclude-tags-regex",
            help='Apply a regular expression on tag names so that they can be '
            'excluded, for example: --exclude-tags-regex ".*\+\d{1,}"'
        )
        parser.add_argument(
            "--since-tag", metavar="TAG",
            help="Change log will start after specified tag."
        )
        parser.add_argument(
            "--due-tag", metavar="TAG",
            help="Change log will end before specified tag."
        )
        parser.add_argument(
            "--max-issues", metavar="NUMBER",
            type=int, default=DEFAULT_OPTIONS["max_issues"],
            help="Max number of issues to fetch from GitHub. "
                 "Default is unlimited."
        )
        parser.add_argument(
            "--release-url", metavar="URL",
            help="The URL to point to for release links, in printf format "
                 "(with the tag as variable)."
        )
        parser.add_argument(
            "--github-api", metavar="URL",
            dest="github_endpoint", default=DEFAULT_OPTIONS["github_api"],
            help="The enterprise endpoint to use for your Github API."
        )
        parser.add_argument(
            "--github-site", metavar="URL",
            dest="github_site", default=DEFAULT_OPTIONS["github_site"],
            help="The Enterprise Github site on which your project is hosted."
        )
        parser.add_argument(
            "--simple-list", action='store_true',
            help="Create simple list from issues and pull requests. "
        )
        parser.add_argument(
            "--future-release", metavar="RELEASE_VERSION",
            help="Put the unreleased changes in the specified release number."
        )
        parser.add_argument(
            "--release-branch",
            help="Limit pull requests to the release branch, "
                 "such as master or release."
        )
        parser.add_argument(
            "--origin", dest="git_remote",
            default=DEFAULT_OPTIONS["git_remote"],
            help="If you named the origin of your repo other than origin."
        )
        parser.add_argument(
            "-v", "--verbose", action='count', default=False,
            help="Run verbosely."
        )
        parser.add_argument(
            "-q", "--quiet", action='store_true',
            help="Don't output progress information."
        )
        parser.add_argument(
            "--version",
            action='version',
            version="%(prog)s v{0}".format(__version__),
            help="Print version number"
        )
        parser.add_argument(
            "--support", action=DebugHelp, nargs="?",
            help="If you have an issue with pygcgen, got to "
                 "https://github.com/topic2k/pygcgen/issues and add a "
                 "new issue. \nAdd this information to your issue discription."
        )
        parser.add_argument(
            "--max-simultaneous-requests", metavar="NUMBER",
            type=int, default=DEFAULT_OPTIONS["max_simultaneous_requests"],
            help=
            "Max number of events to fetch simultaneous from GitHub. "
            "Default is %d." % DEFAULT_OPTIONS["max_simultaneous_requests"]
        )

        opts = parser.parse_args(options)

        if os.path.exists(opts.options_file):
            OptionsFileParser(options=opts).parse()
        if not opts.user or not opts.project:
            self.fetch_user_and_project(opts)

        sections = OrderedDict()
        if opts.section:
            for s in opts.section:
                labels = []
                for l in s[1:]:
                    # this is to remove empty label strings (could happen if
                    # in the config file a section line ends with a comma.)
                    if l:
                        labels.append(l)
                sections.update({s[0]: labels})

        opts.sections = sections
        del opts.section

        return opts

    def fetch_user_and_project(self, options):
        user, project = self.user_and_project_from_git(options)
        if not options.user:
            options.user = user
        if not options.project:
            options.project = project

    def user_and_project_from_git(self, options, arg0=None, arg1=None):
        """ Detects user and project from git. """
        user, project = self.user_project_from_option(options, arg0, arg1)
        if user and project:
            return user, project

        try:
            remote = subprocess.check_output(
                [
                    'git', 'config', '--get',
                    'remote.{0}.url'.format(options.git_remote)
                ]
            )
        except subprocess.CalledProcessError:
            return None, None
        except WindowsError:
            print("git binary not found.")
            exit(1)
        else:
            return self.user_project_from_remote(remote)

    @staticmethod
    def user_project_from_option(options, arg0, arg1):
        """
        Try to find user and project name from git remote output

        @param [String] output of git remote command
        @return [Array] user and project
        """

        site = options.github_site
        if arg0 and not arg1:
            # this match should parse strings such as
            #   "https://github.com/skywinder/Github-Changelog-Generator"
            # or
            #   "skywinder/Github-Changelog-Generator"
            #  to user and project
            match = re.match(
                "(?:.+{site}/)?(.+)/(.+)".format(site=site),
                arg0
            )
            if not match:
                print("Can't detect user and name from first "
                      "parameter: '{arg0}' -> exit'".format(arg0=arg0))
                exit(1)
            return match.groups()
        return None, None

    @staticmethod
    def user_project_from_remote(remote):
        """
        Try to find user and project name from git remote output

        @param [String] output of git remote command
        @return [Array] user and project
        """

        # try to find repo in format:
        # origin    git@github.com:skywinder/Github-Changelog-Generator.git (fetch)
        # git@github.com:skywinder/Github-Changelog-Generator.git
        regex1 = br".*(?:[:/])(?P<user>(-|\w|\.)*)/" \
                 br"(?P<project>(-|\w|\.)*)(\.git).*"
        match = re.match(regex1, remote)
        if match:
            return match.group("user"), match.group("project")

        # try to find repo in format:
        # origin    https://github.com/skywinder/ChangelogMerger (fetch)
        # https://github.com/skywinder/ChangelogMerger
        regex2 = r".*/((?:-|\w|\.)*)/((?:-|\w|\.)*).*"
        match = re.match(regex2, remote)
        if match:
            return match.group("user"), match.group("project")

        return None, None