Skip to content
Knowledge Hub

Common Git mistakes: force push, committed secrets, rebasing shared branches Intermediate

Estimated reading time: 9 min read

This page collects the Git mistakes backend teams commonly make, each next to the right way. Output in the examples comes from test runs on git 2.54.0 in a temp repo.

Loading diagram…

Staging the whole directory without looking

Command people type

Terminal window
git add .
git commit -m "feat: add Stripe payments"
git push
# .env (STRIPE_KEY=sk_live_...) just went to the remote

What to do

Terminal window
echo ".env*" >> .gitignore && echo "!.env.example" >> .gitignore
git add -p # pick hunk by hunk
git status --short # review the file list
git diff --staged --stat # only what is about to be committed
git commit -m "feat(payment): add Stripe payments"

Why

  • git add . stages every non-ignored file: .env, DB dumps, .pem files downloaded for testing.
  • One push and the secret is leaked; you must rotate it, not just delete the file.
  • .gitignore only affects untracked files. A file already committed stays tracked — you need git rm --cached .env.

A message that says nothing

Bad commit message

a1b2c3d fix
d4e5f6a fix review
9f8e7d6 update

Good commit message

fix(order): round the discounted total to whole VND
A 10% discount on a 199,999 VND order returned 179,999.1 and stored a decimal
in a bigint column, making the reconciliation job off by 1 VND.
Refs: PAY-482

Why

  • Three months later, when bisect points at a fix commit, nobody knows what it fixed or whether it can be reverted.
  • Without Conventional Commits, changelog and SemVer tooling skips the commit.
  • A “fix review” commit should be git commit --fixup=<sha> followed by autosquash (see Branch & merge).

Mixing a refactor and a feature in one commit

Section titled “Mixing a refactor and a feature in one commit”

One commit doing three things

Common history

feat(order): support percentage vouchers
src/order/pricing.ts | 180 +++++---- (rename, extract functions, change logic)
src/order/order.module.ts | 12 +- (change DI)
package.json | 2 +- (upgrade lodash)

History to aim for

chore(deps): upgrade lodash 4.17.20 → 4.17.21
refactor(order): extract pricing into PricingService # old tests pass
feat(order): support percentage vouchers # only the new logic

Why

  • Vouchers compute wrong → git revert on the combined commit also removes the refactor and the lodash upgrade, which may break other code that relies on them.
  • The reviewer cannot tell which lines change behavior and which only rename.
  • Split them with git add -p before committing.

Force push overwrites a teammate's commits

Command people type

Terminal window
git rebase origin/main
git push --force
# + 9046029...3ab78af feature -> feature (forced update)
# commit D that Binh just pushed to feature disappears from the remote

Command to type

Terminal window
git rebase origin/main
git push --force-with-lease --force-if-includes
# ! [rejected] feature -> feature (stale info)
# → git fetch, rebase onto Binh's commit, push again

Why

  • --force checks nothing: a commit a teammate just pushed is deleted from the remote and nobody is told. They only find out when the PR is missing code.
  • --force-with-lease refuses if the remote differs from the origin/feature you last saw.
  • An IDE silently running git fetch updates origin/feature and defeats the lease. --force-if-includes catches this case (tested: rejected ... (remote ref updated since checkout)).

Rebasing develop and force pushing

Common

Terminal window
# An, on develop (Binh has committed B on top of A)
git rebase main
git push --force-with-lease
# Binh: git pull (merge) → history has TWO copies of "feat: A"
* 538f95a Merge branch 'develop' ...
|\
| * e06f472 feat: A ← A' (new hash)
| * 7c86048 M2
* | 2f2e839 feat: B
* | e75f376 feat: A ← old A
|/
* 04427b4 M1

What to do

Terminal window
# Shared branch: integrate main with a merge, no rewrite
git switch develop
git merge origin/main
git push
# Rebase only your OWN branch
git switch feature/voucher
git rebase origin/main
git push --force-with-lease --force-if-includes

Why

  • Rebase creates new commits (A') and abandons the old ones (A). Anyone who built on A merges both copies into history.
  • Consequences: duplicate commits, repeated conflicts, bisect and revert become hard to use. Pro Git: “Do not rebase commits that exist outside your repository and that people may have based work on.”
  • If it already happened, affected people should use git pull --rebase: git drops the duplicates (tested: dropping ... feat: A -- patch contents already upstream).

reset --hard on a commit already pushed to main

Section titled “reset --hard on a commit already pushed to main”

Removing a bad commit with reset + force push

Common

Terminal window
git reset --hard HEAD~1 # drop "feat: bug"
git push # ! [rejected] main -> main (non-fast-forward)
git push --force # "there we go"
# A colleague had already pulled "feat: bug", committed more and pushed:
# 53e7633..e93d9cd main -> main
# → "feat: bug" is back on main

What to do

Terminal window
git revert <sha-of-bad-commit> # creates an inverse "Revert ..." commit
git push # normal fast-forward
# reverting a merge commit: git revert -m 1 <sha-merge>

Why

  • reset + force push rewrites history that every other clone still holds. All it takes is one person pushing from an old clone and the bad commit comes back.
  • revert is a new commit that everyone receives on pull; history records clearly what was removed and why.
  • On shared branches there are no exceptions. Branch protection should block force pushes to main.

Bisect counts 'command not found' as bad

Script people write

#!/bin/sh
# test.sh was renamed in some intermediate commits → sh returns 127
sh ./test.sh

Script to write

#!/bin/sh
[ -f ./test.sh ] || exit 125 # cannot test → skip
npm ci --silent || exit 125 # cannot build → skip
sh ./test.sh # only the test reproducing the bug decides good/bad

Why

  • git bisect run treats 1–127 (except 125) as bad. A missing file or command returns 127, so the first commit lacking it is reported as the “first bad commit”.
  • Tested: the real bug is in commit c7, but bisect concludes c2 — the commit that renamed test.sh.
  • Git 2.54 rechecks when it sees 126/127: if the good commit also returns that code, bisect stops with bogus exit code 127 for good revision. But when only a few commits lack the command, the result is still silently wrong.

Reverting a merge, then merging that branch again

Section titled “Reverting a merge, then merging that branch again”

Merging again after reverting a merge

Common

Terminal window
git revert -m 1 <sha-merge> # remove feature X from main
# ... fix the bug on the feature branch, add commit Y
git merge feature # only Y lands on main, X is still gone

What to do

Terminal window
git revert <sha-of-revert-commit> # "revert the revert" → X comes back
git merge feature # then bring in Y

Why

  • git revert docs: reverting a merge declares that you will never want the tree changes brought in by that merge.
  • Merging again afterwards only brings the new commits (Y). Tested: X’s files do not return to main, with no conflict or warning.
  • To bring the feature back, you must revert the revert commit itself.

Thinking a commit that deletes the file is enough

Common

Terminal window
git rm .env
echo ".env" >> .gitignore
git commit -m "chore: remove .env"
git push
# Anyone who clones the repo:
git log -p -S sk_live --oneline
# c9ad1c2 feat: init
# +STRIPE_KEY=sk_live_abc

What to do

Terminal window
# 1. Rotate/revoke the key at Stripe IMMEDIATELY
# 2. Check access logs for the exposure window
# 3. Untrack the file, add the ignore
git rm --cached .env && echo ".env" >> .gitignore
# 4. Only if needed (cannot rotate): rewrite history
git filter-repo --sensitive-data-removal --invert-paths --path .env

Why

  • A delete commit only removes the file from the latest snapshot. Old commits still contain the secret; git log -p -S or git show <sha>:.env reads it back.
  • Attackers scan repos continuously; the time from push to exploitation can be very short. That is why you rotate first.
  • A rewrite cannot remove the data from clones, forks, and commits referenced by PRs on GitHub (see Secrets & PRs).

One PR that bundles everything

Common PR

PR #812: partial refunds + module refactor + Prisma upgrade
48 files changed, 1,900 insertions(+), 640 deletions(-)
Description: "see title"

How to split the PR

PR #810: chore(deps): upgrade Prisma (merged first)
PR #811: refactor(payment): rename module (old tests pass)
PR #812: feat(refund): partial refunds (~300 lines, behind a feature flag)
Description: problem, solution, how tested, risks, rollback = turn off the flag

Why

  • A 2,000-line PR gets “LGTM” because the reviewer gives up, not because the code is right.
  • Squash merge folds everything into one commit: a broken feature cannot be reverted alone, and reverting touches the upgrade’s migrations.
  • The ~400 changed lines target is a rule of thumb, not a standard; what matters is that the reviewer understands all of the PR.

Next: go back to the Git cluster overview or reread the Code review checklist for Git.