Commit d52d7781 authored by Adrian DC's avatar Adrian DC
Browse files

build(pre-commit): migrate to 'pre-commit-crocodile' 1.1.0

Signed-off-by: default avatarAdrian DC <radian.dc@gmail.com>
parent bbf12010
Loading
Loading
Loading
Loading
Loading
+5 −8
Changes for .gitlab-ci.yml: 5 added lines, 8 removed lines.
Original line number Diff line number Diff line
@@ -171,14 +171,13 @@ codestyle:
  image: registry.gitlab.com/radiandevcore/tools/gitlab-projects-settings/gitlab-projects-settings:codestyle
  needs: []
  script:
    - yapf -i ./.hooks/*.py ./src/*.py ./src/*/*.py ./setup.py
    - yapf -i ./src/*.py ./src/*/*.py ./setup.py
    - echo '' && git diff --name-status
    - git diff --quiet
  rules:
    - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
      changes:
        - 'CHANGELOG.md'
        - '.hooks/**/*'
        - 'requirements/**/*'
        - 'setup.py'
        - 'src/**/*'
@@ -246,13 +245,12 @@ lint:
  image: registry.gitlab.com/radiandevcore/tools/gitlab-projects-settings/gitlab-projects-settings:codestyle
  needs: []
  script:
    - pylint --errors-only ./.hooks/*.py ./src/ ./setup.py
    - pylint --disable=missing-docstring --load-plugins=pylint.extensions.no_self_use ./.hooks/*.py ./src/ ./setup.py
    - pylint --errors-only ./src/ ./setup.py
    - pylint --disable=missing-docstring --load-plugins=pylint.extensions.no_self_use ./src/ ./setup.py
  rules:
    - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
      changes:
        - 'CHANGELOG.md'
        - '.hooks/**/*'
        - 'requirements/**/*'
        - 'setup.py'
        - 'src/**/*'
@@ -271,15 +269,14 @@ typings:
    - set +x
    - |
      if [ $(git rev-list --count HEAD) -gt 1 ]; then
        mypy --follow-imports silent --pretty $(git diff --name-only $(git diff --exit-code >/dev/null && echo 'HEAD^' || echo 'HEAD') ./.hooks/*.py ./src/ ./setup.py) 2>/dev/null || true
        mypy --follow-imports silent --pretty $(git diff --name-only $(git diff --exit-code >/dev/null && echo 'HEAD^' || echo 'HEAD') ./src/ ./setup.py) 2>/dev/null || true
      fi
    - sleep 1
    - mypy --follow-imports silent --pretty ./.hooks/*.py ./src/ ./setup.py
    - mypy --follow-imports silent --pretty ./src/ ./setup.py
  rules:
    - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
      changes:
        - 'CHANGELOG.md'
        - '.hooks/**/*'
        - 'requirements/**/*'
        - 'setup.py'
        - 'src/**/*'

.hooks/configurations.py

deleted100755 → 0
+0 −248
Changes for .hooks/configurations.py: 0 added lines, 248 removed lines.
Original line number Diff line number Diff line
#!/usr/bin/env python3

# Standard libraries
from typing import List, NamedTuple

# Changes regex type
ChangesRegex = str

# Changes parsers class
class ChangesParsers(NamedTuple):
    match: str
    groups: List[int]
    commit_type: str

# Changes evaluator class
class ChangesEvaluator(NamedTuple):
    changes: List[ChangesRegex]
    parsers: List[ChangesParsers]

# Changes types class
class ChangesType(NamedTuple):
    commit_type: str
    changes: List[ChangesRegex]

# Changes matcher class
class ChangesMatcher(NamedTuple):
    commit_scope: str
    types: List[ChangesType]

# Commits changes constants
COMMITS_CHANGES_PATTERN: str = r'^#\s*(new file|modified|deleted|renamed|copied):\s*(.+)$'
COMMITS_CHANGES_SECTION: str = '# Changes to be committed:'

# Commits comments constants
COMMITS_COMMENTS_PREFIX: str = '#'

# Commits default constants
COMMITS_DEFAULT_BODY: str = 'Issue: #...'
COMMITS_DEFAULT_SCOPE: str = 'scope'
COMMITS_DEFAULT_SUBJECT: str = ''
COMMITS_DEFAULT_TYPE: str = 'type'

# Commits footers constants
COMMITS_FOOTER_SIGNOFF: str = 'Signed-off-by: '

# Regex constants
REGEX_EXT: str = r'[^/.]*'
REGEX_FOLDER: str = r'[^/]+'
REGEX_STEM: str = r'[^/]+'
REGEX_STEM_VISIBLE: str = r'[^/_\.]{1}[^/]+'

# Changes evaluators constants
CHANGES_EVALUATORS: List[ChangesEvaluator] = [
    # Sources
    ChangesEvaluator(
        changes=[
            r'^sources|^src',
        ],
        parsers=[
            ChangesParsers(
                match=
                fr'(({REGEX_FOLDER})/)?(({REGEX_FOLDER})/)?({REGEX_STEM_VISIBLE})\.{REGEX_EXT}$',
                groups=[5, 4, 2],
                commit_type='fix',
            ),
        ],
    ),
    # Yocto
    ChangesEvaluator(
        changes=[
            fr'conf/(distro|machine)|recipes-{REGEX_STEM}',
        ],
        parsers=[
            ChangesParsers(
                match=fr'conf/(distro|machine)/{REGEX_STEM}.conf$',
                groups=[1],
                commit_type='fix',
            ),
            ChangesParsers(
                match=fr'({REGEX_STEM})/{REGEX_STEM}\.(bb[^/.]*|inc)$',
                groups=[1],
                commit_type='fix',
            ),
        ],
    ),
]

# Changes matchers constant
CHANGES_MATCHERS: List[ChangesMatcher] = [
    ChangesMatcher(
        commit_scope='gitlab-ci',
        types=[
            ChangesType(
                commit_type='ci',
                changes=[
                    r'\.gitlab-ci\.yml$',
                    r'\.gitlab-ci\.d$',
                ],
            ),
        ],
    ),
    ChangesMatcher(
        commit_scope='hooks',
        types=[
            ChangesType(
                commit_type='chore',
                changes=[
                    r'^\.hooks',
                ],
            ),
        ],
    ),
    ChangesMatcher(
        commit_scope='gitignore',
        types=[
            ChangesType(
                commit_type='chore',
                changes=[
                    r'\.gitignore$',
                ],
            ),
        ],
    ),
    ChangesMatcher(
        commit_scope='makefile',
        types=[
            ChangesType(
                commit_type='build',
                changes=[
                    r'Makefile$',
                    r'\.make$',
                ],
            ),
        ],
    ),
    ChangesMatcher(
        commit_scope='commitizen',
        types=[
            ChangesType(
                commit_type='chore',
                changes=[
                    r'\.cz.*$',
                ],
            ),
        ],
    ),
    ChangesMatcher(
        commit_scope='coveragerc',
        types=[
            ChangesType(
                commit_type='chore',
                changes=[
                    r'.coveragerc$',
                ],
            ),
        ],
    ),
    ChangesMatcher(
        commit_scope='mypy',
        types=[
            ChangesType(
                commit_type='chore',
                changes=[
                    r'mypy\.ini$',
                ],
            ),
        ],
    ),
    ChangesMatcher(
        commit_scope='pre-commit',
        types=[
            ChangesType(
                commit_type='chore',
                changes=[
                    r'\.pre-commit.*$',
                ],
            ),
        ],
    ),
    ChangesMatcher(
        commit_scope='setup',
        types=[
            ChangesType(
                commit_type='chore',
                changes=[
                    r'setup\.py$',
                ],
            ),
        ],
    ),
    ChangesMatcher(
        commit_scope='vscode',
        types=[
            ChangesType(
                commit_type='chore',
                changes=[
                    r'\.vscode/',
                ],
            ),
        ],
    ),
    ChangesMatcher(
        commit_scope='license',
        types=[
            ChangesType(
                commit_type='docs',
                changes=[
                    r'LICENSE.*$',
                ],
            ),
        ],
    ),
    ChangesMatcher(
        commit_scope='readme',
        types=[
            ChangesType(
                commit_type='docs',
                changes=[
                    r'docs/',
                    r'README.*$',
                ],
            ),
        ],
    ),
    ChangesMatcher(
        commit_scope='changelog',
        types=[
            ChangesType(
                commit_type='docs',
                changes=[
                    r'CHANGELOG.*$',
                ],
            ),
        ],
    ),
    ChangesMatcher(
        commit_scope='tests',
        types=[
            ChangesType(
                commit_type='test',
                changes=[
                    r'^test/',
                    r'^tests/',
                ],
            ),
        ],
    ),
]
+0 −253
Changes for .hooks/prepare_commit_msg_template.py: 0 added lines, 253 removed lines.
Original line number Diff line number Diff line
#!/usr/bin/env python3

# Standard libraries
from os import environ, linesep
from pathlib import Path
from re import match, search
import sys
from sys import argv, exit as sys_exit, path
from typing import List, NamedTuple, Optional, Tuple

# Bind sources
sys.dont_write_bytecode = True
path.append(str(Path(__file__).resolve().parent))

# Components, pylint: disable=import-error,wrong-import-position
from configurations import (
    COMMITS_CHANGES_PATTERN,
    COMMITS_CHANGES_SECTION,
    COMMITS_COMMENTS_PREFIX,
    COMMITS_DEFAULT_BODY,
    COMMITS_DEFAULT_SCOPE,
    COMMITS_DEFAULT_SUBJECT,
    COMMITS_DEFAULT_TYPE,
    CHANGES_EVALUATORS,
    COMMITS_FOOTER_SIGNOFF,
    CHANGES_MATCHERS,
)

# Parse commit input
def parse_commit_input(filepath: str) -> Tuple[bool, bool, str, List[str]]:

    # Variables
    capture: bool = False
    changes: List[str] = []
    context: List[str] = []
    empty: bool = True
    signoff: bool = False

    # Read commit template
    with open(filepath, encoding='utf8', mode='r') as file:
        for line in file:

            # Get line content
            content = line.rstrip()

            # Append context
            context += [content]

            # Detect signoff
            if content and content.startswith(COMMITS_FOOTER_SIGNOFF):
                signoff = True

            # Detect contents
            if content and not any(
                    content.startswith(prefix) for prefix in [
                        COMMITS_COMMENTS_PREFIX,
                        COMMITS_FOOTER_SIGNOFF,
                    ]):
                empty = False

            # Detect section
            if content.startswith(COMMITS_CHANGES_SECTION):
                capture = True
                continue

            # Parse section
            if capture:
                matches = match(COMMITS_CHANGES_PATTERN, content)
                if matches:
                    changes.append(matches.group(2).strip())
                elif not content.strip() or content.startswith('#'):
                    capture = False

    # Result
    return empty, signoff, linesep.join(context), changes

# Prepare commit title, pylint: disable=too-many-branches,too-many-locals,too-many-nested-blocks
def prepare_commit_title(changes: List[str]) -> Tuple[str, str]:

    # Types
    Result = NamedTuple('Result', [
        ('priority', int),
        ('level', int),
        ('commit_type', str),
        ('commit_scope', str),
    ])

    # Variables
    commit_scope: str = COMMITS_DEFAULT_SCOPE
    commit_type: str = COMMITS_DEFAULT_TYPE
    level: int
    levels: List[int]
    priority: int = 0
    results: List[Result] = []

    # Evaluate common changes
    for changes_evaluator in CHANGES_EVALUATORS:
        priority += 1
        for type_change in changes_evaluator.changes:
            for change in changes:
                if not search(type_change, change):
                    continue
                for parser in changes_evaluator.parsers:
                    matches = search(parser.match, change)
                    if not matches or not isinstance(matches.lastindex, int):
                        continue
                    level = 0
                    for group in parser.groups:
                        level += 1
                        if matches.lastindex >= group:
                            results += [
                                Result(
                                    priority=priority,
                                    level=level,
                                    commit_type=parser.commit_type,
                                    commit_scope=matches.group(group),
                                )
                            ]

    # Match common changes
    for changes_matcher in CHANGES_MATCHERS:
        priority += 1
        for matcher_type in changes_matcher.types:
            for type_change in matcher_type.changes:
                for change in changes:
                    if search(type_change, change):
                        results += [
                            Result(
                                priority=priority,
                                level=0,
                                commit_type=matcher_type.commit_type,
                                commit_scope=changes_matcher.commit_scope,
                            )
                        ]

    # Parse results
    if results:
        priority = min((result.priority for result in results))
        results = [result for result in set(results) if result.priority == priority]
        levels = sorted([result.level for result in results])
        level = min((level for level in set(levels) if levels.count(level) == 1),
                    default=levels[-1])
        for result in [result for result in set(results) if result.level == level]:
            commit_type = result.commit_type
            commit_scope = result.commit_scope
            break

    # Result
    return commit_type, commit_scope

# Prepare commit message
def prepare_commit_template(
    commit_type: str,
    commit_scope: str,
    commit_subject: str,
    commit_body: str,
    signoff: bool,
) -> str:

    # Variables
    commit_lines: List[str] = []

    # Prepare commit title
    commit_lines += [
        f'{commit_type}({commit_scope}): {commit_subject}',
    ]

    # Prepare commit body
    if commit_body:
        commit_lines += [
            '',
            commit_body,
        ]

    # Append commit separator
    if commit_body and signoff:
        commit_lines += [
            '---',
        ]

    # Append signoff separator
    if not signoff:
        commit_lines += [
            '',
        ]

    # Result
    return linesep.join(commit_lines)

# Main, pylint: disable=too-many-branches,too-many-statements
def main() -> None:

    # Variables
    changes: List[str]
    commit_scope: str
    commit_type: str
    context: str
    empty: bool
    filepath: str
    signoff: bool
    source: Optional[str]

    # Validate arguments
    if len(argv) <= 1:
        sys_exit(1)

    # Parse arguments
    filepath = argv[1]
    source = argv[2] if len(argv) > 2 else None

    # Ignore commit source, documentation:
    # - message (if a -m or -F option was given)
    # - template (if a -t option was given or the configuration option commit.template is set)
    # - merge (if the commit is a merge or a .git/MERGE_MSG file exists)
    # - squash (if a .git/SQUASH_MSG file exists)
    # - commit, followed by a commit object name (if a -c, -C or --amend option was given)
    if source and source in ['template', 'squash', 'commit']:
        sys_exit(0)

    # Parse commit input
    empty, signoff, context, changes = parse_commit_input(filepath)

    # Existing commit input
    if not empty:
        sys_exit(0)

    # Prepare commit title
    commit_type, commit_scope = prepare_commit_title(changes)

    # Prepare commit template
    commit_template = prepare_commit_template(
        commit_type=commit_type,
        commit_scope=commit_scope,
        commit_subject=COMMITS_DEFAULT_SUBJECT,
        commit_body=COMMITS_DEFAULT_BODY,
        signoff=signoff,
    )

    # Write commit template
    if environ.get('GIT_EXEC_PATH', ''):
        with open(filepath, encoding='utf8', mode='w') as file:
            file.write(commit_template + context)

    # Dump commit template
    else:
        print(commit_template + context)

    # Result
    sys_exit(0)

# Entrypoint
if __name__ == '__main__': # pragma: no cover
    main()
+3 −9
Changes for .pre-commit-config.yaml: 3 added lines, 9 removed lines.
Original line number Diff line number Diff line
@@ -61,14 +61,8 @@ repos:
        stages:
          - push

  # Repository: local
  - repo: local
  # Repository: pre-commit-crocodile
  - repo: https://gitlab.com/RadianDevCore/tools/pre-commit-crocodile.git
    rev: 1.1.0
    hooks:
      - id: prepare-commit-message
        name: Prepare commit message automatically
        description: Automatically prepare the commit message for manual edition
        stages:
          - prepare-commit-msg
        entry: ./.hooks/prepare_commit_msg_template.py
        args: []
        language: python