Conventional Commits

Keywords: Conventional, Commit, Commits

Conventional Commits 是一种 Git Commit Message 的一种约定.

  • fix: 类型 为 fix 的提交表示在代码库中修复了一个 bug (这和语义化版本中的 PATCH 相对应).

  • feat: 类型 为 feat 的提交表示在代码库中新增了一个功能 (这和语义化版本中的 MINOR 相对应).

  • BREAKING CHANGE: 在脚注中包含 BREAKING CHANGE: 或 <类型>(范围) 后面有一个 ! 的提交, 表示引入了破坏性 API 变更 (这和语义化版本中的 MAJOR 相对应) . 破坏性变更可以是任意 类型 提交的一部分.

  • fix:feat: 之外, 也可以使用其它提交 类型 , 例如 @commitlint/config-conventional (基于 Angular 约定) 中推荐的 build:, chore:, ci:, docs:, style:, refactor:, perf:, test: 等等.

  • 脚注中除了 BREAKING CHANGE: <description> , 其它条目应该采用类似 git trailer format 这样的惯例.

Parse Conventional Commits

这里我写了一个基于 Python 3.7+ 的脚本, 可以用于解析 conventional commits.

# -*- coding: utf-8 -*-

"""
A simple regex parser to parse conventional commit message.
"""

import dataclasses
import re
import string
from typing import Optional, List, Pattern

DELIMITERS = "!@#$%^&*()_+-=~`[{]}\\|;:'\",<.>/? \t\n"
CHARSET = string.ascii_letters


def tokenize(text: str) -> List[str]:
    cleaner_text = text
    for delimiter in DELIMITERS:
        cleaner_text = cleaner_text.replace(delimiter, " ")
    words = [word.strip() for word in cleaner_text.split(" ") if word.strip()]
    return words


def _get_subject_regex(_types: List[str]) -> Pattern:
    return re.compile(
        fr"^(?P<types>[\w ,]+)(?:\((?P<scope>[\w-]+)\))?(?P<breaking>!)?:[ \t]?(?P<description>.+)$"
    )


@dataclasses.dataclass
class Commit:
    """
    Data container class for conventional commits message.
    """
    types: List[str]
    description: str = None
    scope: Optional[str] = None
    breaking: Optional[str] = None


class ConventionalCommitParser:
    def __init__(self, types: List[str]):
        self.types = types
        self.subject_regex = _get_subject_regex(types)

    def extract_subject(self, msg: str) -> str:
        return msg.split("\n")[0].strip()

    def extract_commit(self, subject: str) -> Commit:
        match = self.subject_regex.match(subject)
        types = [
            word.strip()
            for word in match["types"].split(",")
            if word.strip() in self.types
        ]

        # Debug only
        # print(match)
        # print([match["types"],])
        # print([match["description"], ])
        # print([match["scope"], ])
        # print([match["breaking"], ])

        return Commit(
            types=types,
            description=match["description"],
            scope=match["scope"],
            breaking=match["breaking"],
        )


parser = ConventionalCommitParser(
    types=[
        "chore",
        "feat",
        "test",
        "utest",
        "itest",
        "build",
        "pub",
        "fix",
        "rls",
        "doc",
        "style",
        "lint",
        "ci",
        "noci",
    ]
)


def parse_commit(msg: str) -> Commit:
    subject = parser.extract_subject(msg)
    return parser.extract_commit(subject)

这是测试用例:

# -*- coding: utf-8 -*-

"""
A simple regex parser to parse conventional commit message.
"""

import dataclasses
import re
import string
from typing import Optional, List, Pattern

DELIMITERS = "!@#$%^&*()_+-=~`[{]}\\|;:'\",<.>/? \t\n"
CHARSET = string.ascii_letters


def tokenize(text: str) -> List[str]:
    cleaner_text = text
    for delimiter in DELIMITERS:
        cleaner_text = cleaner_text.replace(delimiter, " ")
    words = [word.strip() for word in cleaner_text.split(" ") if word.strip()]
    return words


def _get_subject_regex(_types: List[str]) -> Pattern:
    return re.compile(
        fr"^(?P<types>[\w ,]+)(?:\((?P<scope>[\w-]+)\))?(?P<breaking>!)?:[ \t]?(?P<description>.+)$"
    )


@dataclasses.dataclass
class Commit:
    """
    Data container class for conventional commits message.
    """
    types: List[str]
    description: str = None
    scope: Optional[str] = None
    breaking: Optional[str] = None


class ConventionalCommitParser:
    def __init__(self, types: List[str]):
        self.types = types
        self.subject_regex = _get_subject_regex(types)

    def extract_subject(self, msg: str) -> str:
        return msg.split("\n")[0].strip()

    def extract_commit(self, subject: str) -> Commit:
        match = self.subject_regex.match(subject)
        types = [
            word.strip()
            for word in match["types"].split(",")
            if word.strip() in self.types
        ]

        # Debug only
        # print(match)
        # print([match["types"],])
        # print([match["description"], ])
        # print([match["scope"], ])
        # print([match["breaking"], ])

        return Commit(
            types=types,
            description=match["description"],
            scope=match["scope"],
            breaking=match["breaking"],
        )


parser = ConventionalCommitParser(
    types=[
        "chore",
        "feat",
        "test",
        "utest",
        "itest",
        "build",
        "pub",
        "fix",
        "rls",
        "doc",
        "style",
        "lint",
        "ci",
        "noci",
    ]
)


def parse_commit(msg: str) -> Commit:
    subject = parser.extract_subject(msg)
    return parser.extract_commit(subject)

社区还有其他的包实现了更强的功能:

Reference