Utilizando git hooks de commit e push com Husky e dotnet format

Em tecnologia não gostamos de trabalho que não seja criativo. Por isso delegamos para automações aquele trabalho que julgamos repetitivo e massante. Uma das formas de automatizar esse trabalho indesejado é utilizando git hooks de commit ou até mesmo push utilizando soluções versionáveis como o husky. Neste artigo veremos, além da utilização do husky, como aplicar formatação ao seu código utilizando a ferramenta dotnet format.

O que é husky?

Para falarmos do husky, é preciso que você conheça primeiro os git hooks. Se você entrar em um diretório .git, vai perceber que temos lá dentro uma pasta chamada hooks. Este diretório contém arquivos de exemplo, indicando quais seriam os ganchos (hooks) aplicáveis para cada evento dentro do seu repositório git, além também de poder personalizar coisas como a “mensagem de commit” por exemplo.

O problema é que se você escrever dentro de .git/hooks/ você terá feito a configuração apenas localmente. Como a pasta não é compartilhada, você fatalmente não conseguirá distribuir os hooks construídos para as pessoas que trabalham com você. O ideal seria, inclusive, poder versionar o código! E é aqui que entra o husky:

Husky é um pacote npm que permite a personalização e versionamento de tarefas através de scripts bash

https://www.npmjs.com/package/husky

Com o Husky, a partir de scripts disponibilizados na pasta .husky você consegue personalizar hooks para ações no seu git local, além de permitir a utilização dos scripts npm disponívels no seu package.json.

Estamos falando de node ou dotnet?

Quando pensei em adicionar o Husky nos meus projetos .net, eu me fiz a mesma pergunta, movido pelo mesmo estranhamento. Afinal, eu estaria misturando duas tecnologias e obrigando as pessoas que desenvolveriam o código a, além de manter o .net atualizador, também garantir o node. Seria realmente certo misturar duas tecnologias diferentes, como quem mistura dois tecidos para fazer uma mesma roupa? Foi aí que lembrei do poliéster… Brincadeira.

Pensando no trade-off, me pareceu muito aceitável utilizar as duas tecnologias em conjunto. Eventualmente elas já são realmente utilizadas. Se você escreve back é muito grande a probabilidade de também precisar de um front em algum lugar. Os próprios templates .net já fazem isso ao disponibilizar aplicações monorepo com react ou angular. No final, me pareceu valer muito a pena o esforço.

O que é o dotnet format e o que o lint-staged está fazendo ali?

Há um tempo venho lidando com as questões relativas a qualidade de código. E me incomodou sempre o fato da comunidade javascript ter soluções prontas pra isso. ESLint, Prettier e tantos outros já fazem as correções de código, que geralmente só aparecem no code review – e não deveriam aparecer. Como não havia nada do gênero para C#? Pois agora há! É o dotnet format.

Ele nada mais é do que uma tool que, observando o arquivo .editorconfig, aplica algumas das suas configurações de código. Veja bem: algumas e não todas. Caso você tenha algum interesse em contribuir, a ferramenta está disponível em: https://github.com/dotnet/format. Evidentemente devem existir outras ferramentas do tipo. Caso conheça alguma, que tal adicionar nos comentários?

Continuando a misturar as tecnologias, você vai observar que no nosso exemplo vamos adicionar o lint-staged. Ele nada mais é do que um pacote npm que te permite alterar e re-adicionar os arquivos durante processos de pre-commit. Muito útil para o propósito do dotnet format.

Agora vamos à parte “mãos à obra” da coisa.

Instalando o Husky

Antes de qualquer coisa, garanta que você está em um projeto versionado através do git. Como o Husky trabalha com os git hooks, ter o repositório ao menos inicializado com o comando git init é indispensável.

Uma vez que já está devidamente configurado o repositório, a brincadeira começa!

Para inicializar o meu repositório com o npm init, eu prefiro deixar o arquivo package.json já pronto. Enquanto escrevo esse artigo, estou fazendo alterações no meu projeto de templates. E pensando em já entregar o package.json e o package-lock.json prontos, optei por esse caminho. Exatamente por isso você pode ver alguns comandos que acredite serem desnecessário.

npm init --yes
npx husky-init
npm install
npm install lint-staged --save-dev

Os comandos não são muito complexos e dispensam explicação. Repare apenas que o comando npx husky-init já instala e cria alguns hooks iniciais para o husky, poupando algum trabalho de início. No Readme do pacote, você pode ver outras formas de instalar, inicializar e configurar o husky.

Instalando o dotnet format

Como quase tudo em dotnet, é muito fácil de instalar e utilizar o dotnet format. Você pode fazer isso através da linha de comando e instalando globalmente.

dotnet tool install -g dotnet-format

Ou ainda pode instalar para apenas o seu repositório. Uma estratégia muito utilizada para quando se deseja que todas as pessoas (ou automações) envolvidas no desenvolvimento compartilhem sempre a mesma versão de uma ferramenta.

Para isso, primeiro precisamos criar um manifesto de ferramentas. Só depois instalamos as ferramentas necessárias, sem utilizar a tag -g. Ficando assim:

dotnet new tool-manifest
dotnet tool install dotnet-format

Com esses comandos será criado um arquivo chamado .config/dotnet-tools.json contendo todas as informações de versão, comandos e ferramentas disponíveis.

Atenção: No github do projeto há uma instrução específica para usuários do .net6. O comando a ser utilizado para acionar o dotnet format, para esses usuários, é dotnet-format. Do contrário, o mecanismo utilizado seria o do SDK.

Um truque na manga

Com essa configuração que fizemos, o usuário teria de inicializar o projeto, executando o comando dotnet tools restore. Já que estamos utilizando o node, porque não utilizá-lo ao nosso favor? Pra isso, altere o seu arquivo package.json adicionando a seguinte propriedade:

{
  "scripts": {
    "preinstall": "dotnet tool restore"
  }
}

Com este comando, toda vez que o projeto for inicializado, o comando de restauração das ferramentas do diretório será executado. Legal, né?

Configurando o pre-commit

Se tudo deu certo até agora, você terá o arquivo .husky/pre-commit no seu repositório. E dentro dele, uma chamada para npm test. Com isso, todas as vezes que você fizer um commit no seu código, o script test, configurado no seu package.json será executado. Isso pode ser um problema para nós, especialmente se os testes forem demorados. Por isso, agora vamos apagar essa linha de comando (nós já vamos utilizá-la em outro momento) e adicionar a chamada ao lint-staged, que será responsável por adicionar nossos arquivos formatados ao commit. Seu arquivo deve ficar assim:

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged --relative

As linhas 1 e 2 são boilerplate do husky. A linha que realmente importa é a número 4. Nessa linha executamos o comando lint-staged, onde o comando --relative aguarda pelos arquivos que devem ser adicionados ao stage. Mas só isso não é suficiente. É preciso mais uma alteração no package.json, desta vez, configurando qual o comando deve ser executado para cada arquivo. Adicione as propriedades a seguir no seu package.json.

{
  "lint-staged": {
    "*.cs": "dotnet format --include"
  }
}

Da forma como foi configurado, todos os arquivos com a extensão .cs terão o comando especificado executado. E o fullpath do arquivo também será passado como parâmetro. E caso o arquivo seja modificado, as alterações serão incluídas automaticamente no commit. Como se fosse mágica!

Configurando o pre-push

Dizem as boas práticas de git que você deve fazer commits constantemente. Afinal, é pra isso que ele serve: versionar código. Mas pra que essa prática seja adotada, você concorda comigo que ela precisa ser rápida. Do contrário, fatalmente você irá abandoná-la. É justamente por esse motivo que eu retirei os testes do pre-commit. Imagine você ter de testar toda a aplicação a cada commit? Onde colocar essa tarefa então?

Eu acredito que o push seria o melhor momento para aplicar algumas validações mais demoradas. O push, embora também deveria ser frequente, tem um número de execuções menor que o commit. E é interessante que você assegure que está enviando código que, no mínimo, compile e passe nos testes de unidade.

Para atingir essa meta, eu fiz algumas outras modificações no script. A primeira foi adicionar o script de teste no meu package.json

{
  "scripts": {
    "test": "dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=\"lcov%2ccobertura%2copencover\"",
    "preinstall": "dotnet tool restore"
  },
}

Repare que eu tenho ali um comando da CLI do dotnet, chamando meus testes. A diferença é que ela está gerando uma série de arquivo: lcov, cobertura e opencover. Esses formatos são necessários para gerar relatórios de code coverage e também para a extensão Coverage Gutters.

Para criar o hook de pre-push, é bastante fácil. Faça um COPY-PASTE do já existente pre-commit e renomei-o para pre-push. Pronto. O hook está criado. Fácil, não é? Mas não queremos que ele execute a mesma ação. Por isso modificamos o seu conteúdo para:

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npm test

Só isso é necessário para que, a partir de agora, sua aplicação esteja compilando e passando em todos os testes ANTES de enviar para o repositório remoto.

Disclaimer

Todas essas automações são muito legais. Adiantam o nosso trabalho pra caramba! Mas eu preciso dizer pra você, pessoa desenvolvedora que está começando agora: Não se fie apenas nas automações. É obrigação sua escrever código limpo, bem formato, seguindo os estilos definidos pelo time. Também é obrigação sua garantir que o seu código está passando nos testes de unidade e que ele está sendo executado de acordo com o esperado. E lembre-se que testes de unidade não diminuem a necessidade de testes exploratórios. Quando pensar que apenas os testes de unidade são suficientes, lembre-se dessa imagem:

Unit Testing v/s Integration Testing : r/ProgrammerHumor

Em breve vou explicar como aplicar essa mesma técnica aos seus templates!

Referências:

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.