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:
- tree: um ponteiro (hash) para o objeto tree de nível superior (raiz), que representa o diretório raiz do repositório.
- 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.
- author: o nome, o e-mail e o timestamp Unix da pessoa que escreveu o código.
- committer: o nome, o e-mail e o timestamp Unix da pessoa que fez o commit (por exemplo, quando um mantenedor aplicou um patch).
- gpgsig: assinatura criptográfica opcional, caso o commit tenha sido assinado.
- 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.jsacima (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 addantes de executargit 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 rebaseou 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
| Comando | Ação |
|---|---|
git commit | Abre 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 --amend | Altera o último commit (adiciona arquivos novos preparados e/ou atualiza a mensagem). |
git commit --amend --no-edit | Altera 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…