Saber usar git add, git commit e git push é o mínimo. Em um projeto real com múltiplos desenvolvedores, você vai lidar com conflitos de merge, branches paralelas, histórico bagunçado, releases mal organizadas e deploys que quebraram tudo.
Git é uma ferramenta poderosa — e como toda ferramenta poderosa, pode fazer muito bem ou muito mal dependendo de como é usada. Este artigo ensina o Git que você realmente vai usar no dia a dia profissional.
Revisão — os comandos fundamentais
# Configuração inicial (uma vez por máquina)
git config --global user.name "Seu Nome"
git config --global user.email "seu@email.com"
git config --global core.editor "code --wait" # VS Code como editor padrão
git config --global init.defaultBranch main
# Iniciando
git init # novo repositório
git clone URL # clona repositório existente
git clone URL nome-da-pasta # clona com nome customizado
# Ciclo básico
git status # estado atual
git add arquivo.js # adiciona arquivo específico
git add . # adiciona tudo
git add -p # adiciona interativamente (por partes)
git commit -m "mensagem" # commita
git commit --amend # edita o último commit
# Remoto
git remote add origin URL # conecta ao repositório remoto
git push origin main # envia para o remoto
git pull origin main # busca e mescla
git fetch origin # busca sem mesclar
Branches — trabalhando em paralelo
# Criar e navegar
git branch # lista branches locais
git branch -a # lista branches locais e remotas
git branch nome-da-branch # cria branch
git checkout nome-da-branch # muda para branch
git checkout -b nome-da-branch # cria e muda em um comando
git switch nome-da-branch # alternativa moderna ao checkout
git switch -c nome-da-branch # cria e muda (moderno)
# Remover
git branch -d nome # remove branch local (seguro)
git branch -D nome # remove branch local (forçado)
git push origin --delete nome # remove branch remota
# Renomear
git branch -m nome-antigo nome-novo
# Ver branches e seus últimos commits
git branch -v
git branch --merged # branches já mergeadas em main
git branch --no-merged # branches ainda não mergeadas
Merge vs Rebase — entendendo a diferença
Esta é uma das discussões mais importantes do Git:
# ── Situação: você está em feature e quer integrar main ──
# MERGE — preserva o histórico exato
# Cria um "merge commit" que une os dois históricos
git checkout feature
git merge main
# Histórico após merge (o merge commit nasce na branch ATUAL, feature):
# A---B---C (main, intacta)
# \ \
# D---E---M (feature)
# M = merge commit, com dois pais: E e C
#
# Repare que main não mudou. Quem recebeu o trabalho foi feature — o
# caminho inverso, levar feature para main, só acontece no merge do PR.
# REBASE — reescreve o histórico de forma linear
# Aplica seus commits em cima da branch de destino
git checkout feature
git rebase main
# Histórico após rebase:
# A---B---C (main)
#
# D'--E' (feature rebaseada — novos commits)
# D' e E' são os mesmos que D e E mas com novos hashes
Quando usar cada um:
Merge:
✅ Branches públicas e compartilhadas
✅ Quando o histórico exato importa
✅ Feature branches no merge final (PR)
✅ Mais seguro para iniciantes
Rebase:
✅ Atualizar branch local antes de abrir PR
✅ Manter histórico limpo e linear
✅ Branches privadas ainda não compartilhadas
❌ Nunca em branches públicas já compartilhadas
("golden rule of rebasing")
Estratégias de branching
Git Flow — para projetos com releases programadas
main ──────●────────────────────────●──────
| |
release/1.0 ──────●──────────● |
| | |
develop ──●───●───●───●───●───●───●───●──
| | |
feature/A ────●───●───┘ |
|
feature/B ────────────●───●───┘
|
hotfix/bug ────────────────────●──●──────────
# Branches no Git Flow:
# main → código em produção
# develop → integração de features
# feature/* → novas funcionalidades
# release/* → preparação de release
# hotfix/* → correções urgentes em produção
# Fluxo típico de feature:
git switch develop
git switch -c feature/autenticacao-jwt
# ... trabalho ...
git switch develop
git merge --no-ff feature/autenticacao-jwt # --no-ff preserva contexto
git branch -d feature/autenticacao-jwt
GitHub Flow — para deploys contínuos (mais simples)
main ──●──────────────────────●──────────────●──
/
feature ●──●──●──(PR)────── fix ●──●──(PR)
# Apenas duas regras:
# 1. main sempre está pronta para deploy
# 2. Todo trabalho acontece em branches criadas de main
git switch main
git pull origin main
git switch -c feature/nova-funcionalidade
# ... trabalho e commits ...
git push origin feature/nova-funcionalidade
# Abre Pull Request no GitHub
# Revisão → aprovação → merge → delete branch
Para a maioria dos projetos modernos, GitHub Flow é suficiente e mais simples.
Commits semânticos — mensagens que comunicam
Uma das práticas mais valiosas e menos adotadas:
# Formato: tipo(escopo): descrição curta
#
# tipo: o que foi feito
# escopo: onde foi feito (opcional)
# descrição: o quê, não o como
# Tipos principais:
feat → nova funcionalidade
fix → correção de bug
docs → apenas documentação
style → formatação (sem mudança de lógica)
refactor → refatoração (sem bug fix nem feature)
test → adicionando/corrigindo testes
chore → tarefas de manutenção (build, deps, config)
perf → melhoria de performance
ci → mudanças em CI/CD
# Exemplos reais:
git commit -m "feat(auth): adiciona autenticação JWT"
git commit -m "fix(usuarios): corrige validação de email duplicado"
git commit -m "docs: atualiza README com instruções de instalação"
git commit -m "refactor(tarefas): extrai lógica de paginação para service"
git commit -m "test(auth): adiciona testes de integração para login"
git commit -m "chore: atualiza dependências para versões mais recentes"
git commit -m "perf(consultas): adiciona índice ao campo email no MongoDB"
Com commits semânticos, o histórico vira uma documentação legível:
git log --oneline
# a3f8c21 feat(auth): adiciona autenticação JWT
# b2e1d90 test(auth): adiciona testes de integração para login
# c4d7f12 fix(usuarios): corrige validação de email duplicado
# d9a1b34 refactor(tarefas): extrai lógica de paginação
# e5f2c67 chore: atualiza mongoose para 7.6.0
# f8a3d45 feat(tarefas): adiciona filtro por prioridade
Commitizen — commits semânticos com assistente
npm install -D commitizen cz-conventional-changelog
# package.json
{
"scripts": {
"commit": "cz"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
}
}
# Em vez de git commit -m "..."
npm run commit
# ? Select the type of change:
# ❯ feat: A new feature
# fix: A bug fix
# docs: Documentation only
# style: Formatting changes
# ...
# ? What is the scope? (optional): auth
# ? Short description: adiciona autenticação JWT
# ? Longer description: (optional)
# ? Breaking change? No
# ? Issues closed: #42
Comandos avançados essenciais
git stash — guardar trabalho temporariamente
# Você está no meio de algo e precisa trocar de branch urgente
git stash # guarda mudanças não commitadas
git stash push -m "descrição" # com descrição ("stash save" foi descontinuado)
git stash list # lista o que está guardado
# stash@{0}: On feature/auth: trabalho em progresso
# stash@{1}: On main: fix urgente
git stash pop # aplica o mais recente e remove da lista
git stash apply stash@{1} # aplica específico (mantém na lista)
git stash drop stash@{0} # remove da lista sem aplicar
git stash clear # limpa tudo
git stash branch feature/nova stash@{0} # cria branch do stash
git cherry-pick — aplicar commit específico
# Você quer trazer apenas um commit de outra branch
git log feature/pagamento --oneline
# a1b2c3d fix: corrige cálculo de desconto ← quer este
# d4e5f6g feat: adiciona parcelamento
git switch main
git cherry-pick a1b2c3d
# Aplica apenas esse commit na branch atual
git revert — desfazer commit de forma segura
# Desfaz um commit criando um NOVO commit que reverte as mudanças
# Seguro para branches públicas — não reescreve histórico
git revert a1b2c3d
# Revert de merge commit
git revert -m 1 a1b2c3d # -m 1 = trata o PRIMEIRO pai como a linha principal, ou seja, desfaz o que
# veio da branch mesclada. Não é "voltar ao primeiro pai".
git reset — voltar no tempo
# Três modos — do mais seguro ao mais destrutivo:
# --soft: desfaz commit, mantém mudanças no staging
git reset --soft HEAD~1
# --mixed (padrão): desfaz commit, mantém mudanças no working directory
git reset HEAD~1
git reset --mixed HEAD~1
# --hard: desfaz commit, DESCARTA todas as mudanças
git reset --hard HEAD~1 # ⚠️ cuidado — mudanças perdidas
# HEAD~1 = último commit
# HEAD~3 = três commits atrás
# a1b2c3d = commit específico
# ⚠️ Nunca use --hard em branches públicas compartilhadas
git reflog — o histórico de tudo
# O reflog guarda TUDO que aconteceu — mesmo após reset --hard
git reflog
# HEAD@{0}: commit: feat: adiciona nova rota
# HEAD@{1}: reset: moving to HEAD~1
# HEAD@{2}: commit: fix: corrige bug crítico ← quero recuperar este
# HEAD@{3}: commit: feat: adiciona autenticação
# Recuperar commit perdido após reset --hard
git reset --hard HEAD@{2}
# ou
git checkout -b recuperado HEAD@{2}
Resolvendo conflitos de merge
# Situação: você fez merge e há conflito
git merge feature/nova-rota
# CONFLICT (content): Merge conflict in src/routes/index.js
# Automatic merge failed; fix conflicts and then commit the result.
# Abrindo o arquivo com conflito:
// src/routes/index.js com conflito
const express = require('express');
const router = express.Router();
<<<<<<< HEAD
// Sua versão (branch atual)
router.use('/usuarios', require('./usuarios'));
router.use('/auth', require('./auth'));
=======
// Versão da feature/nova-rota
router.use('/usuarios', require('./usuarios'));
router.use('/produtos', require('./produtos'));
>>>>>>> feature/nova-rota
module.exports = router;
// Resolvendo manualmente — combina as duas versões
const express = require('express');
const router = express.Router();
router.use('/usuarios', require('./usuarios'));
router.use('/auth', require('./auth')); // da nossa branch
router.use('/produtos', require('./produtos')); // da feature
module.exports = router;
# Após resolver todos os conflitos:
git add src/routes/index.js
git commit -m "merge: integra feature/nova-rota com resolução de conflito"
# Ou abortar o merge e voltar ao estado anterior:
git merge --abort
Git hooks com Husky — integrado ao projeto
# Já instalamos no artigo ESLint e Prettier: código limpo e padronizado — expandindo os hooks:
# .husky/commit-msg — valida o formato da mensagem de commit
# O "husky add" foi removido na versão 9. Agora o hook é um arquivo comum:
echo 'npx --no -- commitlint --edit "$1"' > .husky/commit-msg
npm install -D @commitlint/cli @commitlint/config-conventional
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style',
'refactor', 'test', 'chore', 'perf', 'ci'
]],
'subject-max-length': [2, 'always', 100],
'subject-case': [2, 'always', 'lower-case'],
},
};
# Agora commits com mensagens ruins são bloqueados:
git commit -m "arrumei umas coisas"
# ✖ subject may not be empty [subject-empty]
# ✖ type may not be empty [type-empty]
git commit -m "fix: corrige validação de email"
# ✔ Running commitlint...
# ✔ Commit aceito!
.gitignore — o que nunca commitar
# .gitignore para projetos Node.js
# Dependências
node_modules/
.pnp
.pnp.js
# Variáveis de ambiente — NUNCA commite!
.env
.env.local
.env.*.local
.env.production
# Build e dist
dist/
build/
out/
# Cobertura de testes
coverage/
.nyc_output/
# Logs
*.log
npm-debug.log*
yarn-debug.log*
logs/
# Cache
.cache/
.eslintcache
# (o .prettierignore NÃO entra aqui: ele é configuração do projeto e precisa
# ser versionado, como o .eslintignore e o .editorconfig. Só o CACHE do
# ESLint, que é gerado, fica de fora.)
# Editores
.vscode/
!.vscode/settings.json # exceto settings compartilhadas
!.vscode/extensions.json
.idea/
*.swp
*.swo
.DS_Store
Thumbs.db
# Uploads e assets gerados
uploads/
public/uploads/
Pull Requests — revisão de código profissional
Um bom PR (Pull Request / Merge Request) é uma forma de comunicação:
<!-- Template de PR: .github/pull_request_template.md -->
## O que este PR faz?
Breve descrição do que foi implementado ou corrigido.
## Por que esta mudança é necessária?
Contexto do problema que resolve ou feature que adiciona.
## Como foi testado?
- [ ] Testes unitários adicionados/atualizados
- [ ] Testes de integração passando
- [ ] Testado manualmente em desenvolvimento
## Checklist
- [ ] Código segue os padrões do projeto (ESLint/Prettier)
- [ ] Documentação atualizada se necessário
- [ ] Sem `console.log` esquecidos
- [ ] Variáveis de ambiente documentadas no .env.example
## Screenshots (se aplicável)
<!-- Para mudanças visuais -->
## Issues relacionadas
Closes #42
Aliases úteis — produtividade no terminal
# ~/.gitconfig — aliases que aceleram o trabalho
git config --global alias.st "status -sb"
git config --global alias.lg "log --oneline --graph --decorate --all"
git config --global alias.undo "reset --soft HEAD~1"
git config --global alias.unstage "restore --staged"
git config --global alias.branches "branch -v --sort=-committerdate"
git config --global alias.aliases "config --get-regexp alias"
# Uso:
git st # status compacto
git lg # log visual com árvore de branches
git undo # desfaz último commit mantendo mudanças
git unstage . # remove tudo do staging
git branches # lista branches ordenadas pela mais recente
Fluxo completo de trabalho em equipe
# 1. Sempre começar de main atualizado
git switch main
git pull origin main
# 2. Criar branch com nome descritivo
git switch -c feature/autenticacao-jwt
# 3. Trabalhar em pequenos commits semânticos
git add src/middlewares/auth.js
git commit -m "feat(auth): cria middleware de verificação JWT"
git add src/routes/auth.js
git commit -m "feat(auth): adiciona rotas de login e registro"
git add tests/auth.test.js
git commit -m "test(auth): adiciona testes de integração para autenticação"
# 4. Antes de abrir PR, atualizar com main
git fetch origin
git rebase origin/main # histórico linear, sem merge commit extra
# 5. Resolver conflitos se houver, então:
git rebase --continue
# 6. Push para o remoto
git push origin feature/autenticacao-jwt
# 7. Abrir Pull Request no GitHub
# 8. Aguardar revisão, fazer ajustes se solicitado
# 9. Após aprovação: merge e deletar branch
git switch main
git pull origin main
git branch -d feature/autenticacao-jwt
Tarefa para você
Pratique o fluxo completo em um projeto pessoal:
# 1. Crie um repositório no GitHub com o projeto da API REST
# 2. Configure todos os hooks do Husky:
# - pre-commit: lint-staged
# - commit-msg: commitlint
# 3. Crie as seguintes branches e faça trabalho real em cada uma:
# feature/paginacao → adiciona paginação às listagens
# feature/busca → adiciona busca por texto
# fix/validacao-email → melhora regex de validação
# 4. Simule um conflito:
# Edite o mesmo arquivo nas duas feature branches
# Mergeje a primeira em main
# Tente mergear a segunda e resolva o conflito
# 5. Use git log --oneline --graph --decorate --all
# e veja a árvore de branches que você criou
# 6. Tente fazer um commit com mensagem errada
# e veja o commitlint bloqueando
# 7. Use git stash para simular uma interrupção:
# - Você está no meio de um trabalho
# - Precisa corrigir um bug urgente em main
# - Faça stash, vá para main, corrija, volte e aplique o stash
Ver solução — o fluxo inteiro, com a saída real de cada comando
# ==============================================================
# 1 — o repositório
# ==============================================================
git init -b main
git config user.name "Ana Souza"
git config user.email "ana@exemplo.com"
printf 'node_modules/\ncoverage/\n.env\n' > .gitignore
git add . && git commit -m "chore: configura eslint, prettier, husky e commitlint"
# O remoto entra depois — nunca com a chave do .env já commitada.
git remote add origin git@github.com:ana/tarefas-api.git
git push -u origin main
# ==============================================================
# 2 — os hooks do Husky
# ==============================================================
npm install -D husky lint-staged @commitlint/cli @commitlint/config-conventional
npx husky init
echo 'npx lint-staged' > .husky/pre-commit
echo 'npx commitlint --edit "$1"' > .husky/commit-msg
chmod +x .husky/pre-commit .husky/commit-msg
# commitlint.config.js
#
# module.exports = {
# extends: ["@commitlint/config-conventional"],
# rules: {
# "type-enum": [2, "always",
# ["feat", "fix", "docs", "style", "refactor", "test", "chore", "perf", "ci"]],
# "subject-case": [2, "never", ["upper-case", "pascal-case"]],
# "header-max-length": [2, "always", 72],
# },
# };
# ==============================================================
# 3 — três branches, trabalho de verdade em cada uma
# ==============================================================
git switch -c feature/paginacao
# em listar(): teto no por_pagina, para que ?por_pagina=100000
# não vire um scan da coleção inteira
#
# const limite = Math.min(Number(por_pagina) || 10, 50);
# const paginaAtual = Math.max(Number(pagina) || 1, 1);
#
git commit -am "feat: limita por_pagina a 50 na listagem"
git switch main && git switch -c feature/busca
# busca em título E descrição — $or, não dois filtros (que virariam AND)
#
# filtros.$or = [
# { titulo: { $regex: busca, $options: "i" } },
# { descricao: { $regex: busca, $options: "i" } },
# ];
#
git commit -am "feat: busca tambem na descricao da tarefa"
git switch main && git switch -c fix/validacao-email
# /^\S+@\S+\.\S+$/ → /^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i
# O antigo aceita "a@b.c" e, pior, "a@b." seguido de qualquer coisa.
git commit -am "fix: exige tld valido no email do usuario"
# O primeiro commit desta série FALHOU, e o hook estava certo:
#
# src/controllers/tarefaController.js
# 21:11 error 'limite' is assigned a value but never used no-unused-vars
# 22:11 error 'paginaAtual' is assigned a value but never used no-unused-vars
# husky - pre-commit script failed (code 1)
#
# Eu tinha declarado as duas constantes e esquecido de usá-las no .skip()/.limit().
# O código rodaria — e a paginação continuaria sem teto, silenciosamente.
# ==============================================================
# 4 — o conflito
# ==============================================================
git switch main
git merge --no-ff feature/paginacao
git merge --no-ff feature/busca
# Auto-merging src/controllers/tarefaController.js
# Merge made by the 'ort' strategy.
# src/controllers/tarefaController.js | 8 +++++++-
#
# NÃO conflitou. As duas branches editaram o mesmo arquivo — o enunciado pede
# exatamente isso —, mas em linhas diferentes, e o git mescla por trecho.
# Editar o mesmo arquivo não basta: para conflitar, as duas têm de mexer nas
# MESMAS linhas.
#
# Então provoque o caso de verdade: mexa na main na mesma linha que a branch.
# na main, o regex do email de novo (outra pessoa, outra ideia)
git commit -am "fix: exige ao menos dois caracteres apos o ponto"
git merge --no-ff fix/validacao-email
# Auto-merging src/models/Usuario.js
# CONFLICT (content): Merge conflict in src/models/Usuario.js
# Automatic merge failed; fix conflicts and then commit the result.
git status --short
# UU src/models/Usuario.js ← U de "unmerged", dos dois lados
git diff --name-only --diff-filter=U # lista só o que conflitou
# src/models/Usuario.js
# No arquivo:
#
# <<<<<<< HEAD
# match: [/^\S+@\S+\.\S{2,}$/, "Email inválido."],
# =======
# match: [/^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i, "Email inválido."],
# >>>>>>> fix/validacao-email
#
# HEAD é a main; embaixo, a branch. Resolver é escolher — e apagar os três
# marcadores. Aqui fica a de baixo, mais estrita: `\S` aceita vírgula, ponto e
# arroba dentro do domínio, `[^\s@]` não.
git add src/models/Usuario.js
git commit --no-edit # aproveita a mensagem de merge do git
# ==============================================================
# 5 — a árvore que saiu disso
# ==============================================================
git log --oneline --graph --decorate --all
# * fbd7b32 (HEAD -> main) Merge branch 'fix/validacao-email'
# |\
# | * c599d9b (fix/validacao-email) fix: exige tld valido no email do usuario
# * | 7a5cb60 fix: exige ao menos dois caracteres apos o ponto
# * | aa22895 Merge branch 'feature/busca'
# |\ \
# | * | 8c80d8c (feature/busca) feat: busca tambem na descricao da tarefa
# | |/
# * | 822b13f Merge branch 'feature/paginacao'
# |\ \
# | |/
# |/|
# | * d9ede0a (feature/paginacao) feat: limita por_pagina a 50 na listagem
# |/
# * 595f709 refactor: anota o utilitario de slug
# * 5c2105e chore: configura eslint, prettier, husky e commitlint
# ==============================================================
# 6 — o commitlint recusando
# ==============================================================
git commit -m "arrumei umas coisas"
# ⧗ input: arrumei umas coisas
# ✖ subject may not be empty [subject-empty]
# ✖ type may not be empty [type-empty]
# ✖ found 2 problems, 0 warnings
# husky - commit-msg script failed (code 1)
git commit -m "refactor: anota o utilitario de slug" # passa
# [main 595f709] refactor: anota o utilitario de slug
# ARMADILHA: mensagem de merge escrita à mão também é recusada.
#
# git merge --no-ff feature/paginacao -m "merge: integra feature/paginacao"
# ✖ found 1 problems, 0 warnings
# Not committing merge; use 'git commit' to complete the merge.
#
# "merge" não está no type-enum. A mensagem PADRÃO do git passa —
# `Merge branch 'x'` está na lista de exceções do config-conventional.
# Por isso o merge acima usa `git commit --no-edit`, e não `-m`.
# ==============================================================
# 7 — a interrupção: git stash
# ==============================================================
git switch -c feature/tags
# ... meio arquivo novo, meia alteração ...
git status --short
# M src/utils/slug.js
# ?? src/utils/tags.js
git stash push -u -m "wip: normalizacao de tags"
# Saved working directory and index state On feature/tags: wip: normalizacao de tags
#
# O -u é o detalhe que custa caro: sem ele o arquivo NOVO (untracked) fica
# para trás, você troca de branch com ele no disco e acaba commitando o
# tags.js no meio do hotfix.
git status --short # árvore limpa
git stash list
# stash@{0}: On feature/tags: wip: normalizacao de tags
git switch main
# ... corrige o bug urgente ...
git commit -am "fix: ignora status invalido vindo do erro"
# [main d2b91a0] fix: ignora status invalido vindo do erro
git switch feature/tags
git stash pop
# M src/utils/slug.js
# ?? src/utils/tags.js
# Dropped refs/stash@{0} (bb812f2...)
# `pop` aplica e descarta. Se o stash puder conflitar com o que mudou na
# branch, use `git stash apply`: ele mantém a cópia guardada até você
# conferir o resultado.
O hook é uma rede local, não uma garantia. --no-verify passa por cima dele, quem clona sem npm install não o tem, e o lint-staged nem roda em commit de merge — que é justamente onde alguém acabou de editar código à mão para resolver um conflito. O hook serve para você não perder tempo com o óbvio; quem impede o código ruim de entrar na main é o CI, rodando o mesmo npm run lint && npm test num lugar que ninguém consegue pular.
Uma regra evita a maior parte do estrago possível com o que este artigo apresentou: não reescreva história que já saiu da sua máquina. rebase, commit --amend e reset --hard são excelentes enquanto o commit é só seu, e destrutivos depois que ele é de todo mundo. O restante funciona como rede de segurança — e o reflog, em especial, recupera quase tudo que parece perdido para sempre.
Fontes e Referências
- Git — Documentação oficial: https://git-scm.com/doc
- Conventional Commits: https://www.conventionalcommits.org/pt-br
- Commitizen: https://commitizen-tools.github.io/commitizen
- Commitlint: https://commitlint.js.org
- Husky: https://typicode.github.io/husky
- Atlassian — Git Tutorials: https://www.atlassian.com/git/tutorials
- GitHub — Pull Request templates: https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests
- Pro Git — Scott Chacon e Ben Straub (gratuito): https://git-scm.com/book/pt-br/v2
- Oh Shit, Git!?: https://ohshitgit.com/pt_BR
Exercícios
Exercício 1
Você abriu um PR ontem e três pessoas já baixaram sua branch. Hoje, para "limpar o histórico", você roda os comandos abaixo. O que acontece com o trabalho delas?
git switch feature/pagamentos
git rebase -i main # esmaga 8 commits em 2
git push --force origin feature/pagamentos
Ver resposta
✓ Resposta: Os commits que elas têm deixam de existir no remoto. O rebase não move commits: ele cria commits novos, com o mesmo conteúdo e hashes diferentes, e abandona os antigos. Com o --force, a branch remota passa a apontar para a história nova, e quem já tinha baixado a antiga fica com um histórico que diverge do servidor. No próximo git pull, o Git tenta juntar as duas versões dos mesmos commits e produz conflito em código que ninguém escreveu duas vezes — ou, pior, duplica todos os commits. É a "regra de ouro" que o artigo cita: não reescreva história que já saiu da sua máquina. As ferramentas envolvidas — rebase, commit --amend e reset --hard — são excelentes enquanto o commit é só seu, e destrutivas depois que é de todos. Se o force for mesmo necessário, e às vezes é, use --force-with-lease: ele recusa a operação se alguém tiver enviado algo que você ainda não viu, o que transforma um estrago silencioso num erro na sua frente. E quem já baixou a versão antiga se recupera com git fetch seguido de git reset --hard origin/feature/pagamentos — desde que não tenha trabalho local em cima dela.
Exercício 2
Você está em feature e executa os comandos abaixo. Depois disso, main contém as suas alterações?
git switch feature
git merge main
git log --oneline --graph
Ver resposta
✓ Resposta: Não. O git merge main traz o conteúdo de main para dentro da branch em que você está — o merge commit nasce em feature, e main continua exatamente onde estava. A confusão é comum porque o comando nomeia a branch de origem, não a de destino: quem recebe é sempre a branch atual. Para levar o trabalho no sentido inverso seria git switch main seguido de git merge feature — e é isso que o botão de merge do Pull Request faz no servidor. O caso apresentado aqui tem um nome e um propósito: é a atualização da branch, feita para trazer o que mudou em main e resolver os conflitos antes da revisão, em vez de despejá-los no PR. Para essa finalidade, muita gente prefere git rebase main, que produz o mesmo efeito sem criar um merge commit e deixa o histórico linear — com a ressalva do exercício anterior, de que isso só é seguro enquanto a branch não foi compartilhada. Um jeito rápido de conferir o que aconteceu: git log --oneline --graph --all mostra as duas linhas e para onde cada uma aponta.
Exercício 3
Um commit com a senha do banco foi enviado ontem. O desenvolvedor resolve assim. O segredo está protegido?
git rm --cached .env
echo ".env" >> .gitignore
git commit -m "chore: remove .env do versionamento"
git push
Ver resposta
✓ Resposta: Não está. Os comandos fazem a parte certa daqui para a frente — o git rm --cached tira o arquivo do rastreamento sem apagá-lo do disco, e o .gitignore impede que ele volte —, mas o segredo continua no histórico. Qualquer pessoa recupera com git show HEAD~1:.env, e quem clonou ontem já tem uma cópia. Para um repositório público, assuma que já foi lido: existem robôs varrendo commits novos em busca exatamente disso, e o intervalo entre o push e a primeira tentativa de uso costuma ser de minutos. A primeira providência, portanto, não é mexer no Git — é rotacionar a credencial: trocar a senha do banco, revogar a chave, gerar outra. Só depois vem a limpeza do histórico, com git filter-repo ou o BFG, que é operação destrutiva: reescreve todos os commits a partir do ponto afetado, muda todos os hashes, exige force push e obriga a equipe inteira a refazer o clone. E ela não devolve o que já vazou. A defesa que funciona é anterior ao acidente: .env no .gitignore desde o primeiro commit, um .env.example com placeholders no lugar dele, e um hook de pré-commit que recuse arquivos com cara de segredo.
Exercício 4
Dois commits foram desfeitos com reset --hard e a branch foi apagada por engano. O trabalho de dois dias sumiu do git log. Ele é recuperável?
git reset --hard HEAD~2
git branch -D feature/relatorios
git log --oneline
# nenhum sinal dos commits
Ver resposta
✓ Resposta: Sim, quase certamente. O git log mostra apenas os commits alcançáveis a partir das referências atuais, e os dois deixaram de ser alcançáveis — mas continuam no repositório. Quem sabe onde eles estão é o reflog, que registra todo movimento do HEAD e das branches, inclusive resets e exclusões: git reflog lista as posições anteriores, e git reset --hard HEAD@{2} ou git checkout -b recuperado <hash> traz tudo de volta. Para a branch apagada, git reflog show feature/relatorios costuma revelar o último commit dela. Três ressalvas importantes: o reflog é local, não vem no clone e não ajuda em commit que nunca existiu na sua máquina; ele expira, por padrão em 90 dias para o que é alcançável e 30 para o resto; e nada disso recupera alteração que nunca foi commitada — um reset --hard sobre trabalho apenas salvo no editor é perda definitiva, e é por isso que git stash vale o hábito antes de qualquer operação arriscada. A regra prática que fica: enquanto houver commit, há como voltar; o que o Git não protege é o que ele nunca viu.
Exercício 5
O commitlint está ativo com a configuração do artigo. Quais destes commits passam?
git commit -m "Corrige bug do login"
git commit -m "fix: Corrige validação de email"
git commit -m "fix(auth): corrige validação de email"
git commit -m "feature: adiciona relatório"
Ver resposta
✓ Resposta: Só o terceiro. O primeiro não tem tipo nem os dois-pontos, então o commitlint nem consegue analisar a mensagem e reclama de type-empty e subject-empty ao mesmo tempo — o que confunde quem lê, porque o assunto está claramente lá. O segundo tem tipo válido, mas o assunto começa com maiúscula, e a regra subject-case configurada como lower-case o reprova. O quarto usa feature, que não está na lista de type-enum: o tipo correto é feat, e essa é provavelmente a rejeição mais comum entre quem está começando, junto com bugfix no lugar de fix. O terceiro tem tipo, escopo, dois-pontos e assunto em minúscula — passa. O valor de tudo isso não está na disciplina pela disciplina: com o padrão garantido por hook, dá para gerar o CHANGELOG automaticamente a partir do histórico e deduzir a próxima versão pelo tipo dos commits, já que fix sugere um patch, feat um minor e um rodapé BREAKING CHANGE um major. Sem a garantia automática, o padrão dura duas semanas.