Nesta página

git commit

Em resumo: git commit captura um instantâneo da sua Área de Staging (índice) e o grava permanentemente no banco de objetos do Git. Internamente, o Git empacota a estrutura de diretórios em objetos tree, grava um objeto commit com os metadados (autor, committer, hashes dos commits pai, mensagem) e atualiza a ref do branch ativo para apontar para o hash do novo commit.


O que git commit realmente faz?

Para entender git commit, você precisa abandonar o equívoco comum de que o Git acompanha alterações como “diffs” ou “deltas”. No Git, cada commit é um instantâneo completo de todo o projeto.

Quando você faz um commit, não está salvando uma lista de edições de linhas. Em vez disso, está tirando uma foto da área de staging e atribuindo a ela um identificador único e permanente. Se um arquivo não mudou em um commit, o Git não o duplica; ele simplesmente aponta para o objeto blob existente de um commit anterior. Isso é conhecido como compartilhamento estrutural.

graph TD
    subgraph "A Git Commit Object"
        C[Commit Object<br>Hash: 8a4c10...] -->|Points to| T[Root Tree Object<br>Hash: 4a2b99...]
        C -->|Points to| P[Parent Commit Hash<br>Hash: 55a420...]
        C -.->|Metadata| M[Author, Committer, Timestamp, Message]
    end

A estrutura de um objeto commit

Internamente, um commit é representado por um arquivo simples de texto sem formatação que contém metadados estruturados. Quando o Git calcula o hash desse texto usando SHA-1, ele gera seu identificador hexadecimal de 40 caracteres.

Um objeto commit bruto contém os seguintes campos:

  1. tree: um ponteiro (hash) para o objeto tree de nível superior (raiz), que representa o diretório raiz do repositório.
  2. parent: o hash do commit anterior (ou dos commits anteriores).
    • Um commit inicial não tem pais.
    • Um commit comum tem um pai.
    • Um commit de merge tem dois ou mais pais.
  3. author: o nome, o e-mail e o timestamp Unix da pessoa que escreveu o código.
  4. committer: o nome, o e-mail e o timestamp Unix da pessoa que fez o commit (por exemplo, quando um mantenedor aplicou um patch).
  5. gpgsig: assinatura criptográfica opcional, caso o commit tenha sido assinado.
  6. message: a descrição textual que explica por que as alterações foram feitas.

O que acontece dentro de .git/ (exploração prática)

Vamos olhar por baixo do capô de um repositório para acompanhar, passo a passo, como o Git constrói esses objetos tree e commit.

Etapa 1: preparar um arquivo e verificar o índice

Primeiro, vamos garantir que temos um repositório inicializado e um arquivo preparado usando git add.

$ mkdir git-commit-demo && cd git-commit-demo
$ git init
Initialized empty Git repository in /git-commit-demo/.git/

$ echo "console.log('hello world');" > app.js
$ git add app.js

Neste ponto, o arquivo é compactado em um objeto blob e registrado no arquivo .git/index. Vamos verificar:

$ git ls-files --stage
100644 6be02374db118b9fb99cd98c6b403e5a558d0d57 0	app.js

O hash de 40 caracteres 6be02374db118b9fb99cd98c6b403e5a558d0d57 é o ID SHA-1 do Git para um blob que contém console.log('hello world'); (27 bytes mais a nova linha escrita por echo, totalizando 28 bytes). Se seu editor ou shell escrever a linha de outra forma, o hash do blob será diferente — no endereçamento por conteúdo, o hash é calculado a partir dos bytes exatos do arquivo.

Uma observação sobre os hashes desta lição: o hash do blob app.js acima (6be02374…) é exato e reproduzível, porque um blob depende apenas do conteúdo do arquivo. Os hashes de commits e trees incorporam os timestamps e o fuso horário do autor/committer; por isso, os hashes abreviados de commits mostrados nas saídas abaixo (por exemplo, d4a123f) são representativos. Executar exatamente estes comandos em um repositório novo produzirá a mesma estrutura, mas IDs de commit e tree diferentes.

Etapa 2: criar um commit

Agora, vamos gravar nosso primeiro commit.

$ git commit -m "feat: introduce logging script"
[main (root-commit) d4a123f] feat: introduce logging script
 1 file changed, 1 insertion(+)
 create mode 100644 app.js

Vamos verificar as referências do branch dentro de .git/refs/heads/. O arquivo do branch ativo main agora deve conter o hash completo de 40 caracteres do commit:

$ cat .git/refs/heads/main
d4a123f4c7b8d80c0ef14815a5bb1d6a362bf2c9

E .git/HEAD informa ao Git qual branch está ativo:

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

Etapa 3: desmontar o objeto commit

Vamos inspecionar o objeto commit criado usando o comando de baixo nível git cat-file:

# Print type of the object
$ git cat-file -t d4a123f4c7b8d80c0ef14815a5bb1d6a362bf2c9
commit

# Print raw content of the commit object
$ git cat-file -p d4a123f4c7b8d80c0ef14815a5bb1d6a362bf2c9
tree b8a27d0ef093ab7e20cf7b68a417534431e21b7b
author Purna Pattela <purna@example.com> 1772960000 +0000
committer Purna Pattela <purna@example.com> 1772960000 +0000

feat: introduce logging script

Observe que o objeto commit não contém conteúdo de arquivo nem diff. Ele aponta para um objeto tree com o hash b8a27d0.... Vamos inspecionar essa tree:

$ git cat-file -p b8a27d0ef093ab7e20cf7b68a417534431e21b7b
100644 blob 6be02374db118b9fb99cd98c6b403e5a558d0d57	app.js

O objeto tree funciona como uma listagem de diretório. Ele associa o nome do arquivo app.js às permissões do arquivo (100644) e ao blob de conteúdo compactado com zlib (6be0237...).

Etapa 4: adicionar subdiretórios e verificar os pais

Vamos adicionar um subdiretório para ver como o Git lida com pastas aninhadas.

$ mkdir src
$ echo "const port = 3000;" > src/config.js
$ git add src/config.js
$ git commit -m "chore: add source config"
[main 7f2b58c] chore: add source config
 1 file changed, 1 insertion(+)
 create mode 100644 src/config.js

Vamos ler este segundo commit:

$ git cat-file -p 7f2b58c
tree a832dbf521b4a56c071d2b512c98a3e74211a512
parent d4a123f4c7b8d80c0ef14815a5bb1d6a362bf2c9
author Purna Pattela <purna@example.com> 1772960100 +0000
committer Purna Pattela <purna@example.com> 1772960100 +0000

chore: add source config

Observe a nova linha: parent d4a123f.... Ela aponta de volta para o primeiro commit, conectando os dois em uma cadeia histórica.

Vamos examinar a tree deste novo commit:

$ git cat-file -p a832dbf521b4a56c071d2b512c98a3e74211a512
100644 blob 6be02374db118b9fb99cd98c6b403e5a558d0d57	app.js
040000 tree f3b3a3c9e6db5832a875a6c11d2b270a2936274b	src

Em vez de listar uma relação simples de arquivos, o objeto tree raiz aponta para outro objeto tree (que representa o diretório src/, indicado pelo modo 040000). Se espiarmos dentro da tree src aninhada:

$ git cat-file -p f3b3a3c9e6db5832a875a6c11d2b270a2936274b
100644 blob c8384df24a5611dfd9a1db4b86270a1a8a25c156	config.js

Essa arquitetura de tree dentro de tree reproduz perfeitamente seu sistema de arquivos.


Opções detalhadas de comandos e uso avançado

1. Alterar o último commit (git commit --amend)

Se você cometer um erro de digitação na mensagem do commit ou esquecer de preparar uma alteração antes de fazer o commit, poderá alterar o último commit:

$ git add forgotten_file.txt
$ git commit --amend -m "chore: add source config and settings"

Por baixo do capô: como os commits no Git são imutáveis (o hash SHA-1 é calculado a partir do conteúdo), o Git não pode editar o commit existente. Em vez disso, --amend cria um objeto commit totalmente novo que aponta para uma nova tree e uma mensagem modificada, definindo como pai o pai do commit antigo. Em seguida, o Git atualiza o ponteiro do branch para o novo commit e deixa o antigo órfão.

graph TD
    subgraph "How Amend Works"
        C1[Commit 1] --> C2[Commit 2: Original]
        C1 --> C3[Commit 3: Amended]
        Branch[Branch Ref: main] -->|Moved from C2 to C3| C3
    end
    style C2 fill:#faa,stroke:#333
    style C3 fill:#afa,stroke:#333

2. Ignorar a área de staging (git commit -a)

Você pode ignorar o staging usando a flag -a (ou --all). Isso instrui o Git a preparar automaticamente todos os arquivos modificados e excluídos e, em seguida, gravar um commit imediatamente:

$ git commit -am "refactor: simplify logger interface"

[!WARNING] Este comando não prepara arquivos novos ainda não rastreados. Se você criar um arquivo totalmente novo, deverá executar git add antes de executar git commit -a.

3. Assinar commits com GPG ou SSH (git commit -S)

Em ambientes de produção, é fundamental verificar se os commits realmente vêm do autor indicado. Você pode assinar seus commits criptograficamente:

$ git commit -S -m "feat: release critical payment gateway integration"

Isso assina o commit usando uma chave privada local. Depois, o GitHub ou o GitLab pode exibir um selo “Verificado” ao lado do commit, comparando a assinatura com sua chave pública.


Cenário do mundo real: escrevendo commits atômicos

Uma base de código saudável depende de commits atômicos. Um commit atômico faz uma, e somente uma, coisa lógica. Ele deve conter todas as alterações necessárias para implementar um único recurso ou corrigir um único bug, e nada além disso.

Por exemplo, se você estiver criando um sistema de autenticação, não escreva uma mensagem de commit como implemented login and fixed layout issues on homepage. Se a correção do layout da página inicial quebrar outra coisa, reverter esse commit também apagará o sistema de login!

Em vez disso, divida o trabalho usando git add e commits separados:

# First, stage only the layout changes
$ git add src/styles.css
$ git commit -m "fix(styles): repair home page navigation alignment"

# Next, stage the login files
$ git add src/auth/
$ git commit -m "feat(auth): implement user authentication mechanism"

Essa estrutura organizada facilita a auditoria do histórico usando git log e permite que os desenvolvedores isolem problemas com segurança.


Armadilhas e erros comuns

1. Alterar commits já enviados

Nunca execute git commit --amend em um commit que já tenha sido enviado para um repositório remoto (como o GitHub) compartilhado com outros desenvolvedores.

  • Por quê: alterar o commit muda seu hash. Se você enviar esse commit alterado com push forçado, substituirá o histórico de todas as outras pessoas, provocando conflitos de merge difíceis quando elas fizerem pull.
  • Regra prática: altere apenas commits que existam exclusivamente no seu computador local.

2. O erro de commit vazio

Se você tentar executar git commit quando não houver alterações preparadas, o Git rejeitará o comando:

$ git commit -m "nothing changes"
On branch main
nothing to commit, working tree clean
  • Correção: se você realmente precisar criar um commit sem alterações em arquivos (por exemplo, para acionar um executor de build de CI/CD), use a flag de substituição:
$ git commit --allow-empty -m "trigger: run test suite in pipeline"

3. Confusão entre autor e committer

Às vezes, nos históricos de projetos, você verá um commit que lista uma pessoa como Author e outra como Committer.

  • Autor: a pessoa que escreveu as alterações de código de fato.
  • Committer: a pessoa que aplicou o commit (por exemplo, usando git rebase ou aplicando um patch).
  • Normalmente, são a mesma pessoa. Porém, se você fizer rebase ou squash do pull request de outra pessoa no GitHub, será o Committer, enquanto ela continuará sendo a Author.

Referência rápida de comandos

ComandoAção
git commitAbre o editor do terminal para escrever uma mensagem de commit com várias linhas.
git commit -m "msg"Faz o commit das alterações preparadas com uma mensagem curta na linha de comando.
git commit -a -m "msg"Prepara arquivos modificados/excluídos e faz o commit em uma única etapa.
git commit --amendAltera o último commit (adiciona arquivos novos preparados e/ou atualiza a mensagem).
git commit --amend --no-editAltera o último commit com arquivos novos, mas mantém exatamente a mesma mensagem.
git commit --allow-empty -m "msg"Força um commit mesmo quando nenhuma alteração está preparada ou modificada.
git commit -S -m "msg"Assina o commit criptograficamente.

Perguntas frequentes (FAQ)

Qual é a diferença entre git commit e git push?

git commit é um comando local. Ele registra um instantâneo no banco de diretórios local (.git/objects/). Não entra em contato com servidores externos. git push é um comando de rede que envia esses commits, trees e blobs locais para um repositório remoto (como GitHub, GitLab ou um servidor próprio), para que possam ser compartilhados com outras pessoas.

Como o Git calcula o hash de commit de 40 caracteres?

O Git calcula o hash do cabeçalho e do conteúdo do objeto commit usando SHA-1. O conteúdo é composto pelo hash da tree, pelo(s) hash(es) dos pais, pelas informações do autor, pelas informações do committer e pela mensagem do commit. Como o cálculo do hash inclui o timestamp atual, dois commits criados com exatamente os mesmos arquivos e a mesma mensagem ainda terão hashes diferentes.

Posso reverter ou excluir meu último commit?

Sim. Se você fez um commit, mas quer desfazê-lo preservando suas alterações no diretório de trabalho (removendo-as do staging), execute:

git reset --soft HEAD~1

Se quiser apagar completamente o último commit e descartar todas as modificações, execute:

git reset --hard HEAD~1

(Aviso: git reset --hard não pode ser desfeito facilmente; verifique seu trabalho antes de executá-lo!)

O que é um estado HEAD desanexado?

Se você fizer checkout de um hash de commit específico (por exemplo, git checkout a1b2c3d - link git-checkout) em vez de um branch, seu ponteiro HEAD apontará diretamente para um objeto commit, e não para uma referência de branch. Se você fizer um commit nesse estado, nenhum branch apontará para ele. Se trocar de branch, seu novo commit ficará órfão e acabará sendo excluído pela coleta de lixo do Git. Antes de fazer commit, confirme sempre que você está em um branch (link git-branch).


Teste seus conhecimentos

Carregando quiz…