Nesta aula, vamos explorar recursos avançados do Git que são fundamentais para equipes de DevOps. Dominar rebase, cherry-pick, tags e hooks permite manter um histórico limpo, integrar mudanças de forma seletiva e automatizar tarefas repetitivas. Vamos direto ao ponto, com exemplos práticos em Bash.

Se você já conhece os comandos básicos do Git, agora é hora de elevar seu nível. Veremos como cada técnica resolve problemas reais do dia a dia, desde a organização de branches até a automação de deploys.

Rebase vs Merge

Ambos integram mudanças de uma branch em outra, mas com diferenças importantes. Merge cria um commit de merge, preservando o histórico exato de quando as branches divergiram. Rebase reaplica os commits de uma branch sobre a outra, resultando em um histórico linear e mais limpo.

O merge é mais seguro para branches públicas, pois não reescreve o histórico. Já o rebase é ideal para branches locais ou de feature, permitindo evitar commits de merge desnecessários. No entanto, rebase nunca deve ser usado em branches compartilhadas, pois reescreve commits que outros podem ter como base.

Exemplo de merge:

git checkout main
git merge feature-branch

Exemplo de rebase:

git checkout feature-branch
git rebase main

Após o rebase, a branch feature-branch parece ter sido criada a partir do commit mais recente de main. Em seguida, pode-se fazer um merge fast-forward em main.

Cherry-pick

O comando git cherry-pick permite aplicar um commit específico de uma branch para outra, sem trazer todo o histórico. É útil para portar correções urgentes ou commits isolados entre branches.

Sintaxe: git cherry-pick <commit-hash>. Você pode cherry-pick múltiplos commits em sequência. Se houver conflitos, resolva normalmente e use git cherry-pick --continue.

Exemplo: aplicar o commit abc123 da branch hotfix para main:

git checkout main
git cherry-pick abc123

Use com moderação, pois cherry-pick pode duplicar commits e dificultar o rastreamento. Prefira merge ou rebase quando possível.

Tags

Tags são referências estáticas que marcam pontos específicos no histórico, como versões de release. Existem dois tipos: lightweight (apenas um ponteiro para um commit) e annotated (armazenam metadados como autor, data e mensagem).

Para criar uma tag anotada:

git tag -a v1.0.0 -m "Versão 1.0.0"

Para listar tags:

git tag -l

Para enviar tags ao repositório remoto:

git push origin v1.0.0

Tags são imutáveis e não devem ser movidas após o push. Use-as para releases estáveis, e combine com workflows de CI/CD para disparar deploys.

Hooks (visão geral)

Hooks são scripts que o Git executa automaticamente em determinados eventos, como antes de um commit ou após um merge. Eles ficam na pasta .git/hooks e podem ser escritos em qualquer linguagem (bash, Python, etc.).

Exemplos de hooks úteis: pre-commit (validar código, rodar linters), post-commit (enviar notificações), pre-push (rodar testes antes de enviar).

Para ativar um hook, remova a extensão .sample do arquivo correspondente e torne-o executável:

mv .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

Exemplo de hook pre-commit simples (verifica se há espaços em branco no final das linhas):

#!/bin/bash
if grep -I -rn '[[:blank:]]$' --include='*.py' .; then
    echo "Erro: há espaços em branco no final das linhas. Remova-os antes de commitar."
    exit 1
fi

Hooks são locais a cada repositório; para compartilhá-los, considere ferramentas como pre-commit (framework de hooks) ou inclua scripts no repositório e configure-os via Makefile.

Boas práticas e observações finais

Use rebase para manter histórico linear em branches de feature, mas prefira merge em branches públicas. Cherry-pick é útil para correções pontuais, mas evite uso excessivo. Tags anotadas são preferíveis para releases. Hooks automatizam verificações e economizam tempo, mas lembre-se de que não substituem validações no servidor.

Exercícios

  1. Crie um repositório Git local. Faça dois commits na branch main. Crie uma branch feature, faça mais dois commits. Faça o rebase da feature sobre main e depois faça um merge fast-forward em main. Mostre o log.

    ✓ Resposta:
    git init repo && cd repo
    echo "commit1" > file1 && git add . && git commit -m "commit1"
    echo "commit2" > file2 && git add . && git commit -m "commit2"
    git checkout -b feature
    echo "feature1" > feature1 && git add . && git commit -m "feature1"
    echo "feature2" > feature2 && git add . && git commit -m "feature2"
    git rebase main
    git checkout main
    git merge feature
    git log --oneline
  2. No mesmo repositório, faça um cherry-pick do segundo commit da branch main para uma nova branch hotfix. Verifique o histórico.

    ✓ Resposta:
    git checkout -b hotfix main~1  # cria branch hotfix a partir do commit anterior ao segundo
    git cherry-pick main  # aplica o commit apontado por main (o segundo)
    git log --oneline
  3. Crie uma tag anotada v1.0.0 no commit atual do main. Liste as tags e mostre os detalhes da tag.

    ✓ Resposta:
    git tag -a v1.0.0 -m "Versão 1.0.0"
    git tag -l
    git show v1.0.0
  4. Crie um hook pre-commit que impeça commits com a palavra "TODO" em qualquer arquivo .txt. Teste criando um arquivo com TODO e tentando commitar.

    ✓ Resposta:
    cd .git/hooks
    cat > pre-commit << 'EOF'
    #!/bin/bash
    if grep -rn "TODO" --include="*.txt" .; then
        echo "Erro: arquivos .txt contêm 'TODO'. Commit abortado."
        exit 1
    fi
    EOF
    chmod +x pre-commit
    cd ../..
    echo "TODO: fazer algo" > arquivo.txt
    git add .
    git commit -m "teste" # deve falhar
  5. No hook pre-commit criado, modifique-o para pular a verificação se a variável de ambiente SKIP_HOOK estiver definida como "1". Teste com e sem a variável.

    ✓ Resposta:
    cd .git/hooks
    cat > pre-commit << 'EOF'
    #!/bin/bash
    if [ "$SKIP_HOOK" = "1" ]; then
        exit 0
    fi
    if grep -rn "TODO" --include="*.txt" .; then
        echo "Erro: arquivos .txt contêm 'TODO'. Commit abortado."
        exit 1
    fi
    EOF
    cd ../..
    # Teste sem variável (deve falhar)
    echo "TODO" > outro.txt && git add . && git commit -m "teste sem skip"
    # Teste com variável (deve passar)
    SKIP_HOOK=1 git commit -m "teste com skip"

Referências