git
Manter o código fonte do software desenvolvido em um repositório git remoto facilita a colaboração de um time de desenvolvimento e é essencial para o uso de algumas práticas contínuas de engenharia de software, como a integração e entrega contínua.
O trabalho de uma pessoa desenvolvedora envolve ler e compreender código que já existe com mais frequência do que escrever código novo. Nesse contexto, usar o git para produzir um bom histórico pode facilitar muito o trabalho de compreender o porquê de determinados trechos de código.
Neste guia de estilo para git serão apresentados recomendações para a escrita de commits, composição de pull/merge requests e manutenção de branches.
Commits
Um commit tem um conjunto de alterações em alguns arquivos, uma mensagem, uma data e um e-mail para identificar a autoria.
Neste guia de estilo será discutido qual deve ser o conteúdo de um commit, ou seja, quais alterações devem ser incluídas, e qual deve ser a mensagem que descreve essas alterações.
Para compreender melhor a importância das orientações aqui faça o seguinte exercício:
- Acesse a interface web de um serviço que gerencia o repositório git de seus projetos (ex: GitHub, GitLab ou Bitbucket)
- Abra um repositório de algum projeto que você já trabalhou e que existe há pelo menos 8 meses
- Navegue até algum arquivo antigo que tenha uma quantidade considerável de código
- Procure algum condicional, laço, cálculo ou alguma lógica que você não sabe porque existe
- Clique no botão Blame/Annotate (presente no GitHub, GitLab e Bitbucket)
- Leia o commit que introduziu essa(s) linha(s), tanto a mensagem quanto as alterações
Ficou mais claro? Ou não? Poucos projetos mantém um bom histórico, geralmente é necessário olhar alguma fonte externa para ganhar mais contexto sobre alguma parte do projeto: KBs, wiki, vídeos, documentação separada ou até mesmo perguntar para alguém com "mais tempo de casa".
Agora repita os passos acima, a partir do terceiro, no seguinte repositório:
https://github.com/openstack/nova/
Melhor? Observe mais commits no arquivo que você selecionou para dar um Blame/Annotate.
Esse repositório, e outros do projeto OpenStack, usa alguns princípios para escrever as mensagens de commit e incentiva que mantenedores avaliem também as mensagens de commit quando alguém envia uma proposta de alteração.1 Alguns anos seguindo esses princípios alcançaram o resultado que você observou. Esse rigor é muito positivo para projetos de código aberto, porque isso facilita a colaboração de pessoas externas; mas isso também pode ser muito útil para projetos fechados/privados. Imagine o tempo economizado quando o contexto de cada linha de código está tão próximo a essas linhas.
Conteúdo
Um commit ideal contém:
- A implementação: uma única "alteração lógica/funcional" no código
- Testes: os testes que provam que a implementação funciona
- Documentação: a atualização da documentação sobre a implementação
- Link para a issue
Implementação
A implementação de um commit deve conter uma única alteração lógica/funcional ou alterar apenas uma "coisa" no projeto.
O objetivo é que o resultado seja uma alteração fácil de revisar tanto no momento em que ela é proposta (no pull/merge request) quanto no futuro através de um git blame. Daqui em diante, quando ver o termo "revisar" assuma que isso se refere a qualquer momento: na proposta da alteração ou meses depois da alteração estar aplicada no projeto.
Uma nova funcionalidade, user story, caso de uso, issue ou card em um projeto provavelmente envolverá alterações em múltiplas partes do código, incluindo refatorar código antigo, escrever código novo e adicionar novos testes. Apesar disso, não é necessário que todas essas alterações estejam em um único commit. Pelo contrário, a não ser que a funcionalidade seja muito pequena, as alterações no código deve estar separada em múltiplos commits. Se houver um link para a issue na mensagem de commit, é possível no futuro ver a figura completa.
Refatoração e formatação de código sempre deve estar em commits separados, quando essas alterações estão juntas da alteração funcional no código fica bem mais difícil revisar a alteração, pois há intenções misturadas num mesmo commit. Quais comportamentos devem continuar os mesmos e quais devem ser alterados?
Refatorar não é sinônimo de alterar
O termo refatoração aqui usa a definição do livro Refactoring: Improving the Design of Existing Code, ou seja, uma alteração pequena no design/estrutura de código existente que preserva seu comportamento externo.2 Alterar o código para corrigir um defeito ou implementar uma nova funcionalidade não é refatorar.
Commits atômicos, focados em apenas uma alteração, são mais fáceis de revisar. Durante um troubleshooting fica mais fácil compreender porquê determinada linha de código está daquela forma. Commits atômicos também facilitam o uso de cherry-pick.
A definição do que é uma alteração não é bem definida, vai depender do projeto. É importante, no entanto, que em todo commit a aplicação continue funcionando. Uma aplicação Web deve continuar saudável e com as funcionalidades desenvolvidas até então funcionando, bem como um CLI.
Deploy não é Release
Lembre-se que em projetos que adotam práticas contínuas de engenharia de software, como integração e entrega contínua, é possível subir múltiplas alterações na base de código e fazer deploy dessas alterações sem concluir uma release. A exposição das novas funcionalidades pode ser postergada sem interromper o fluxo de desenvolvimento.
Testes
Quando os testes automatizados estão no mesmo commit que a implementação funcional/lógica fica mais fácil de detectar o que está sendo testado ao olhar para o commit.
Essa prática também reforça a inclusão de testes automatizados e contribui para um projeto com alta cobertura e boa produtividade. Com o aumento da quantidade de testes no projeto, para adicionar novos testes você terá acesso à diversos exemplos no próprio código fonte do projeto.
Subir o teste de uma funcionalidade em commits separados deve ser evitado.
Alguns commits podem incluir apenas testes quando os testes forem refatorados com o objetivo de eliminar test smells que prejudicam o fluxo de entrega do projeto, principalmente testes frágeis3 e testes erráticos (flaky)4. Outra situação que justifica um commit que tenha apenas testes é o pagamento de uma dívida técnica num projeto que está sem alguns testes, dependendo do estado da documentação e conhecimento geral sobre a funcionalidade pode ser mais adequado usar testes de caracterização.5
Documentação
O mesmo commit que introduz uma funcionalidade deve conter a atualização (ou criação) da documentação relacionada. Atualize as docstrings necessárias no código fonte ou outros arquivos como markdown.
Se a documentação vive num repositório separado da aplicação, tente movê-la para o mesmo repositório do código fonte. Tente resolver a construção e publicação através das ferramentas ao invés da organização em repositórios. É mais fácil de manter a documentação atualizada quando ela vive próxima ao código fonte. A documentação fica versionada e segue o mesmo versionamento da aplicação (incluindo tags) sem a necessidade de coordenar releases. A documentação pode ser revisada no merge/pull request junto com as alterações feitas ao código fonte.
Em funções puras você pode incluir exemplos na documentação e testar continuamente esses exemplos.
Exemplos de atualização de documentação:
- Docstrings, JSDoc ou similar de módulos, classes, constantes, métodos e funções públicas, que serão importados para usar em outro lugar
- API Web, independente do formato usado.
- REST com a spec OpenAPI mantendo atualizado o resumo, a descrição, os parâmetros e as respostas das operações. Use exemplos.
- GraphQL mantendo atualizado as docstrings dos tipos e das operações
- Texto de ajuda/descrição para documentar as ações e opções de CLI
- Instruções para setup do ambiente local, execução do ambiente de desenvolvimento, execução de testes, build da aplicação
Deixe claro o que pode ser melhorado
Ao atualizar uma documentação interna você pode incluir algumas etapas que precisam ser executadas manualmente. Se você já tiver em mente as restrições que existem que impediram a automação dessas etapas deixe isso explícito tanto na documentação quanto na mensagem de commit. Às vezes uma etapa não foi automatizada simplesmente por falta de tempo, para atender um prazo, e não por algum desafio técnico. Sem esse contexto alguém pode ficar na dúvida no futuro se é possível ou não automatizar esse passo.
Issue relacionada
O repositório remoto do seu projeto provavelmente está num serviço que já inclui ou está conectado a um sistema de issues/tickets. O GitHub e o GitLab tem issues, o Bitbucket é facilmente conectado ao Jira.
Inclua na mensagem de commit uma referência à issue sobre o que foi alterado no código fonte. Se não existir uma issue crie uma. A issue pode incluir muito mais contexto referente ao que foi implementado, com links para documentações, screenshots, gifs, comentários sobre problemas encontrados durante o desenvolvimento e assim por diante. Além disso, após o commit ser integrado ao projeto você não pode mais alterar sua descrição, a issue, no entanto, ainda pode ser atualizada ou receber mais comentários.
Outros tipos de commit
Nem todo commit precisa incluir todos os itens citados acima. A seguir há alguns exemplos de exceções.
Refatoração
Um commit de refatoração deve conter apenas alterações no código refatorado. Não deve mexer nos testes ou na documentação, porque refatorar não altera o comportamento da aplicação.
O commit de refatoração pode incluir uma referência a uma issue, porque você pode refatorar código com o objetivo de facilitar a correção de um bug, por exemplo.
Atualização de dependência
É interessante deixar atualização de dependências em commits separados. Ao atualizar uma versão minor ou patch de uma dependência, é esperado que o código se comporte da mesma forma, portanto não deve ser necessário atualizar os testes ou a documentação.
A atualização de uma minor ou patch pode resolver um bug na sua aplicação, nesse caso é importante incluir o teste que prova que o bug foi resolvido.
A atualização de uma major provavelmente requer atualizações no código fonte, ambas alterações no projeto devem ficar no mesmo commit, porque fica claro que aquelas alterações foram feitas para lidar com breaking changes de uma biblioteca usada pelo projeto.
O valor de deixar a atualização de dependências em commits separados é novamente facilitar o trabalho futuro de olhar para o passado do projeto. Se for identificado um bug, uma piora ou melhora de performance a partir de um deploy, fica mais fácil identificar se a alteração na versão de alguma biblioteca teve responsabilidade nisso. Se essas atualizações estiverem misturadas com a implementação de novas funcionalidades ou correções, é mais difícil fazer essa análise.
Formatação de código
Idealmente deve haver apenas um commit no histórico do projeto que formata código. Um commit introduz o formatador, um commit na sequência aplica o formatador em todos os arquivos do projeto, e um terceiro commit adiciona o segundo commit como "ignorado" pelo blame. A partir desse último commit todo código que subir no projeto deve seguir a formatação do formatador.
Deixe o editor de texto formatar
Configure o editor de texto para aplicar o formatador de código ao salvar um arquivo. Compartilhe essa configuração no repositório para que todas as pessoas que trabalhem ali tenham a mesma experiência de desenvolvimento.
O git 2.23 introduziu a opção --ignore-revs-file que permite ignorar alguns
commits ao usar o git blame. Ou seja, ao olhar para as alterações feitas em
uma linha através do git blame, esse commit que mexeu em todos os arquivos do
projeto será ignorado e não deve atrapalhar.
Crie um arquivo chamado .git-blame-ignore-revs na raiz do projeto e adicione
o hash do commit que aplica o formatador no código. Esse arquivo pode ter um
comentário explicando porquê esse commit é ignorado por padrão.
A interface do GitHub suporta isso nativamente6, o GitLab ainda não7.
A popular extensão GitLens para VS Code também suporta8, através da seguinte configuração:
{
"gitlens.advanced.blame.customArguments": [
"--ignore-revs-file",
".git-blame-ignore-revs"
]
}
Adoção incremental de um formatador
A opção de adotar incrementalmente um formatador de código existe, ou seja, aplicar a formatação apenas nas linhas que são alteradas. Porém isso exige algumas configurações adicionais ou ferramentas adicionais e as vantagens dessa abordagem não são tão claras. Algumas desvantagens são óbvias: o código fonte vai continuar com mais de um estilo e os commits vão misturar implementação com formatação de código. Essas características vão persistir até que todo o código fonte seja reformatado, ou seja, por tempo indeterminado.
Mensagem
Lembre-se que o objetivo aqui é criar commits úteis para análise futura, ou seja, eles não devem simplesmente salvar o último estado do código fonte e empurrar essas alterações para o repositório remoto. A mensagem do commit deve ter informações úteis para futuros leitores e leitoras se contextualizarem sobre as alterações.
Alguns pontos importantes para ter em mente ao escrever a mensagem de commit:
- A pessoa não sabe qual problema resolvido com aquele commit
- A pessoa não terá conhecimento do domínio do problema resolvido
- A pessoa não sabe quais restrições existiam quando o commit foi escrito
- A pessoa não sabe como você validou a alteração feita
- O seu código não é self-documenting ou self-describing
É muito comum uma pessoa nova entrar num projeto. Raramente um mesmo grupo de pessoas começa um projeto de software e permanece nele até que o projeto deixe de existir. Seu trabalho como dev envolve entrar em projetos existentes e passar a manutenção de um projeto para outro grupo de pessoas.
As pessoas que entram num projeto não participaram das discussões que resultaram nas alterações incluídas no projeto. É seu dever manter um histórico que disponibilize esse contexto.
Uma boa mensagem de commit deve, portanto, responder 3 perguntas:
- Por que essa alteração é necessária?
- Como e qual problema é resolvido com essa alteração?
- Quais são as consequências conhecidas dessa alteração?
Conventional Commits
O Conventional Commits introduz também algumas convenções herdadas do estilo do Angular. Essas convenções são usadas em muitos projetos, tornou-se um padrão esperado e facilita na identificação dos commits com a introdução do tipo e escopo. Usa essas convenções também possibilita gerar changelogs automaticamente.
Um commit que segue o Conventional Commits segue a seguinte estrutura:
<tipo>(escopo): <assunto>
<corpo>
<footer>
Sendo os tipos que usamos:
- feat: Implementa algo novo no projeto, inclui testes e documentação
- fix: Corrige algum problema no projeto, inclui teste(s)
- chore: Mudanças de configuração ou de código que não entra em produção
- ci: Alteração em arquivos de CI
- refactor: Alteração no design do código, sem alterar o comportamento
- docs: Mudanças na documentação, evite documentar separadamente da implementação
- style: Alteração no estilo/formatação do código, evite
- test: Refatoração de teste ou adição de testes novos. Evite adicionar testes em commits separados da implementação
As opções de escopo dependem do projeto. O corpo é um conjunto de parágrafos que responde as perguntas da seção anterior e o footer é uma referência a uma issue/ticket.
Template
Abaixo um template para ter uma cola disponível sempre que escrever um commit:
<tipo>(escopo): <resumo do commit em uma linha>
corpo
ref
# Explique qual problema este commit resolve e como ele é resolvido. Não
# descreva de forma superficial a alteração feita no código. Lembre-se que
# esta mensagem será lida no futuro por alguém sem contexto sobre o
# problema.
#
# Quebre o corpo da mensagem em paragráfos se necessário. Mantenha as
# linhas com menos do que 76 caracteres.
#
# Adicione as restrições que você tem em mente ou consequências dessa
# alteração. Se você teve que fazer algum teste manual para validar essa
# alteração, comente aqui também.
#
# Tipos disponíveis:
#
# feat: Implementa algo novo no projeto, inclui testes e documentação
# fix: Corrige algum problema no projeto, inclui teste(s)
# chore: Mudanças de configuração ou de código que não entra em produção
# ci: Alteração em arquivos de CI
# refactor: Alteração no design do código, sem alterar o comportamento
# docs: Mudanças na documentação, evite documentar separadamente da
# implementação
# style: Alteração no estilo/formatação do código, evite
# test: Refatoração de teste ou adição de testes novos. Evite adicionar
# testes em commits separados da implementação
Para usar o template salve o conteúdo acima num arquivo de texto, por exemplo,
na sua home ~/.gitmessage.txt e configure o git para usar esse template:
git config --global commit.template ~/.gitmessage.txt
Resumo (dos and don'ts)
Dos
- Mantenha as mensagens de commit dentro do limite de 76 colunas
- Escreva o cabeçalho do commit na voz imperativa
- Especifique o tipo do commit no cabeçalho
- Especifique a issue no rodapé do commit
- Escreva um corpo na mensagem de commit
- Escreva como e qual problema é resolvido no corpo do commit
- Escreva as restrições conhecidas no corpo do commit
- Descreva validações manuais no corpo do commit
Don'ts
- Não misture formatação de código com implementação
- Não misture atualização de bibliotecas com implementação
- Não misture múltiplas implementações em um mesmo commit
- Não deixe a aplicação quebrada
- Não polua o histórico com múltiplos commits de formatação
- Não deixe os testes de uma funcionalidade em commit separado
- Não deixe a documentação de uma funcionalidade em commit separado
Referências
- https://wiki.openstack.org/wiki/GitCommitMessages
- https://www.qemu.org/docs/master/devel/submitting-a-patch.html
- https://simonwillison.net/2022/Oct/29/the-perfect-commit/
-
https://wiki.openstack.org/wiki/GitCommitMessages ↩
-
https://refactoring.com/ ↩
-
http://xunitpatterns.com/Fragile%20Test.html ↩
-
http://xunitpatterns.com/Erratic%20Test.html ↩
-
https://michaelfeathers.silvrback.com/characterization-testing ↩
-
https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view ↩
-
https://gitlab.com/gitlab-org/gitlab/-/issues/31423 ↩
-
https://github.com/gitkraken/vscode-gitlens/issues/947 ↩