Encontrando erros com Git Bisect

Um cenário que quase não acontece no nosso dia-a-dia é alguém enviar um commit que quebra o build do projeto. Mesmo com uma bateria de testes e um servidor de integração contínua, aparece um commit que quebra tudo.

Uma das várias ferramentas a disposição de quem precisa manter um olho no build do projeto pra garantir que tudo está green é o comando bisect do git.

Como funciona

Imagine que você tem dois pontos no tempo, hoje, onde o build está vermelho, e ontem, onde você viu que o build estava verde antes de sair do escritório. Em algum momento entre esses dois pontos algum commit quebrou o build e todo mundo recebeu um e-mail do jenkins. Como encontrar o exato momento onde o build quebrou?

Uma das diversas formas é realizar uma busca binária nos commits do projeto. Então você tem um certo commit de ontem, que você sabe que está funcionando, e o commit atual, que está quebrado. O que você pode fazer é procuar os commits entre estes dois e executar um git checkout para verificar se os testes passam, certo?

Essa é a ideia por trás do git bisect. Apesar de simples, ela é bem útil, pois realiza esse processo pra você. Ao iniciar um git bisect você indica um commit que contém um estado bom do seu repositório (good) e um commit que contém um estado ruim do seu repositório (bad). Com isso o git vai realizando checkouts, seguindo uma busca binária, e você pode indicar se o estado é bom ou ruim. Ao final, o git lhe diz qual commit danificou o repositório.

Mão na massa

Para exemplificar o uso do git bisect, considere o seguinte repositório (https://github.com/MarcosX/git_bisect_tutorial). É um repositório com código ruby e alguns testes com rspec. Se você clonar o repositório, verá que os testes não estão passando.

Uma olhada no git log mostra a seguinte saída:

git_bisect_example (master) $ git lg
* fe41160 (HEAD, origin/master, master) Refactoring evaluate
* cff0b74 Adding tie test case
* eda39b0 Refactoring evluator
* 4a35ef0 Player 2 wins
* 63735b1 Refactoring evaluate
* a6a5224 Player 1 wins
* 64432c0 First commit
* 7ace5bc Initial commit

Sabemos que no commit onde o master está apontando os testes estão falhando. Também podemos assumir que o primeiro commit os testes estão passando, provavelmente nem existam testes ainda. Então vamos começar com o git bisect:

git_bisect_example (master) $ git bisect start
git_bisect_example (master) $ git bisect good 7ace5bc
git_bisect_example (master) $ git bisect bad master
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[63735b13802427f9326b3469a41607849335bce6] Refactoring evaluate

O primeiro comando inicia o processo de rastreamento de commits do git. Em seguida indicamos um commit onde os testes estão passando, utilizando o SHA do commit (no exemplo o commit foi 7ace5bc Initial commit). Logo abaixo indicamos um commit onde os testes estão falhando (no exemplo o commit onde o master está apontando). Após isso, o git informa que existem 3 possíveis revisões para serem analisadas e informa que atualmente o master está apontando para o commit “63735b1 Refactoring evaluate”. Um git log mostra isso mais claramente:

git_bisect_example ((no branch)) $ git lg
* 63735b1 (HEAD) Refactoring evaluate
* a6a5224 Player 1 wins
* 64432c0 First commit
* 7ace5bc (refs/bisect/good-7ace5bc4fc9f3830e8a067216ce5607df65c9f8c) Initial commit

Ou seja, o git reduziu o repositório pela metade. Agora precisamos indicar se esse commit é bom ou ruim. Após rodar os testes vemos que tudo passa, então esse é um bom commit:

git_bisect_example ((no branch)) $ git bisect good
Bisecting: 1 revision left to test after this (roughly 1 step)
[eda39b0c80b4d66ad2372a0b3302887c0c3486d7] Refactoring evluator

Como o commit foi bom, então o git sabe que o commit que introduziu o problema está na outra metade do repositório. Uma olhada no git log mostra:

git_bisect_example ((no branch)) $ git lg
* eda39b0 (HEAD) Refactoring evluator
* 4a35ef0 Player 2 wins
* 63735b1 (refs/bisect/good-63735b13802427f9326b3469a41607849335bce6) Refactoring evaluate
* a6a5224 Player 1 wins
* 64432c0 First commit
* 7ace5bc (refs/bisect/good-7ace5bc4fc9f3830e8a067216ce5607df65c9f8c) Initial commit

Temos dois bons commits e o commit atual. Mais uma vez rodamos os testes e vemos que eles falham. Então vamos marcar esse commit como ruim.

git_bisect_example ((no branch)) $ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[4a35ef0f64a686e6301d63736c507f51f2804400] Player 2 wins

Esta é a última revisão para encontrar o commit que introduziu o problema. Rodando os testes de novo vemos que eles passam, então marcamos este commit como bom e o git nos mostra o último commit ruim:

git_bisect_example ((no branch)) $ git bisect good
eda39b0c80b4d66ad2372a0b3302887c0c3486d7 is the first bad commit
commit eda39b0c80b4d66ad2372a0b3302887c0c3486d7
Author: Marcos Brizeno
Date:   Mon Nov 5 21:19:17 2012 -0300

    Refactoring evluator

:040000 040000 595a29074b383565186194f054f8bd2a208fb812 e91bf98032a324f5316f0b4dd5360e3a49a3b30a M      lib

Com isso conseguimos encontrar o commit que introduziu o erro. Para finalizar o git bisect e voltar para onde o master aponta, basta executar:

git_bisect_example ((no branch)) $ git bisect reset

Com isso podemos dar um git show e analisar o commit que quebrou os testes:

git_bisect_example (master) $ git show eda39b0c80b4d66ad2372a0b3302887c0c3486d7

Automatizando

Uma opção ao executar um bisect é passar um commando que será executado automaticamente a cada checkout do git. Dessa forma ele faz o trabalho por nós e apenas nos diz o commit que introduziu o erro. Basta apenas iniciar o bisect e apontar um commit bom e um ruim. Depois passamos o comando utilizar o git bisect run:

git_bisect_example (master) $ git bisect start
git_bisect_example (master) $ git bisect good 7ace5bc
git_bisect_example (master) $ git bisect bad master
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[63735b13802427f9326b3469a41607849335bce6] Refactoring evaluate
git_bisect_example ((no branch)) $ git bisect run rspec
running rspec

Esse comando vai executar o rspec entre cada checkout. Após algum tempo o git vai nos mostrar o commit que introduziu o problema, da mesma forma que foi encontrado antes.

Customize seu Git! .gitconfig

Quando se fala em controle de versão, a primeira coisa que vem a cabeça da grande maioria dos desenvolvedore é git. Neste post vamos falar um pouco sobre como melhorar ainda mais o seu desempenho com o git através da configuração e customização de comandos.

Níveis de customização

Uma das melhores funcionalidades do git é a capacidade de customização. Um exemplo de customização é uma coisa que todo mundo faz, e até é recomendado quando se instala o git, é configurar o seu nome de usuário e email:

$>git config --global user.name "Nome"
$>git config --global user.email email@email.com

Ao executar o um git config, as informações ficam salvas em um arquivo, que é lido pelo git quando algum comando é executado. No exemplo acima estamos criando uma configuração global do git, por isso a flag –global é utilizada. Isto quer dizer que qualquer diretório git do seu usuário irá utilizar essa configuração. As opções globais são colocadas em arquivo chamado .gitconfig no seu diretório home:

$>cat ~/.gitconfig
[user]
	name = Nome
	email = email@email.com

Um outro nível de customização se refere ao projeto atual. Um exemplo é quando você adiciona o repositório remoto “origin” ao seu projeto:

$>git remote add origin git@github.com/Nome/repo.git

Se você abrir o arquivo “.git/config” do seu projeto, você verá algo assim:

$>cat .git/config
[remote "origin"]
	url = git@github.com:Nome/repo.git
	fetch = +refs/heads/*:refs/remotes/origin/*
http://"master"
	remote = origin
	merge = refs/heads/master

Essa configuração é exclusiva de um projeto, por isso fica armazenada dentro da pasta .git no diretório raiz do projeto.

O outro nível de customização do Git é relativo ao sistema como todo. Quando fizemos uma configuração global, o arquivo fica armazenado no diretório home do usuário, no entanto, as configurações a nível de sistema se aplicam a todos os usuário:

$>git config --system user.name "Nome"

As configurações de sistema ficam armazenadas em “/etc/gitconfig”, mas dificilmente são feitas.

Também existe uma ordem herárquica entre os níveis de configurações. As configurações de projeto sobreescrevem as configurações globais, que por sua vez, sobreescrevem as configurações de sistema. Ou seja em caso de conflito a prioridade de configuração é: projeto>global>sistema.

Assim, na maior parte do tempo você vai querer realizar configurações globais, para que tenham efeito em todos os seus projeto, deixando apenas as configurações de branches como locais.

Configurações

Todas as configurações do git podem ser feitas tanto executando comandos no terminal quanto editando o arquivo correspondente ao nível de customização. Por exemplo, executar o comando:

$>git config --global color.ui auto

é o mesmo que alterar o arquivo ~/.gitconfig e adicionar:

[color]
  ui = auto

Vamos então ficar com a segunda opção por ser mais rápida.

Primeiro vamos alterar as configurações de cores do git. Quando você executa um git status ou git log, por padrão, são apenas mostradas letras brancas no fundo preto. Na seção de configuração [color] é possível resolver isso, então abra o arquivo ~/.gitconfig e adicione o seguinte:

[color]
  status = auto
  diff = auto
  branch = auto
  interactive = auto
  ui = true

Com isso, quando você executar um git status, por exemplo, vão ser exibidos em vermelho os arquivos que não foram adicionados e em verde os que foram.

Uma outra configuração que eu também gosto muito é a de sempre utilizar a opção –rebase ao executar um git pull:


  autosetuprebase = always

A diferença é que, ao executar apenas pull será criado um commit com o merge entre os branches. Quando se utiliza –rebase, o git remove seus commits locais, atualiza a base de código e em seguida aplica seus commits. Eu particularmente gosto disso pois seu histórico não fica “sujo” com commits sobre merge entre branches.

Existem várias outras configurações que podem ser aplicadas no git, no final do post tem dois links que falam sobre isso, vale a pena dar uma conferida.

Alias

Outra forma de customização do git é a utilização de aliases para os seus comandos. Um alias é só uma outra forma de executar um comando. Um exemplo bem simples é o seguinte:

[alias]
  co = checkout

Assim, ao executar git co é o mesmo que executar git checkout. No entanto é possível adicionar opções para os comandos também, alguns que eu gosto bastante são:

[alias]
  st = status -s
  lg = log --graph --pretty=oneline --abbrev-commit --decorate

Ao executar git st veremos apenas o nome dos arquivos e o estado atual deles, adicionado, não adicionado, etc. Uma maneira bem mais simples de executar o git status.

Ao executar git lg será mostrado o log do git em forma de grafo, mostrando os branches e as divisões, os commits serão exibidos em uma linha, o código SHA1 do commit será abreviado para o formato mínimo com sete dígitos e por fim será mostrado para qual commit o HEAD está apontando e os branches onde este commit está presente.

Informações customizadas

Também é possível configurar informações customizadas com o git, por exemplo:

[foo]
  bar = baz

Ou o seguinte comando:

$>git config foo.bar = baz

Essa informação pode ser recuperada da seguinte forma:

$>git config foo.bar
baz

Exemplo

O meu arquivo de configuração global é esse: (git://gist.github.com/3969565.git)

[alias]
  st = status -s -b
  co = checkout
  ci = commit
  lg = log --graph --pretty=oneline --abbrev-commit --decorate
  timeline = log --graph --branches --pretty=oneline --decorate --abbrev-commit
[color]
  status = auto
  diff = auto
  branch = auto
  interactive = auto
  ui = true

  autosetuprebase = always
[push]
  default = tracking

Você pode utilizá-lo como exemplo e/ou adicionar seus próprio atalhos e configurações para melhorar ainda mais seu desempenho com o git.

Mais informações

Para mais informações sobre configurações do git, eu recomendo muito o seguinte video http://marakana.com/s/video_git_tips_and_tricks_for_ruby_teams,422/index.html. É uma palestra de Jared Grippe onde ele fala sobre a sua experiência com o Git e mostra vários exemplos de configurações e outros comandos legais do Git.

Um outro vídeo, bem mais longo, mas que fala com detalhe sobre as partes mais internas do Git é esse http://blip.tv/scott-chacon/git-tips-4232122 de Scott Chacon.

Git diff menos doloroso com Meld

Se você já utilizou alguma vez, mesmo que só por curiosidade, o comando git diff para visualizar diferenças entre arquivos, commits, diretórios, etc., deve ter visto uma tela pouco amigável como essa:

A tela é bem útil, o que está em vermelho foi removido e o que está em verde foi adicionado, simples e rápido.

Mas quando a mudança é grande, as linhas verdes e vermelhas se entrelaçam, linhas são repetidas para prover contexto e a comparação pode ficar um pouco confusa.

Em plataformas como o Mac, existem várias ferramentas gráficas que auxiliam o gerenciamento de projetos git. Até mesmo o Windows recebeu uma ferramenta do github (http://windows.github.com/).

No linux, a única opção gráfica é o gitk, que não é essa coca-cola toda. Principalmente quando é necessário comparar códigos com o git diff, pois a interface apresenta as mesmas informações do terminal. No entanto é possível fazer uma análise bem legal das diferenças utilizando a ferramenta Meld (http://meldmerge.org/).

Com o Meld podemos comparar visualmente e lado-a-lado as diferenças entre os dois códigos. O que é uma mão na roda para desenvolvedores, que assim como eu, são desprovidos de um mecenas que banque um MacBook =]

Para utilizar o Meld efetivamente com git basta instalar o aplicativo (via apt-get) e configurar algumas coisas.

sudo apt-get install meld

É necessário também criar um script para que os arquivos passados pelo git diff sejam enviados corretamente para o meld. Crie o arquivo em qualquer local, mas salve a localização para quando formos configurar o git. No exemplo, eu coloquei dentro da pasta .config/ na home do meu usuário.

touch ~/.config/git_meld_diff.sh
echo "#!/bin/bash" >> ~/.config/git_meld_diff.sh
echo "meld \"\$5\" \"\$2\"" >> ~/.config/git_meld_diff.sh
chmod +x ~/.config/git_meld_diff.sh

Pronto, com o script construído e com a autorização para ser executado, basta configurar o git para utilizá-lo como ferramente de diff.

git config --global diff.external ~/.config/git_meld_diff.sh

Se você quiser, basta fazer o download desses comandos em um único script automatizado disponível no Gist (https://gist.github.com/3004245) e executar o script como sudo, devido a necessidade de instalação.