Git hooks em projetos Flutter 💙
Tags: flutter, dart, githook
Published: 2022-09-27 17:24:22


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/idbpytwbxsegyrgfquzs.png)
> 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.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zq2mofrb14fmkjh9rbig.gif)

**// 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?

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zqn5l01h300t4n2ey3ze.png)

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:

```dart
#!/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?

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6ht6a22n304ktsljtgnu.jpeg)

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:

```dart
#!/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.

```dart
#!/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?

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/icbui4g2j3m00iaouo7v.png)

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.

```dart
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!

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dykkaqztxmpm9da1haal.png)

---

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](https://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:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n3gyns9lolb9epome08c.png)

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](https://docs.flutter.dev/release/breaking-changes/flutter-lints-package), e por esse motivo, vamos utilizar o [flutter_lints](https://pub.dev/packages/flutter_lints).

No arquivo `analysis_options.yaml` adicione os vetores _analyzer > errors_ e defina a chave/valor abaixo: `avoid_print: error`

```yaml
include: package:flutter_lints/flutter.yaml

analyzer:
  errors:
    avoid_print: error
```

Agora se eu rodar um `flutter analyze` novamente, teremos um erro como resultado.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vecqez9ftpajq9c3b26g.png)

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.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k6y9rf0m70gxcmuk90j2.gif)

## 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:

```makefile
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](https://www.amazon.com/Pro-Git-Scott-Chacon-ebook-dp-B01ISNIKES/dp/B01ISNIKES/ref=mt_other?_encoding=UTF8&me=&qid=), que está gratuito na Amazon.com na versão Kindle, e no [site oficial](https://git-scm.com/book/en/v2), onde você poderá ler online ou baixar em vários formatos.

O projeto de exemplo está disponível no [github](https://github.com/emanuel-braz/flutter_githooks).

### That’s all folks!