Script para validar commits

O Conventional Commits é uma forma de padronização de commits dentro de um projeto de desenvolvimento de software, utilizando regras simples e claras com o objetivo de tornar os commits mais descritivos e padronizados, contribuindo para reduzir o tempo gasto em compreender como e por que algo foi feito em uma alteração ou correção posterior.

Neste post será apresentado de forma simples e objetiva um script em bash para validar a estrutura do commit seguindo o padrão do Conventional Commits.

O que é o Conventional Commits?

Para saber mais acesse a publicação detalhada sobre o Conventional Commits

Validações

As validações realizadas pelo script antes de realizar o commit são as seguintes:

Este script deve estar em um repositório git!

Validação se é um repositório GIT

O commit deve possuir opção (-am ou -m) e mensagem!

Validação se o commit possui opção (-am ou -m) e mensagem

O commit deve possuir uma das seguintes opções: -am, -m

Validação se o commit possui a opção -am ou -m

O commit deve estar no padrão: <tipo>[escopo opcional]: <descrição>

Validação se o commit está no padrão do Conventional Commits (Tipo, escopo, descrição, corpo e rodapé)

O tipo do commit deve ser: feat, fix, chore, refactor, perf, style, test, docs, build, ci, revert

Validação se o tipo do commit é um dos permitidos segundo o Conventional Commits

O escopo do commit foi aberto de forma errada!

Validação se o escopo do commit foi aberto corretamente

O escopo do commit foi fechado de forma errada!

Validação se o escopo do commit foi fechado corretamente

O escopo do commit não deve ser vazio!

Validação se o escopo do commit foi preenchido

A descrição do commit não deve ser menor que 10 caracteres! Tamanho atual: X caracteres.

Validação do tamanho mínimo de caracteres permitidos para criação de um commit

A descrição do commit não deve ser maior que 80 caracteres! Tamanho atual: X caracteres.

Validação do tamanho máximo de caracteres permitidos para criação de um commit

Ajuda

Breve descrição dos tipos permitidos em caso de dúvida antes de montar um commit:

Resumo dos tipos de commit permitidos segundo o Conventional Commits

Sucesso

Sempre que o commit estiver na estrutura correta, o commit será realizado com o seguinte retorno:

Commit correto segundo o Conventional Commits

Info

Sempre que o commit estiver na estrutura correta, mas não houver alterações a seguinte informação é retornada:

Nenhuma informação para commitar

Configurando o seu Linux para utilizar o comando

Primeiro é necessário criar um arquivo na pasta pessoal do usuário, neste caso o nome do arquivo é commitlint.scripts.py. Após criar o arquivo, dentro do arquivo coloque o seguinte conteúdo, que é o script em Python que será chamado sempre que um commit for realizado:

import re, sys, subprocess


ERROR = "msg_error"
SUCCESS = "msg_success"
WARNING = "msg_warning"
DEFAULT = "msg_default"


def __exec(command: str) -> str:
    """Método responsável por executar um comando no terminal

    Args:
        command (str): Comando para executar
    Returns:
        str: Resultado do comando executado
    """
    result = subprocess.run(
        command,
        shell=True,
        capture_output=True,
        encoding="utf8",
    ).stdout
    return re.sub(r"\n$", "", result)


def __get_args() -> list:
    """Método responsável por capturar os argumentos informados

    Returns:
        list: Array com os argumentos
    """
    # ignora o parametro da posição 0, pois é a URL do script
    # ignora o parametro da posição 1, pois é o método chamado
    return sys.argv[2 : len(sys.argv)]


def __msg(message: str, bold=True, type=ERROR, newLine=True):
    """Método responsável por formatar a mensagem antes de apresentar no console

    Args:
        message (str): Mensagem para exibição
        bold (bool, optional): Determina se a mensagem será apresentada em negrito
        type (str, optional): Tipo da mensagem (ERROR, SUCCESS, WARNING, DEFAULT). Defaults to ERROR
        newLine (bool, optional): Nova linha antes de apresentar mensagem. Defaults to True
    """
    result = "\n" if newLine else ""
    if type == SUCCESS:
        result += "✅ "
    if type == WARNING:
        result += "❕ "
    if type == ERROR:
        result += "❌ "
    if bold:
        message = __bold(message)
    print(f"{result}{message}")


def __bold(text: str) -> str:
    """Retorna o texto em negrito

    Args:
        text (str): Texto para retornar em negrito
    Returns:
        str: Texto em negrito
    """
    return f"\033[1m{text}\033[0m"


def commit_lint():
    """Método responsável por criar um novo commit validando antes se o commit está
    no padrão do Conventional Commits (https://www.conventionalcommits.org/)
    """
    # valida se o commit está sendo realizado dentro de um projeto GIT
    if not __exec("git rev-parse --is-inside-work-tree"):
        return __msg("Este script deve estar em um repositório git!")

    args = __get_args()
    type = args[0]
    message = args[1]

    if type == "-h":
        return __msg(
            __bold("Tipos de commits permitidos:")
            + "\n\tfeat     > nova funcionalidade/recurso"
            + "\n\tfix      > correção de bug"
            + "\n\tstyle    > ajustes na apresentação do código"
            + "\n\trefactor > reestruturação do código"
            + "\n\tperf     > melhoria no desempenho do código"
            + "\n\ttest     > testes do código"
            + "\n\tdocs     > atualização de documentação"
            + "\n\tbuild    > configurações gerais do código"
            + "\n\tci       > configurações de integração contínua"
            + "\n\tchore    > manutenções no código"
            + "\n\trevert   > reverter código"
            + "\nMais informações: https://schulz.net.br/blog/conventional-commits",
            False,
            WARNING,
            False,
        )

    # valida se foi informado os argumentos
    if len(args) != 2 or not args[0] or not args[1]:
        return __msg("O commit deve possuir opção (-am ou -m) e mensagem!")

    # valida se a estrutura do commit é válida
    allowedTypes = ["-am", "-m"]
    if type not in allowedTypes:
        return __msg(
            f"O commit deve possuir uma das seguintes opções: {', '.join(allowedTypes)}"
        )

    # valida se foi informado tipo e descricao
    messageToValidate = message.split(": ", 1)
    if len(messageToValidate) != 2:
        return __msg(
            __bold("O commit deve estar no padrão:")
            + "\n\t<tipo>[escopo opcional]: <descrição>"
            + "\n\n\t[corpo opcional]"
            + "\n\n\t[rodapé opcional]",
            False,
        )

    # remove indicativo de breaking change se possuir, pois não necessita de validação
    messageToValidate[0] = re.sub(r"!$", "", messageToValidate[0])

    typeCommit = messageToValidate[0].split("(")
    # cria um array com os tipos possiveis do commit de acordo com o Conventional Commits
    typesCommit = [
        "feat",
        "fix",
        "chore",
        "refactor",
        "perf",
        "style",
        "test",
        "docs",
        "build",
        "ci",
        "revert",
    ]
    # valida o tipo do commit de acordo com o Conventional Commits
    if typeCommit[0] not in typesCommit:
        return __msg(f"O tipo do commit deve ser: {', '.join(typesCommit)}")

    # se o tamanho do array for maior que 2, o escopo foi aberto mais de uma vez
    if len(typeCommit) > 2:
        return __msg("O escopo do commit foi aberto de forma errada!")
    # validações se possui escopo
    if len(typeCommit) == 2:
        # valida se possui apenas um ")" e está no final do escopo
        if typeCommit[1].count(")") != 1 or not typeCommit[1].endswith(")"):
            return __msg("O escopo do commit foi fechado de forma errada!")
        # valida se foi preenchido escopo
        if typeCommit[1] == ")":
            return __msg("O escopo do commit não deve ser vazio!")

    # valida o tamanho da descricao do commit (ignora body e footer)
    description = messageToValidate[1].split("\n")[0]
    if len(description) < 10:
        return __msg(
            f"A descrição do commit não deve ser menor que 10 caracteres! Tamanho atual: {len(description)} caracteres."
        )
    if len(description) > 80:
        return __msg(
            f"A descrição do commit não deve ser maior que 80 caracteres! Tamanho atual: {len(description)} caracteres."
        )

    # ajusta as aspas e executa o commit
    message = message.replace('"', "'")
    result = __exec(f'git commit {type} "{message}"')
    __msg(result, False, DEFAULT, False)

    # realiza o tratamento do retorno
    if result.find("file changed") != -1 or result.find("files changed") != -1:
        return __msg("Commit realizado com sucesso!", True, SUCCESS)
    if (
        result.find("nothing to commit, working tree clean") != -1
        or result.find("nothing added to commit") != -1
    ):
        return __msg("Nenhuma informação para commitar!", True, WARNING)


# array de métodos que podem ser chamados fora do script
METHODS = ["commit_lint"]

if __name__ == "__main__":
    try:
        if sys.argv[1] not in METHODS:
            raise Exception(
                f"Não é possível chamar o método '{sys.argv[1]}' fora do script!"
            )
        # executa o método informado
        globals()[sys.argv[1]]()
    except Exception as e:
        __msg(f"Erro inesperado: {str(e)}")
Obs: Caso necessário é possível modificar esse script Python para adicionar/remover validações.

Adicionar o comando gc

Após criar o arquivo commitlint.scripts.py na pasta pessoal do usuário, é necessário modificar o arquivo .bashrc e criar um comando para validar os commits de acordo com o Conventional Commits.

O que é o .bashrc?

O .bashrc é um arquivo de configuração usado pelo shell Bash, que é o interpretador de comandos padrão em muitos sistemas Unix e Linux.
Ele é executado toda vez que um terminal é aberto e permite diversas adições, como: criação de variáveis de ambiente, configurações do prompt do shell, criação de comandos e scripts personalizados.

Para modificar o arquivo .bashrc utilizando o editor Vim, siga as instruções abaixo:

  1. Abra um terminal.

  2. Digite o seguinte comando para abrir o arquivo .bashrc:

vim ~/.bashrc

Use as teclas de navegação para mover o cursor até o final do arquivo e em seguida pressione a tecla i para entrar no modo de edição.

No modo de edição adicione o seguinte código para criar um comando chamado gc para validar os commits de acordo com o Conventional Commits:

# Criação de um commit com base no Conventional Commits
#
# $1 => -am ou -m
# $2 => Mensagem do commit
gc() {
  python3 ~/commitlint.scripts.py commit_lint "$1" "$2"
}

Após finalizar as modificações no arquivo pressione a tecla Esc para sair do modo de edição.

Em seguida digite :wq (w=write, q=quit) e pressione a tecla Enter para salvar as alterações e sair do Vim.

Com o comando criado, para que seja possível utilizar o comando é necessário reiniciar o terminal ou executar o comando source ~/.bashrc para que as alterações entrem em vigor.

Após essas etapas, o comando gc estará disponível no terminal para a criação de novos commits seguindo o padrão do Conventional Commits.


As informações nesta publicação representam a minha experiência como desenvolvedor de software e pesquisas realizadas. Se você encontrou alguma inconsistência ou deseja sugerir uma melhoria entre em Contato comigo e irei ajustar.