Nesta página

Git Branch: por dentro do sistema leve de ponteiros do Git

Em resumo: no Git, um branch não é uma cópia do diretório nem um snapshot pesado dos arquivos-fonte. No formato de referência files padrão do Git, ela é simplesmente uma referência leve e mutável — um arquivo de texto de 41 bytes que contém um hash SHA-1 de commit com 40 caracteres e um caractere de nova linha — armazenado no diretório .git/refs/heads/. Criar, renomear ou excluir branches no Git é praticamente instantâneo porque essas operações apenas alteram os arquivos de referência, em vez de modificar ou copiar os arquivos do seu diretório de trabalho.

Nota sobre o escopo: a descrição do “arquivo de 41 bytes” é exata para o backend de referências files padrão do Git quando um branch está desempacotada. As referências também podem ser empacotadas em .git/packed-refs pelo git gc, e o Git oferece um backend alternativo reftable, no qual os branches não são arquivos individuais. Os comandos e o comportamento são idênticos em todos os casos — só muda a representação no disco. Consulte Git Refs para saber mais.


Um branch é uma ref, não uma cópia dos seus arquivos

Para entender por que o modelo de branches do Git é tão revolucionário, é útil compará-lo a sistemas de controle de versão (VCS) mais antigos, como Subversion (SVN) ou Perforce. Nesses sistemas legados, criar um branch significava fazer uma cópia física de toda a árvore de diretórios do projeto. Se sua base de código tivesse 5 GB, criar um branch significava copiar 5 GB de arquivos no servidor. Essa operação demorava, desperdiçava espaço em disco e desencorajava os desenvolvedores a criar branches para tarefas pequenas.

O Git não copia arquivos. Em vez disso, o Git organiza o histórico do projeto como um grafo acíclico direcionado (DAG) de objetos de commit. Cada commit aponta para seu(s) commit(s) pai(s), criando uma estrutura semelhante a uma árvore para o histórico. Nesse grafo:

  • Um commit representa um snapshot dos arquivos rastreados do seu projeto em um momento específico, junto com metadados do autor e links para os commits pais. Consulte /git-commit para ver detalhes sobre a estrutura dos objetos de commit.
  • Um branch é simplesmente uma referência nomeada que aponta para um commit específico nesse grafo. Ela é um ponto de partida a partir do qual o Git pode percorrer os ponteiros para os commits pais e construir o histórico daquele branch.

Imagine um branch como uma nota adesiva presa a um nó específico em uma cadeia de commits. À medida que você trabalha e cria novos commits no branch ativo, o Git move automaticamente a nota adesiva para a frente, para apontar para o commit mais recente. Como um branch é apenas uma referência (ou “ref”), as operações de branching do Git exigem apenas a escrita ou a alteração de uma pequena string de 40 caracteres.

Para uma análise mais detalhada do sistema geral de referências do Git, leia nosso guia sobre /git-refs.

Três referências costumam se confundir: refs/heads/main é o seu branch local; refs/remotes/origin/main é o seu registro local do branch remoto, atualizado por comandos como git fetch; e HEAD identifica o branch atualmente em checkout, ou um commit diretamente no estado detached HEAD. Um git push bem-sucedido também pode atualizar a ref de rastreamento remoto correspondente.


Acompanhe a mudança de refs/heads ao criar branches

Para compreender totalmente a mecânica dos branches do Git, vamos fazer um passo a passo no terminal e observar como o diretório .git/ muda durante as operações de branching.

Etapa 1: inicializar um repositório limpo

Vamos criar um repositório novo, adicionar um commit e examinar a estrutura do diretório .git/.

$ mkdir git-branch-demo
$ cd git-branch-demo
$ git init
Initialized empty Git repository in /users/admin/git-branch-demo/.git/

Neste estágio, se listarmos o conteúdo de .git/refs/heads/, veremos que ele está completamente vazio:

$ ls -la .git/refs/heads/
total 0
drwxr-xr-x  2 admin  staff  64 Jul  7 10:25 .
drwxr-xr-x  4 admin  staff 128 Jul  7 10:25 ..

Ainda não existe um arquivo de branch padrão porque nenhum commit foi criado. Vamos ver para onde .git/HEAD aponta:

$ cat .git/HEAD
ref: refs/heads/main

Embora refs/heads/main ainda não exista no disco, HEAD já está configurado para apontar para ele. Esse estado é conhecido como “branch não nascido” (unborn branch). Vamos criar nosso primeiro commit para fazer o branch existir.

$ echo "Hello Git Branching!" > README.md
$ git add README.md
$ git commit -m "Initial commit"
[main (root-commit) f24e93e] Initial commit
 1 file changed, 1 insertion(+)
 create mode 100644 README.md

Agora que existe um commit, vamos inspecionar novamente .git/refs/heads/:

$ ls -la .git/refs/heads/
total 8
drwxr-xr-x  3 admin  staff   96 Jul  7 10:27 .
drwxr-xr-x  4 admin  staff  128 Jul  7 10:25 ..
-rw-r--r--  1 admin  staff   41 Jul  7 10:27 main

Um arquivo chamado main apareceu! Vamos imprimir o conteúdo desse arquivo:

$ cat .git/refs/heads/main
f24e93e25b90f46c6a7e0c4a45adbb474f8e6c7c

O arquivo contém exatamente um hash de commit SHA-1 de 40 caracteres seguido por um caractere de nova linha. Vamos verificar para qual objeto esse hash aponta usando o comando de baixo nível git cat-file:

$ git cat-file -t f24e93e25b90f46c6a7e0c4a45adbb474f8e6c7c
commit

E vamos imprimir o conteúdo do objeto de commit para ver como ele representa nosso snapshot:

$ git cat-file -p f24e93e25b90f46c6a7e0c4a45adbb474f8e6c7c
tree 5b38a7c29375ab2b4a53239a584a22c5432a138b
author User <user@example.com> 1783420000 +0000
committer User <user@example.com> 1783420000 +0000

Initial commit

Etapa 2: criar um novo branch

Agora vamos criar um novo branch chamado feature-login usando o comando git branch:

$ git branch feature-login

Vamos conferir o conteúdo de .git/refs/heads/ para ver o que o Git fez internamente:

$ ls -la .git/refs/heads/
total 16
drwxr-xr-x  4 admin  staff  128 Jul  7 10:30 .
drwxr-xr-x  4 admin  staff  128 Jul  7 10:25 ..
-rw-r--r--  1 admin  staff   41 Jul  7 10:30 feature-login
-rw-r--r--  1 admin  staff   41 Jul  7 10:27 main

Um novo arquivo chamado feature-login foi criado. Vamos ler seu conteúdo:

$ cat .git/refs/heads/feature-login
f24e93e25b90f46c6a7e0c4a45adbb474f8e6c7c

Ele contém exatamente o mesmo hash de commit que main! O Git não copiou nenhum arquivo-fonte. Ele simplesmente criou um arquivo de texto de 41 bytes apontando para o commit existente (supondo o formato de referência files padrão e desempacotado). Isso explica por que a criação de branches é instantânea.

Vamos conferir nosso branch ativo lendo .git/HEAD:

$ cat .git/HEAD
ref: refs/heads/main

HEAD ainda aponta para main, o que significa que continuamos no branch main. Para trocar de branch, precisamos atualizar a referência HEAD, algo normalmente feito por comandos como git checkout ou git switch. Leia mais sobre esse mecanismo em /git-checkout.

Etapa 3: fazer commit em um branch

Vamos trocar para nosso novo branch e fazer uma alteração.

$ git checkout feature-login
Switched to branch 'feature-login'

Agora vamos verificar .git/HEAD:

$ cat .git/HEAD
ref: refs/heads/feature-login

HEAD foi atualizado para apontar para refs/heads/feature-login. Vamos criar um novo commit:

$ echo "Login Form UI" > login.html
$ git add login.html
$ git commit -m "Add login page"
[feature-login a7c8e9d] Add login page
 1 file changed, 1 insertion(+)
 create mode 100644 login.html

Vamos conferir agora o conteúdo das referências das nossos branches:

$ cat .git/refs/heads/feature-login
a7c8e9d8928424ef245eefb925b42d3851b2c129

$ cat .git/refs/heads/main
f24e93e25b90f46c6a7e0c4a45adbb474f8e6c7c

feature-login avançou para apontar para nosso novo commit a7c8e9d.... Já main continua apontando para f24e93e.... Se conferirmos o histórico usando /git-log:

$ git log --oneline --graph --all
* a7c8e9d (HEAD -> feature-login) Add login page
* f24e93e (main) Initial commit

O grafo mostra que feature-login divergiu. O ponteiro do branch main permanece estático, enquanto feature-login avançou junto com HEAD. Se decidirmos mesclar esses branches mais tarde, o Git lerá esses ponteiros para calcular seus históricos. Saiba mais em /git-merge.


Criar, renomear e excluir branches

O comando git branch não serve apenas para criar referências; ele é uma ferramenta versátil para gerenciar, consultar e auditar o grafo de branches.

1. Listar e filtrar branches

Ao gerenciar projetos grandes, você frequentemente terá dezenas de branches locais e remotas. O Git oferece filtros para manter tudo organizado.

  • Listar todas os branches (locais e remotas):
    $ git branch -a
    Isso mostra os branches locais em uma cor (por exemplo, verde) e os branches de acompanhamento remoto (armazenadas em .git/refs/remotes/) em outra (por exemplo, vermelho).
  • Filtrar pelo status de mesclagem: Antes de limpar branches antigas, você precisa saber quais branches foram mescladas com segurança à branch atual:
    $ git branch --merged
    Para ver branches que contêm trabalho ainda não mesclado à branch atual:
    $ git branch --no-merged
    Executar git branch -d em qualquer branch da lista --no-merged falhará, a menos que seja forçado.
  • Listagem detalhada com acompanhamento do upstream:
    $ git branch -vv
    Isso mostra o hash do commit mais recente, a linha de assunto do commit e o status de acompanhamento do upstream (por exemplo, se seu branch local está à frente ou atrás do branch remoto correspondente).

2. Detalhes da exclusão e coleta de lixo

Excluir um branch é uma tarefa comum, mas seu funcionamento depende das flags usadas.

  • Exclusão segura (-d ou --delete):
    $ git branch -d feature-login
    O Git impedirá a exclusão se o branch contiver commits que não sejam alcançáveis a partir do branch atual ou do branch upstream. Isso evita a perda acidental de trabalho.
  • Exclusão forçada (-D ou -d --force):
    $ git branch -D feature-login
    Isso diz ao Git: “Exclua a referência, independentemente de qualquer coisa.”

O que acontece com os commits?

Excluir um branch remove a ref dela; isso não apaga os objetos de commit imediatamente. Se nenhuma outra ref alcançar esses commits, eles ficam inacessíveis e podem continuar recuperáveis pelo reflog por um tempo. O Git pode eventualmente remover objetos inacessíveis, e o momento depende da configuração e do estado do repositório. Se o trabalho for importante, crie outra branch antes da limpeza; veja Git objects para entender o ciclo de vida dos objetos.

3. Acompanhar branches upstream

Ao colaborar, suos branches locais acompanham branches em um servidor remoto (como o GitHub). Essa configuração de acompanhamento é armazenada em .git/config.

Para definir um branch upstream para o branch ativo:

$ git branch -u origin/feature-login
# Or:
$ git branch --set-upstream-to=origin/feature-login

Isso modifica o arquivo .git/config para incluir metadados de acompanhamento:

[branch "feature-login"]
    remote = origin
    merge = refs/heads/feature-login

Essa configuração permite simplesmente digitar git pull ou git push sem especificar o remoto e o nome do branch.


Recuperar um branch excluído por acidente

Imagine que você está limpando seu diretório de trabalho. Você acha que mesclou seu branch feature-payment em main, então executa:

$ git branch -D feature-payment
Deleted branch feature-payment (was c3b91a2).

De repente, você percebe que feature-payment não foi mesclado e que acabou de excluir dias de trabalho! Como os arquivos não estão mais visíveis no editor, parece um desastre.

Como recuperar

Como os branches do Git são apenas referências, os commits ainda estão no banco de dados. Você só precisa encontrar o hash do commit e anexar a ele uma nova referência de branch.

  1. Pesquisar o reflog: O reflog registra todas as vezes que HEAD muda de posição. Execute:
    $ git reflog
    f24e93e HEAD@{0}: checkout: moving from feature-payment to main
    c3b91a2 HEAD@{1}: commit: Add Stripe Integration and webhooks
    f24e93e HEAD@{2}: checkout: moving from main to feature-payment
    Observando com atenção, vemos que c3b91a2 era o commit feito no branch feature-payment antes de voltar para main.
  2. Recriar o ponteiro do branch: Agora, execute git branch seguido do novo nome do branch e do hash recuperado:
    $ git branch feature-payment c3b91a2
    Pronto! O Git cria um novo arquivo de referência em .git/refs/heads/feature-payment contendo c3b91a2, restaurando seu trabalho instantaneamente.

Onde os comandos de branch dão errado

1. Achar que git branch troca para o novo branch

Executar git branch new-feature apenas cria o ponteiro. Isso não atualiza HEAD. Se você começar a editar arquivos imediatamente, fará commits no branch antigo (por exemplo, main). Lembre-se sempre de executar git checkout new-feature (ou usar git checkout -b new-feature para criar e trocar em uma única etapa).

2. Colisões de namespace (arquivo versus diretório)

Como o Git armazena as refs dos branches como arquivos em .git/refs/heads/, elas precisam seguir as regras de caminho de arquivo do seu sistema operacional.

  • Se você tiver um branch chamado feature, o Git criará um arquivo chamado .git/refs/heads/feature.
  • Se depois você tentar criar um branch chamado feature/login, o Git precisará criar um diretório chamado feature e colocar dentro dele um arquivo login.
  • Na maioria dos sistemas, não é possível ter um arquivo e um diretório com exatamente o mesmo nome na mesma pasta pai. Isso resulta no seguinte erro: error: cannot lock ref 'refs/heads/feature/login': 'refs/heads/feature' exists; cannot create 'refs/heads/feature/login'
  • Correção: evite criar um branch genérico chamado feature se pretende usar namespaces com barra, como feature/login e feature/payment.

3. Referências locais obsoletas para branches remotos excluídos

Quando um colega exclui um branch no servidor remoto, o Git local ainda mantém o registro dela em .git/refs/remotes/origin/. Isso pode causar confusão.

  • Correção: limpe branches de acompanhamento remoto obsoletas executando:
    $ git fetch --prune

Comandos de branch em resumo

ComandoDescriçãoAção interna do Git
git branchLista todas os branches locaisLê arquivos dentro de .git/refs/heads/
git branch <name>Cria uma nova referência de branchCria um arquivo de 41 bytes em .git/refs/heads/<name> (formato files padrão desempacotado)
git branch -d <name>Exclui com segurança um branch se ele tiver sido mescladoExclui o arquivo de ref; verifica a alcançabilidade primeiro
git branch -D <name>Exclui um branch à forçaExclui o arquivo de ref independentemente da alcançabilidade
git branch -m <new-name>Renomeia o branch atualRenomeia o arquivo de ref em .git/refs/heads/
git branch -aLista branches locais e remotasLê .git/refs/heads/ e .git/refs/remotes/
git branch -vvMostra o status de acompanhamento e os commitsCorrelaciona arquivos de ref locais com configurações upstream

Perguntas sobre branches, respondidas

O que significa “HEAD desanexado”?

O estado de “HEAD desanexado” ocorre quando HEAD aponta diretamente para um hash de commit específico, em vez de apontar para uma referência de branch nomeada. Por exemplo, .git/HEAD pode conter um ID de commit em vez de ref: refs/heads/main. Um commit feito nesse estado não pertence a nenhum branch; depois de mudar de branch, ele pode ficar inacessível a partir de refs nomeadas, embora o reflog possa ajudar a recuperá-lo. Para preservar o trabalho, crie um branch enquanto ainda estiver nesse commit com git switch -c rescue-work. Veja Git refs para entender como HEAD e as refs de branch se relacionam.

Dois branches podem apontar para o mesmo commit?

Sim. Vários branches podem apontar para exatamente o mesmo hash de commit. Na verdade, quando você cria um novo branch a partir de main, os dois branches apontam para o mesmo commit até que você crie um novo commit em uma delas. O Git não tem problema com várias refs contendo a mesma string SHA-1 de 40 caracteres.

Como enviar um branch local para um servidor remoto?

Para enviar seu branch a um repositório remoto e configurá-lo para acompanhar esse remoto, use o comando:

$ git push -u origin <branch-name>

A flag -u (ou --set-upstream) instrui o Git a criar uma associação de acompanhamento, atualizando seu arquivo .git/config para que futuros pushes e pulls nesse branch exijam apenas git push ou git pull.


Teste seu entendimento

Carregando quiz…