Nesta aula, vamos explorar funcionalidades avançadas do Git que são fundamentais para equipes que buscam controle de versão eficiente e colaboração suave. Dominar rebase, cherry-pick, tags e hooks permite gerenciar o histórico de commits de forma precisa, automatizar tarefas e manter a qualidade do código. Vamos começar com um dos debates mais comuns: rebase versus merge.

Rebase vs Merge

Tanto rebase quanto merge são usados para integrar mudanças de uma branch em outra, mas cada um tem implicações diferentes no histórico. O merge cria um commit de merge que une duas linhas de desenvolvimento, preservando o contexto de quando as branches divergiram. Já o rebase reescreve o histórico ao aplicar os commits de uma branch sobre a ponta de outra, resultando em uma linha linear.

A escolha entre eles depende da política da equipe e do objetivo. Merge é mais seguro para branches compartilhadas, pois não reescreve o histórico. Rebase é útil para manter um histórico limpo antes de integrar uma feature branch, mas nunca deve ser usado em branches públicas que outros já usam.

# Exemplo de merge
$ git checkout main
$ git merge feature-branch

# Exemplo de rebase
$ git checkout feature-branch
$ git rebase main

Cherry-pick

Cherry-pick permite aplicar commits específicos de uma branch para outra, sem trazer todo o histórico. É útil para portar correções urgentes ou selecionar mudanças isoladas. O comando copia o diff do commit e cria um novo commit na branch atual.

Use com cuidado, pois pode gerar conflitos e duplicar commits. Prefira merge ou rebase quando possível, mas cherry-pick é uma ferramenta valiosa para cenários de hotfix.

# Aplicar um commit específico na branch atual
$ git cherry-pick <hash-do-commit>

# Exemplo real
$ git checkout release-1.0
$ git cherry-pick a1b2c3d

Tags

Tags são referências fixas a um commit específico, geralmente usadas para marcar versões de release (ex: v1.0.0, v2.1.0). Elas podem ser leves (apenas um ponteiro) ou anotadas (com metadados como autor, data e mensagem). Tags anotadas são recomendadas para releases, pois armazenam informação adicional.

Tags são imutáveis e não se movem com novos commits. Para compartilhá-las, é necessário fazer push explícito com git push origin --tags.

# Criar tag anotada
$ git tag -a v1.0.0 -m "Versão 1.0.0"

# Listar tags
$ git tag

# Enviar tags para o remoto
$ git push origin --tags

Hooks (Visão Geral)

Hooks são scripts que o Git executa automaticamente em pontos específicos do ciclo de vida (commit, push, merge, etc.). Eles ficam no diretório .git/hooks e podem ser escritos em qualquer linguagem (bash, Python, etc.). Hooks do lado do cliente (como pre-commit) podem validar código ou executar testes antes de um commit. Hooks do lado do servidor (como pre-receive) podem impor políticas de aceitação.

Para ativar um hook, remova a extensão .sample do arquivo correspondente e torne-o executável. Exemplo comum: pre-commit para rodar linter e testes unitários.

# Exemplo de hook pre-commit (salvo em .git/hooks/pre-commit)
#!/bin/sh
# Verifica se há arquivos com erros de sintaxe
if ! npm run lint; then
  echo "Erro no lint. Commit cancelado."
  exit 1
fi

Boas Práticas e Observações Finais

  • Preferir merge para branches compartilhadas e rebase para branches locais ou de feature.
  • Usar cherry-pick com moderação e documentar o motivo.
  • Sempre criar tags anotadas para versões oficiais.
  • Não versionar hooks no repositório; compartilhe scripts de instalação separadamente.

Referências

Exercícios

  1. Crie um repositório Git local, faça dois commits na branch main, crie uma branch feature com dois commits adicionais. Faça o rebase da feature sobre main. Explique o que acontece com o histórico.

    ✓ Resposta: O rebase pega os commits da branch feature e os aplica sobre o commit mais recente da main, resultando em um histórico linear. Os commits originais da feature são reescritos com novos hashes. Após o rebase, a branch feature aponta para os novos commits.
  2. Usando o mesmo repositório do exercício anterior, faça um cherry-pick do primeiro commit da branch feature para a branch main. Verifique o log.

    ✓ Resposta: O comando git cherry-pick <hash-primeiro-commit-feature> aplica as mudanças do commit na branch main, criando um novo commit com hash diferente. O log mostrará um commit extra em main com as mesmas alterações.
  3. Crie uma tag anotada chamada v1.0.0 no commit atual e envie-a para um repositório remoto (simulado). Liste as tags.

    ✓ Resposta:
    $ git tag -a v1.0.0 -m "Versão 1.0.0"
    $ git push origin v1.0.0
    $ git tag
    v1.0.0
  4. Crie um hook pre-commit que impeça commits com mensagens vazias. Teste criando um commit com e sem mensagem.

    ✓ Resposta: Crie o arquivo .git/hooks/pre-commit com:
    #!/bin/sh
    if [ -z "$GIT_EDITOR" ] && [ -z "$(git log -1 --format=%s)" ]; then
      echo "Mensagem de commit não pode ser vazia"
      exit 1
    fi
    Torne-o executável: chmod +x .git/hooks/pre-commit. Ao tentar commitar sem mensagem, o hook cancela.
  5. Explique a diferença entre merge e rebase e quando usar cada um.

    ✓ Resposta: Merge preserva o histórico completo com um commit de merge, mostrando quando as branches divergiram e foram unidas. Rebase reescreve o histórico, aplicando commits linearmente, resultando em um histórico mais limpo. Use merge em branches públicas (ex: main) para evitar reescrita. Use rebase em branches de feature locais antes de integrar, para manter histórico linear.