Chapters ▾ 2nd Edition

3.1 Ramificação (Branching) no Git - Branches em Poucas Palavras

Quase todos os VCSs têm alguma forma de suporte à ramificação (branching). Branching (ramificação) significa que você diverge da linha principal de desenvolvimento e continua a fazer o trabalho sem mexer nessa linha principal. Em muitas ferramentas de VCS, este é um processo um tanto custoso, muitas vezes exigindo que você crie uma nova cópia do diretório de código-fonte, o que pode levar muito tempo em projetos grandes.

Algumas pessoas se referem ao modelo de ramificação do Git como seu “recurso matador (killer feature)”, e ele certamente destaca o Git na comunidade de VCS. Por que é tão especial? A maneira como o Git ramifica é incrivelmente leve, tornando as operações de ramificação quase instantâneas, e alternar entre as ramificações (branches) geralmente é igualmente rápido. Ao contrário de muitos outros VCSs, o Git incentiva fluxos de trabalho que ramificam (branch) e mesclam (merge) frequentemente, até várias vezes ao dia. Compreender e dominar esse recurso fornece a você uma ferramenta poderosa e única, e pode mudar completamente a maneira como você desenvolve.

Branches em Poucas Palavras

Para realmente entender a maneira como o Git faz ramificação (branching), precisamos dar um passo atrás e examinar como o Git armazena seus dados.

Como você deve se lembrar do O que é Git?, o Git não armazena dados como uma série de conjuntos de alterações (changesets) ou diferenças, mas sim como uma série de snapshots (instantâneos).

Quando você faz um commit, o Git armazena um objeto de commit que contém um ponteiro para o snapshot do conteúdo que você preparou (staged). Este objeto também contém o nome e o endereço de e-mail do autor, a mensagem que você digitou e ponteiros para o commit ou commits que vieram diretamente antes deste commit (seu pai ou pais): zero pais para o commit inicial, um pai para um commit normal e vários pais para um commit que resulta de uma mesclagem (merge) de dois ou mais branches.

Para visualizar isso, vamos assumir que você tem um diretório contendo três arquivos, e você prepara (stage) todos eles e faz o commit. Preparar (staging) os arquivos calcula um checksum para cada um (o hash SHA-1 que mencionamos em O que é Git?), armazena essa versão do arquivo no repositório Git (o Git se refere a eles como blobs) e adiciona esse checksum à área de preparação (staging area):

$ git add README test.rb LICENSE
$ git commit -m 'Initial commit'

Quando você cria o commit executando git commit, o Git calcula o checksum de cada subdiretório (neste caso, apenas o diretório raiz do projeto) e os armazena como um objeto de árvore (tree) no repositório Git. O Git então cria um objeto de commit que tem os metadados e um ponteiro para a árvore do projeto raiz para que ele possa recriar aquele snapshot quando necessário.

Seu repositório Git agora contém cinco objetos: três blobs (cada um representando o conteúdo de um dos três arquivos), uma árvore (tree) que lista o conteúdo do diretório e especifica quais nomes de arquivo são armazenados como quais blobs, e um commit com o ponteiro para essa árvore raiz e todos os metadados do commit.

Um commit e sua árvore
Figure 9. Um commit e sua árvore

Se você fizer algumas alterações e fizer commit novamente, o próximo commit armazenará um ponteiro para o commit que veio imediatamente antes dele.

Commits e seus pais
Figure 10. Commits e seus pais

Um branch no Git é simplesmente um ponteiro móvel e leve para um desses commits. O nome do branch padrão no Git é master. À medida que você começa a fazer commits, você recebe um branch master que aponta para o último commit que você fez. Toda vez que você faz um commit, o ponteiro do branch master avança automaticamente.

Note

O branch “master” no Git não é um branch especial. É exatamente como qualquer outro branch. A única razão pela qual quase todo repositório tem um é que o comando git init o cria por padrão e a maioria das pessoas não se dá ao trabalho de alterá-lo.

Um branch e seu histórico de commits
Figure 11. Um branch e seu histórico de commits

Criando um Novo Branch

O que acontece quando você cria um novo branch? Bem, fazer isso cria um novo ponteiro para você mover por aí. Digamos que você queira criar um novo branch chamado testing. Você faz isso com o comando git branch:

$ git branch testing

Isso cria um novo ponteiro para o mesmo commit em que você está no momento.

Dois branches apontando para a mesma série de commits
Figure 12. Dois branches apontando para a mesma série de commits

Como o Git sabe em qual branch você está no momento? Ele mantém um ponteiro especial chamado HEAD. Observe que isso é muito diferente do conceito de HEAD em outros VCSs aos quais você pode estar acostumado, como Subversion ou CVS. No Git, este é um ponteiro para o branch local em que você está no momento. Neste caso, você ainda está no master. O comando git branch apenas criou um novo branch — ele não mudou para aquele branch.

HEAD apontando para um branch
Figure 13. HEAD apontando para um branch

Você pode ver isso facilmente executando um comando simples git log que mostra para onde os ponteiros de branch estão apontando. Esta opção é chamada --decorate.

$ git log --oneline --decorate
f30ab (HEAD -> master, testing) Add feature #32 - ability to add new formats to the central interface
34ac2 Fix bug #1328 - stack overflow under certain conditions
98ca9 Initial commit

Você pode ver os branches master e testing que estão bem ali ao lado do commit f30ab.

Mudando de Branches

Para mudar para um branch existente, você executa o comando git checkout. Vamos mudar para o novo branch testing:

$ git checkout testing

Isso move o HEAD para apontar para o branch testing.

HEAD aponta para o branch atual
Figure 14. HEAD aponta para o branch atual

Qual o significado disso? Bem, vamos fazer outro commit:

$ vim test.rb
$ git commit -a -m 'Make a change'
O branch HEAD avança quando um commit é feito
Figure 15. O branch HEAD avança quando um commit é feito

Isso é interessante, porque agora o seu branch testing avançou, mas o seu branch master ainda aponta para o commit em que você estava quando executou git checkout para mudar de branch. Vamos voltar para o branch master:

$ git checkout master
Note
git log não mostra todos os branches o tempo todo

Se você executasse git log agora mesmo, poderia se perguntar para onde foi o branch "testing" que você acabou de criar, pois ele não apareceria na saída.

O branch não desapareceu; o Git apenas não sabe que você está interessado naquele branch e está tentando mostrar o que ele acha que você está interessado. Em outras palavras, por padrão, o git log mostrará apenas o histórico de commits abaixo do branch que você fez o checkout.

Para mostrar o histórico de commits do branch desejado, você deve especificá-lo explicitamente: git log testing. Para mostrar todos os branches, adicione --all ao seu comando git log.

HEAD se move quando você faz checkout
Figure 16. HEAD se move quando você faz checkout

Aquele comando fez duas coisas. Ele moveu o ponteiro HEAD de volta para apontar para o branch master, e reverteu os arquivos no seu diretório de trabalho de volta para o snapshot para o qual o master aponta. Isso também significa que as alterações feitas a partir deste ponto divergirão de uma versão anterior do projeto. Ele essencialmente retrocede (rewinds) o trabalho que você fez no seu branch testing para que você possa seguir em uma direção diferente.

Note
Mudar de branch altera os arquivos no seu diretório de trabalho

É importante observar que quando você muda de branch no Git, os arquivos no seu diretório de trabalho serão alterados. Se você mudar para um branch mais antigo, seu diretório de trabalho será revertido para ficar como na última vez em que você fez commit naquele branch. Se o Git não conseguir fazer isso de forma limpa (cleanly), ele não permitirá a mudança.

Vamos fazer algumas alterações e commitar novamente:

$ vim test.rb
$ git commit -a -m 'Make other changes'

Agora o histórico do seu projeto divergiu (veja Histórico divergente). Você criou e mudou para um branch, fez algum trabalho nele, e então mudou de volta para o seu branch principal e fez outro trabalho. Ambas as alterações estão isoladas em branches separados: você pode alternar entre os branches e mesclá-los quando estiver pronto. E você fez tudo isso com simples comandos branch, checkout e commit.

Histórico divergente
Figure 17. Histórico divergente

Você também pode ver isso facilmente com o comando git log. Se você executar git log --oneline --decorate --graph --all ele imprimirá o histórico de seus commits, mostrando onde estão os ponteiros do seu branch e como o seu histórico divergiu.

$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) Make other changes
| * 87ab2 (testing) Make a change
|/
* f30ab Add feature #32 - ability to add new formats to the central interface
* 34ac2 Fix bug #1328 - stack overflow under certain conditions
* 98ca9 Initial commit of my project

Como um branch no Git é na verdade um arquivo simples que contém o checksum SHA-1 de 40 caracteres do commit para o qual ele aponta, os branches são baratos de criar e destruir. Criar um novo branch é tão rápido e simples quanto gravar 41 bytes em um arquivo (40 caracteres e uma nova linha).

Isso está em nítido contraste com a maneira como a maioria das ferramentas de VCS mais antigas ramificam (branch), o que envolve copiar todos os arquivos do projeto para um segundo diretório. Isso pode levar alguns segundos ou até minutos, dependendo do tamanho do projeto, enquanto que no Git o processo é sempre instantâneo. Além disso, como estamos registrando os pais quando fazemos o commit, encontrar uma base de mesclagem (merge base) adequada para a mesclagem é feito automaticamente para nós e geralmente é muito fácil de fazer. Esses recursos ajudam a incentivar os desenvolvedores a criar e usar branches com frequência.

Vamos ver por que você deve fazer isso.

Note
Criando um novo branch e mudando para ele ao mesmo tempo

É comum criar um novo branch e querer mudar para ele ao mesmo tempo — isso pode ser feito em uma única operação com git checkout -b <newbranchname>.

Note

A partir do Git versão 2.23 em diante, você pode usar git switch em vez de git checkout para:

  • Mudar para um branch existente: git switch testing-branch.

  • Criar um novo branch e mudar para ele: git switch -c new-branch. A flag -c significa create (criar), você também pode usar a flag completa: --create.

  • Retornar ao branch do qual você fez checkout anteriormente: git switch -.