Esse post é um complemento da Tech Talk que apresentei no dia 20/09/2022
Fala dev! E aí, tudo compilando certinho?
Neste story, eu vou mostrar como criar git hooks usando apenas Dart(ou outra linguagem de sua preferência), de uma maneira simples e sem usar nenhuma biblioteca. Eu também vou ensinar como criar teste funcional para seus scripts, como evitar commits com código quebrado, como rodar os testes antes de enviar código para o repositório, e como personalizar seu linter para não permitir commits que não estejam seguindo as guidelines do projeto.
// BEGIN TL;DR;
Git hooks são scripts executados automaticamente sempre que um determinado evento ocorre em um repositório Git. E utilizar esses gatilhos no seu projeto, pode ser uma boa maneira de melhorar ainda mais a qualidade das suas entregas.
Esses hooks são uma ótima oportunidade para começar a automatizar testes, documentação, formatação, linters e garantir a padronização do projeto, dando um boost na produtividade de seu time.
Disclaimer #1 Utilizarei um projeto Dart, mas você muito provavelmente, poderá adaptar para a sua linguagem preferida.
Disclaimer #2 Utilizarei comandos do Mac OS, por favor adapte para o sistema operacional que você estiver utilizando.
// END TL;DR;
Para começar, crie uma pasta hooks
na raiz do seu projeto
$ mkdir hooks
Crie o arquivo para o nosso primeiro script, que será executado automaticamente antes de cada commit que você fizer.
$ touch hooks/pre-commit
Agora, vamos tornar esse arquivo executável, para que o git consiga rodar ele toda vez que um evento de pre-commit for disparado.
$ chmod +x hooks/pre-commit
Pronto, já temos o necessário para começar a escrever nossos scripts! Mas antes de começarmos, é muito IMPORTANTE que você configure o git para apontar para a pasta de hooks que você acabou de criar. Assim ele saberá onde encontrar todos os nossos scripts.
$ git config core.hooksPath hooks
P.S. Você precisa estar em um repositório git, caso não esteja, inicialize com o comando $ git init
.
Vamos para o código?
A primeira coisa que precisamos fazer é usar uma shebang(#!
) na primeira linha do arquivo, isso serve para dizer ao kernel qual será o interpretador a ser utilizado quando for executar os comandos presentes nos nossos hooks.
IMPORTANTE: Configure o dart, como nosso interpretador #!/usr/bin/env dart
. O aplicativo dart deve estar previamente disponível e configurado no $PATH
da sua máquina.
E defina nosso script, que irá fazer uma análise estática do código, antes de cada commit executado. Nosso arquivo ficará assim:
#!/usr/bin/env dart
// arquivo pre-commit
import 'dart:io';
main(List<String> arguments) async {
print('* Analizando código dart, aguarde...\n');
final process = await Process.run('dart', ['analyze'], runInShell: true);
if (process.exitCode != 0) {
print('* O código não é válido. verifique o erro!\n\n${process.stdout}');
}
exit(process.exitCode);
}
Note que na linha 14 eu finalizei o script com o exitCode que foi gerado pelo processo dart analyze
. Caso ele retorne zero, então o commit será executado logo em seguida, e se for qualquer outro número, o commit será abortado. Isso evitará que a gente comite código com erro.
Você não só pode, como deve fazer análise de código no seu pipeline remoto também. Mas o legal de automatizar local, é que você já sobe tudo bonitinho, fora que você terá feedbacks mais rápidos sobre eventuais problemas, e também utilizará menos recursos de cloud.
Vamos conhecer o próximo git hook?
Outra coisa que gosto de fazer é rodar os testes de unidade, antes de cada git push
. Eu prefiro rodar os testes no push, porque os testes geralmente demoram um pouco mais, mas sinta-se livre para usar a sua imaginação e adaptar os hooks para as suas necessidades.
Para ganhar tempo, eu vou criar um hook de pre-push a partir do hook de pre-commit.
$ cp hooks/pre-commit hooks/pre-push
Irei trocar o parâmetro analyze
por test
, e quando os testes terminarem, irei dar um comando para o computador falar “os testes passaram” ou os “os testes falharam” dependendo da situação.
Onde eu trabalho todos os devs utilizam o mesmo OS, mas caso o seu time tenha sistemas operacionais diferentes, você sempre precisará considerar isso! Visto que alguns comandos podem ser diferentes para cada sistema operacional.
Para exemplificar, eu irei verificar se o sistema é Platform.isMacOS
, e garantir que estou rodando um comando compatível. Veja como ficou o meu script abaixo:
#!/usr/bin/env dart
// arquivo pre-push
import 'dart:io';
main(List<String> arguments) async {
print('* Executando os testes, aguarde...');
final process = await Process.run('dart', ['test'], runInShell: true);
print(process.stdout.toString());
if (process.exitCode == 0) {
if (Platform.isMacOS) {
Process.runSync('say', ['os testes passaram']);
} else if (Platform.isWindows) {
// faz algo
} else if (Platform.isLinux) {
// faz algo
}
} else {
if (Platform.isMacOS) {
Process.runSync('say', ['os testes falharam']);
} else if (Platform.isWindows) {
// faz algo
} else if (Platform.isLinux) {
// faz algo
}
}
exit(process.exitCode);
}
Legal né? agora quando eu tentar fazer um push, os meus testes de unidade serão executados automaticamente, dá até para eu mudar de aba ou aplicativo, porque sei que o computador irá me avisar quando eles forem concluídos! Caso os testes falhem, nada será enviado para o repositório.
O seu time utiliza “Conventional Commits”?
O CC é uma boa prática, para deixar as mensagens de commits mais inteligíveis. Isso nos ajuda a manter um histórico de commits explícito; o que torna mais fácil escrever ferramentas automatizadas em cima deles.
Caso você não saiba o que isso significa, dá uma olhadinha nesse conteúdo: www.conventionalcommits.org
A primeira coisa que iremos fazer, será criar nosso hook commit-msg
. Esse hook serve para a gente ter acesso ao arquivo que guarda a mensagem do commit no momento que tentarmos fazer um commit. Este hook sempre é executado depois do pre-commit
e antes do post-commit
, a não ser que um hook antecessor falhe e interrompa todo o processo.
$ cp hooks/pre-commit hooks/commit-msg
Caso você queira conhecer os outros hooks, utilize o comando abaixo, no seu terminal, para listar todos os hooks disponíveis.
$ man githooks
Neste momento iremos apenas verificar se a mensagem possui um dos possíveis prefixos aceitos no projeto(podemos evoluir isso em um próximo post), caso o contrário, não deixaremos fazer o commit e iremos printar quais são os prefixos permitidos no terminal.
#!/usr/bin/env dart
// arquivo commit-msg
import 'dart:io';
main(List<String> arguments) async {
final commitMessage = (await File(arguments[0]).readAsString()).trim();
final allowedPrefixes = [
'build:',
'chore:',
'ci:',
'docs:',
'feat:',
'fix:',
'perf:',
'refactor:',
'revert:',
'style:',
'test:',
'bump:'
];
final hasPrefix = allowedPrefixes.any((prefix) => commitMessage.startsWith(prefix));
if (!hasPrefix) {
print('* O commit deve conter um prefixo válido.\n'
'Os prefixos válidos são:\n\n'
'${allowedPrefixes.join(" ")}\n'
);
exit(1);
}
}
Nesse hook escrevemos algumas regras importantes, que tal já criar testes funcionais para ele?
Primeiro crie a pasta test
dentro da pasta hooks
, então crie nosso primeiro arquivo de teste commit_msg_test.dart
$ mkdir hooks/test
$ touch hooks/test/commit_msg_test.dart
No arquivo de testes, teremos dois testes. Em ambos os casos iremos escrever uma mensagem de commit dentro do arquivo .git/COMMIT_EDITMSG
, para garantir um cenário controlado.
Lembre-se, nunca devemos testar nem depender de recursos que estão além da própria unidade que estamos querendo testar.
import 'dart:io';
import 'package:test/test.dart';
void main() {
test(
'Dado que a mensagem de commit possui o prefixo "fix:", '
'Quando o git hook commit-msg for disparado, '
'Entao o programa deve retornar o exitCode 0', () async {
// arrange
ProcessResult result;
int expected = 0;
File('.git/COMMIT_EDITMSG').writeAsStringSync('fix: correcao de bug no login');
// act
result = await Process.run('hooks/commit-msg', ['.git/COMMIT_EDITMSG'], runInShell: true);
final actual = result.exitCode;
// assert
expect(actual, equals(expected));
});
test(
'Dado que a mensagem de commit não possui qualquer prefixo, '
'Quando o git hook commit-msg for disparado, '
'Entao o programa deve retornar o exitCode 1', () async {
// arrange
ProcessResult result;
int expected = 1;
File('.git/COMMIT_EDITMSG').writeAsStringSync('correcao de bug no login');
// act
result = await Process.run('hooks/commit-msg', ['.git/COMMIT_EDITMSG'], runInShell: true);
final actual = result.exitCode;
// assert
expect(actual, equals(expected));
});
}
Rode os testes da pasta hooks/test/.
$ dart test hooks/test
E voilà, todos os testes passaram!
Top ein!? Agora a gente saberá facilmente caso o teammate ou a gente mesmo, quebre algum script. Quem sabe em um próximo post a gente evolui esses testes? Deixa no comentário se você quer ver mais sobre hooks, testes ou dar outra sugestão.
Vamos definir alguns combinados de time?
Combinados de times são artefatos importantes e essenciais para times ágeis e de alta performance. Esses combinados precisam fazer sentido para um conjunto de coisas, como a maturidade do time, projeto que está rodando, momento da empresa, entre outros.
Dentre tantos combinados que podemos ter, o Code Style é um dos, senão o mais comumente utilizado pelos devs.
Digamos por exemplo que estamos criando um package para publicar no pub.dev, e queremos evitar subir código com print()
, já que sabemos que o método print irá expor informações para os usuários do nosso package.
Se você rodar um dart analyze
ou flutter analyze
no terminal, você terá o seguinte resultado:
Como você deve ter percebido, o linter acusou uma issue do tipo info
, mas o tipo info
não é o suficiente para abortar a execução, por isso, mesmo dando essa mensagem, ainda seria possível fazer commit do código contendo os prints.
Para mudar isso, precisamos dizer para o linter, que os prints devem ser considerados como erro
! Além disso, é uma boa utilizar um conjunto de lints recomendados pela Google, e por esse motivo, vamos utilizar o flutter_lints.
No arquivo analysis_options.yaml
adicione os vetores analyzer > errors e defina a chave/valor abaixo: avoid_print: error
include: package:flutter_lints/flutter.yaml
analyzer:
errors:
avoid_print: error
Agora se eu rodar um flutter analyze
novamente, teremos um erro como resultado.
Isso é o suficiente para abortar o commit, já que temos um hook de pre-commit que vai rodar um flutter analyze
e abortar todo o processo em caso de erro.
Estamos chegando ao final do artigo, Ufaaa! Bora compartilhar os git hooks com o time?
Antes de mandar tudo para o staging area, vamos criar um arquivo chamado makefile
, e usaremos ele pra facilitar com que o time configure o path dos git hooks em suas máquinas também.
Crie o arquivo makefile e crie os comandos install-hooks, uninstall-hooks e test-hooks.
$ touch makefile
O arquivo vai ficar assim:
install-hooks:
@git config core.hooksPath hooks
uninstall-hooks:
@git config --unset core.hooksPath
test-hooks:
@dart test hooks/test
Agora commita tudo, manda para o remoto e avisa pra galera rodar a instalação dos hooks na máquina deles também!
É só rodar o comando $ make install-hooks
Lembre-se sempre de usar o bom senso, tanto a escassez quanto o excesso de regras podem acabar atrapalhando mais do que ajudando, busque o equilíbrio, veja o que faz sentido para o seu time e beba bastante água.
Para saber mais sobre Git Hooks, recomendo o livro Pro Git, que está gratuito na Amazon.com na versão Kindle, e no site oficial, onde você poderá ler online ou baixar em vários formatos.
O projeto de exemplo está disponível no github.
Top comments (0)