Git avançado e fluxo de trabalho em equipe

Git avançado e fluxo de trabalho em equipe

Saber add, commit e push é o mínimo. O que aparece em projeto com mais gente é outra coisa: branch desatualizada, histórico bagunçado, conflito no meio do arquivo e commit que precisa voltar. Aqui estão merge contra rebase, Git Flow e GitHub Flow, commits semânticos, stash, cherry-pick, revert, reset e o reflog.
Javascript

• • 26 min de leitura

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 leituras recomendadas

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.

Comentários

Mais em Javascript

Criando um servidor HTTP com Node.js puro
Criando um servidor HTTP com Node.js puro

Um require de http, uma função de dois argumentos, e o servidor está de pé. O…

Eventos: click, input, submit e muito mais
Eventos: click, input, submit e muito mais

Ler e escrever no DOM é metade do trabalho; a outra metade é reagir. Aqui…

O que é o DOM e como o JavaScript interage com o HTML
O que é o DOM e como o JavaScript interage com o HTML

O navegador não guarda o seu HTML como texto: ele o transforma numa árvore de…