metacmd 0.0.1

Abstracting commands over multiple environments
Documentation
import re
import subprocess
import sys


def eprint(*args, **kwargs):
    print(*args, file=sys.stderr, **kwargs)


def error(msg):
    eprint("error:", msg)
    sys.exit(1)


def run(*cmd, capture_output=True):
    output = subprocess.run(cmd, capture_output=capture_output)

    if output.returncode != 0:
        if capture_output:
            eprint(output.stderr.decode("utf-8").strip())
        sys.exit(output.returncode)

    if capture_output:
        return output.stdout.decode("utf-8").strip(), output.stderr.decode("utf-8").strip()


TYPES = {
    "feat": [],
    "fix": [],
    "ci": ["scripts"],
    "docs": [],
}
IGNORED_TYPES = ["release"]
def parse_commit_msg(msg):
    regex = (
        "^(?P<type>[a-z]+)(\((?P<scope>[^)\n]+)\))?(?P<is_breaking>!)?: "
        "(?P<group>[^\s]+) (?P<desc>[^\n]+)"
        "(\n\n(?P<body>[\s\S]+))?$"
    )
    match = re.match(regex, msg)
    if match is None:
        error(f'Failed parsing: "{msg}"')

    type = match.group("type")

    if type in IGNORED_TYPES:
        return None

    scope = match.group("scope")
    is_breaking = match.group("is_breaking") is not None
    group = match.group("group").lower()
    desc = match.group("desc")
    body = match.group("body")
    breaking_desc = None

    if body is not None:
        body_regex = (
            "^((?P<body>[\s\S]+?)\n\n)?"
            "BREAKING[ -]CHANGE: (?P<breaking_desc>[\s\S]+)$"
        )
        match = re.match(body_regex, body)
        if match is not None:
            body = match.group("body")
            breaking_desc = match.group("breaking_desc")

        whitespace_regex = " *\n *"
        if body is not None:
            body = re.sub(whitespace_regex, " ", body)
        if breaking_desc is not None:
            breaking_desc = re.sub(whitespace_regex, " ", breaking_desc)

    if type not in TYPES:
        error(f"Unsupported type: {type}")

    if scope is not None and scope not in TYPES[type]:
        error(f"Unsupported scope: {scope}")

    if group in ["add", "support"]:
        group = "Added"
    elif group in ["remove", "delete"]:
        group = "Removed"
    elif group == "fix" or type == "fix":
        if group != "fix":
            desc = f"Fixed {desc}"
        group = "Fixed"
    else:
        desc = f"{group} {desc}"
        group = "Changed"

    obj = {
        "group": group,
        "description": "%c%s%s" % (desc[0].upper(), desc[1:], "." if desc[-1] != "." else ""),
        "is_breaking_change": is_breaking or breaking_desc is not None,
    }

    if scope is not None:
        obj["scope"] = scope

    if body is not None:
        obj["long_description"] = "%c%s%s" % (
            body[0].upper(),
            body[1:],
            "." if body[-1] != "." else ""
        )

    if breaking_desc is not None:
        obj["breaking_change_description"] = "%c%s" % (
            breaking_desc[0].upper(),
            breaking_desc[1:],
            "." if breaking_desc[-1] != "." else ""
        )

    return obj