DevOps, CI/CD e contêineres
Fundamentos de Git: commits, working tree, remotos e como desfazer mudanças
Iniciante10 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
Fica muito mais fácil usar bem o Git quando você entende o modelo por trás dos comandos: um grafo de snapshots imutáveis, três lugares onde uma mudança pode estar e branches que não passam de ponteiros móveis. Este guia constrói esse modelo e depois cobre o fluxo de trabalho diário — clone, add, commit, fetch, pull, push — e as diferentes formas de desfazer as coisas, para você se recuperar de erros sem medo.
O que você vai aprender
- Explicar como o Git guarda o histórico como um grafo de snapshots endereçados por conteúdo
- Mover mudanças de forma deliberada entre o working tree, o índice e o repositório
- Sincronizar com um remoto usando fetch, pull e push e entender o que eles informam
- Desfazer erros com restore, reset, revert e o reflog, escolhendo sempre a opção segura
- Configurar o Git, ignorar arquivos corretamente e escrever mensagens de commit que ajudem quem vai ler depois
A maioria dos tutoriais de Git ensina uma sequência de comandos para decorar. Isso funciona até algo dar errado — e, com Git, uma hora algo sempre dá errado: um commit na branch errada, um pull que gerou um merge inesperado, um reset que parece ter engolido uma tarde de trabalho. A saída é entender o pequeno conjunto de ideias sobre o qual os comandos são construídos. O modelo do Git é mais simples do que a linha de comando dá a entender, e, quando você consegue visualizar o que um comando faz com o grafo de commits, o manual de referência passa a fazer sentido e o pânico vai embora. Este guia é voltado ao Git 2.4x e usa os comandos modernos switch e restore onde materiais mais antigos usavam checkout para tudo.
O que o Git armazena
Um repositório Git é um banco de dados com quatro tipos de objeto, cada um identificado pelo hash SHA-1 (ou SHA-256, em repositórios mais novos) do seu conteúdo:
- Um blob é o conteúdo de um arquivo, sem nome e sem metadados.
- Um tree é a listagem de um diretório: nomes, permissões e os hashes dos blobs e sub-trees que ele contém.
- Um commit aponta para um tree (o snapshot completo do projeto naquele momento) e para zero ou mais commits pais, e carrega o autor, o committer, os timestamps e a mensagem.
- Um objeto tag associa a um commit um nome, uma mensagem e, opcionalmente, uma assinatura.
Daí decorrem duas consequências. Primeiro, um commit é um snapshot, não um diff: o Git armazena a árvore inteira a cada vez, deduplicada por hash, de modo que arquivos inalterados não custam nada e dá para fazer checkout de qualquer commit sem reprocessar o histórico. Os diffs são calculados sob demanda. Segundo, tudo é imutável e endereçado por conteúdo: mude um único byte em qualquer ponto do histórico e todos os hashes acima dele mudam. É por isso que "reescrever o histórico" significa criar commits novos, e é por isso que um hash é um identificador confiável de exatamente um estado do código.
Os commits, ligados aos seus pais, formam um grafo acíclico dirigido. Uma branch não passa de um arquivo contendo o hash de um commit — um ponteiro móvel que avança quando você faz commit nela. O HEAD é um ponteiro para a branch em que você está no momento. Criar uma branch é escrever 41 bytes em um arquivo, e é por isso que a operação é instantânea e que quem usa Git cria branches com muito mais liberdade do que quem usa sistemas mais antigos.
As três áreas
Uma mudança passa por três lugares, e boa parte da confusão com o Git vem de não saber qual deles um comando afeta.
| Área | O que é | Comandos que escrevem nela |
|---|---|---|
| Working tree | Os arquivos em disco que você edita | editor, git restore, git switch, git merge |
| Índice (staging area) | O próximo commit proposto, guardado em .git/index | git add, git rm, git restore --staged, git reset |
| Repositório | O banco de objetos e as refs dentro de .git | git commit, git fetch, git merge, git reset |
O índice é a peça que mais causa estranheza a quem está começando. Ele existe para você montar um commit de forma deliberada: você edita cinco arquivos, prepara os três que fazem sentido juntos, faz o commit e depois prepara os outros dois em um commit separado. O git add -p vai além e prepara hunks individuais dentro de um arquivo. O git status sempre mostra tanto o que está preparado quanto o que foi modificado e não está; o git diff compara o working tree com o índice, e o git diff --staged compara o índice com o último commit.
git status # what is staged, modified, untracked
git add src/Order.java # stage one file
git add -p # stage hunks interactively
git diff --staged # review exactly what the commit will contain
git commit -m "Reject orders with a negative quantity"
O fluxo de trabalho diário
Partindo de um projeto existente:
git clone [email protected]:acme/shop.git
cd shop
git switch -c fix-negative-quantity # create and switch to a branch
# ... edit, test ...
git add -A
git commit
git push -u origin fix-negative-quantity # -u sets the upstream so later pushes need no arguments
O clone copia o repositório inteiro — todo o histórico, todas as branches — e configura um remoto chamado origin apontando para a origem. Para cada branch do remoto você ganha uma branch de rastreamento remoto: origin/main, origin/release-2.4 e assim por diante. Elas são marcadores somente leitura de como o remoto estava na última vez que você se comunicou com ele; você nunca faz commit diretamente nelas.
O git fetch atualiza as branches de rastreamento remoto e baixa os objetos novos, sem alterar nada nas suas próprias branches. O git pull é um fetch seguido da integração de origin/<branch> na sua branch atual, por merge ou rebase, conforme a configuração. Como um pull pode gerar um commit de merge que você não queria, muitos desenvolvedores configuram git config --global pull.rebase true, para que os commits locais sejam reaplicados em cima dos que foram baixados e o histórico continue linear; o guia de fluxos de trabalho com branches discute esse trade-off.
O git push envia seus commits para o remoto e faz a branch de lá avançar. Ele é recusado se a branch remota tiver commits que você não tem — um non-fast-forward —, o que impede você de sobrescrever o trabalho de outra pessoa. A resposta correta é fazer fetch, integrar e fazer push de novo; reserve o --force para branches em que só você trabalha e, mesmo assim, prefira --force-with-lease, que falha se o remoto tiver mudado desde o seu último fetch.
Lendo o histórico
git log --oneline --graph --decorate -20 # compact graph with branch and tag labels
git log -p -- src/Order.java # every change to one file, with diffs
git log --author=alice --since=2.weeks
git show a1b2c3d # one commit: metadata plus diff
git blame src/Order.java # which commit last touched each line
git diff main...feature # what feature adds relative to the merge base
Vale a pena aprender a forma com três pontos da última linha: A..B significa "commits alcançáveis a partir de B, mas não de A", enquanto A...B no git diff significa "mudanças em B desde que ela divergiu de A", que é exatamente o que você quer ao revisar uma branch.
Desfazendo com segurança
O Git oferece várias formas de desfazer, e a escolha certa depende de duas perguntas: a mudança já foi commitada? E já foi compartilhada?
Descartar edições não commitadas no working tree. git restore <file> substitui o arquivo pela versão do índice. A mudança se perde de vez — ela nunca foi commitada —, então confira o git diff antes.
Tirar do índice sem perder as edições. git restore --staged <file> copia de volta para o índice a versão do último commit e não mexe no working tree.
Corrigir o último commit. git commit --amend substitui o commit da ponta por um novo contendo o índice atual (e uma mensagem possivelmente editada). Tudo bem antes do push; depois do push, isso reescreve histórico compartilhado.
Mover uma branch para trás. git reset move o ponteiro da branch atual para outro commit. Com --soft, o índice e o working tree mantêm as mudanças posteriores (útil para juntar os últimos commits em um só); com --mixed, o padrão, o índice é resetado, mas os arquivos ficam intactos; com --hard, tudo passa a corresponder ao commit de destino e o trabalho não commitado é destruído.
git reset --soft HEAD~3 # collapse the last three commits into staged changes
git commit -m "Implement discount codes"
Desfazer um commit compartilhado. git revert <hash> cria um commit novo que aplica a mudança inversa. O histórico é preservado, nada do que outras pessoas baixaram fica inválido, e essa é a única forma correta de desfazer algo na main depois do push.
Recuperar commits "perdidos". Toda vez que o HEAD se move, o Git registra onde ele estava no reflog. Depois de um reset malfeito ou de uma branch apagada, git reflog lista as posições recentes, e git reset --hard HEAD@{2} ou git switch -c rescued <hash> traz os commits de volta. Objetos inalcançáveis são mantidos por pelo menos 30 dias por padrão, então quase tudo o que foi commitado pode ser recuperado dentro desse prazo.
Stash
O git stash salva as mudanças do working tree e do índice em uma pilha e limpa o working tree, para você trocar de branch, corrigir algo urgente e voltar depois. O git stash pop restaura o stash mais recente e o descarta; o git stash list mostra o que está guardado; git stash push -m "wip: pricing" -- src/pricing guarda só alguns caminhos, com um rótulo. Stashes também são commits, e um pop que gera conflito mantém tanto o stash quanto os marcadores de conflito, então nada se perde.
Ignorando arquivos e configurando o Git
Um arquivo .gitignore lista padrões de arquivos que o Git não deve rastrear — saída de build, diretórios de dependências, configurações de editor, segredos. Os padrões são avaliados em relação ao diretório do próprio arquivo; uma / no início ancora o padrão nesse diretório, uma / no final casa apenas com diretórios, e ! nega o padrão.
# Build output
/target/
/build/
node_modules/
# IDE and OS noise
.idea/
*.iml
.vscode/
.DS_Store
# Never commit secrets
.env
*.pem
Duas coisas costumam pegar as pessoas de surpresa. O .gitignore não afeta arquivos que já são rastreados — primeiro é preciso tirá-los com git rm --cached. E uma entrada de ignore não é um controle de segurança: um arquivo commitado uma única vez fica no histórico até o histórico ser reescrito, e qualquer segredo que ele continha precisa ser trocado.
A configuração existe em três níveis — sistema, global (~/.gitconfig) e repositório (.git/config) —, cada um sobrepondo o anterior. As configurações que vale aplicar em toda máquina nova:
git config --global user.name "Alice Martin"
git config --global user.email [email protected]
git config --global init.defaultBranch main
git config --global pull.rebase true
git config --global push.autoSetupRemote true # first push needs no -u
git config --global core.autocrlf input # on Linux/macOS; "true" on Windows
git config --global rerere.enabled true # remember how you resolved conflicts
Tags
Uma tag marca um commit como importante, geralmente uma release. Tags leves (git tag v2.4.0) são simples ponteiros; tags anotadas (git tag -a v2.4.0 -m "Release 2.4.0") são objetos com autor, data e mensagem, e podem ser assinadas. Use tags anotadas para releases: elas registram a procedência e o git describe dá preferência a elas. Tags não são enviadas por padrão — use git push origin v2.4.0 ou git push --tags — e, ao contrário das branches, foram feitas para ser permanentes: mover uma tag que outras pessoas já baixaram causa confusão duradoura.
Escrevendo commits que valem a leitura
Um commit é uma unidade de mudança e uma mensagem para quem vai ler o histórico um ano depois, muitas vezes no meio de um bisect atrás de um bug. Mantenha cada commit restrito a uma única mudança lógica, para que ele possa ser revertido ou aplicado com cherry-pick isoladamente. Escreva a linha de assunto no imperativo ("Add retry to payment client"), com cerca de 50 caracteres no máximo, e coloque o porquê no corpo: qual problema existia, por que essa abordagem, quais alternativas foram descartadas. O diff já mostra o que mudou; a mensagem existe para explicar o que o diff não consegue.
O git bisect é a recompensa por commits pequenos e bem descritos. A partir de um commit sabidamente bom e de outro sabidamente ruim, ele faz checkout do ponto intermediário, você (ou um script) informa se está bom ou ruim, e ele vai cercando o commit culpado em um número logarítmico de passos. Só é útil quando todo commit compila e faz uma única coisa.
Erros comuns
- Commitar arquivos gerados e dependências.
target/,node_modules/e arquivos.classincham o repositório e causam conflitos sem fim. Ignore-os desde o primeiro commit. git add .no diretório errado. Ele prepara tudo abaixo do diretório atual, inclusive arquivos que você não pretendia incluir. Confira ogit statusantes de commitar.- Fazer force push em uma branch compartilhada. Os pushes dos colegas são recusados, as branches locais deles divergem e alguém acaba reenviando o histórico antigo. Use
--force-with-leasee apenas em branches pessoais. - Fazer pull com mudanças não commitadas. Um merge ou rebase sobre arquivos modificados gera conflitos confusos. Faça commit ou stash antes.
- Tratar
reset --hardcomo resposta para tudo. Ele destrói o trabalho não commitado; use stash no lugar e o reflog para voltar a commits. - Commitar segredos "temporariamente". Eles ficam no histórico. Troque a credencial imediatamente e use
git filter-repose for preciso limpar o histórico. - Commits gigantes. Um commit de 60 arquivos com o título "changes" é impossível de revisar, reverter ou usar no bisect. Prepare as mudanças em partes lógicas.
Principais pontos
- O Git armazena snapshots imutáveis, endereçados por conteúdo e ligados em um grafo; branches são ponteiros móveis dentro desse grafo e o
HEADmarca onde você está. - As mudanças ficam no working tree, no índice ou no repositório;
status,diffediff --stageddizem em qual, e oaddmonta o próximo commit de forma deliberada. - O
fetchatualiza as branches de rastreamento remoto sem mexer no seu trabalho; opulltambém integra; opushse recusa a sobrescrever commits que você não viu. - Desfaça trabalho não commitado com
restore, reorganize commits locais comreseteamend, desfaça commits compartilhados comreverte recupere-se de quase tudo com oreflog. - Mantenha os arquivos gerados fora do repositório com
.gitignore, configurepull.rebasee as opções de fim de linha globalmente e use tags anotadas para releases. - Commits pequenos, com um único propósito e mensagens que explicam o motivo, são o que torna o histórico, o bisect e a revisão de código úteis mais tarde.
Fontes e leituras complementares
Perguntas frequentes
- Qual é a diferença entre git fetch e git pull?
- O fetch baixa os commits novos e atualiza as branches de rastreamento remoto (como origin/main) sem mexer na sua branch nem no seu working tree. O pull é um fetch seguido de merge ou rebase na sua branch atual. Faça fetch primeiro quando quiser examinar o que mudou antes de integrar; use pull quando estiver pronto para integrar direto.
- Fiz commit na branch errada. Como corrijo?
- Se você ainda não fez push, anote o hash do commit pelo git log, mude para a branch certa e rode git cherry-pick com esse hash; depois volte para a branch errada e rode git reset --hard HEAD~1 para remover o commit de lá. Se a branch errada já foi enviada, prefira git revert para não reescrever um histórico que outras pessoas talvez já tenham baixado.
- git reset --hard é perigoso?
- Ele descarta de forma permanente as mudanças não commitadas do working tree e do índice — elas nunca estiveram em um commit, então nada consegue recuperá-las. Os commits que o reset deixa para trás não são apagados na hora: o git reflog continua listando-os por pelo menos 30 dias por padrão, e você pode voltar para eles com outro reset. Antes de um reset --hard, rode git status e guarde com git stash tudo o que possa querer manter.
- Por que o Git diz 'detached HEAD'?
- Normalmente o HEAD aponta para o nome de uma branch, e cada commit faz a branch avançar. Se você fizer checkout de um commit ou de uma tag específica, o HEAD passa a apontar diretamente para esse commit. Os commits feitos nesse estado não pertencem a nenhuma branch e acabam removidos pelo garbage collector, a menos que você rode git switch -c alguma-branch para criar uma naquele ponto.