Git avançado e fluxo de trabalho em equipe

[117] 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 Referências

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

Laços de Repetição: for, while e do...while
Laços de Repetição: for, while e do...while

Esquecer o incremento dentro do while congela a aba, e trocar um sinal na…

A evolução das requisições: de XMLHttpRequest ao Fetch
A evolução das requisições: de XMLHttpRequest ao Fetch

O fetch não caiu do céu. Antes dele foram quinze anos de XMLHttpRequest —…

Express.js: o framework web do Node
Express.js: o framework web do Node

O mesmo endpoint que ocupava trinta linhas de if e regex cabe em quatro com…