Common Git mistakes: force push, committed secrets, rebasing shared branches Intermediate
Estimated reading time: 9 min read
Read first
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…
Commits
Section titled “Commits”git add . drags in .env
Section titled “git add . drags in .env”Staging the whole directory without looking
Command people type
git add .git commit -m "feat: add Stripe payments"git push# .env (STRIPE_KEY=sk_live_...) just went to the remoteWhat to do
echo ".env*" >> .gitignore && echo "!.env.example" >> .gitignoregit add -p # pick hunk by hunkgit status --short # review the file listgit diff --staged --stat # only what is about to be committedgit commit -m "feat(payment): add Stripe payments"Why
git add .stages every non-ignored file:.env, DB dumps,.pemfiles downloaded for testing.- One push and the secret is leaked; you must rotate it, not just delete the file.
.gitignoreonly affects untracked files. A file already committed stays tracked — you needgit rm --cached .env.
Commit message “fix”
Section titled “Commit message “fix””A message that says nothing
Bad commit message
a1b2c3d fixd4e5f6a fix review9f8e7d6 updateGood 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 decimalin a bigint column, making the reconciliation job off by 1 VND.
Refs: PAY-482Why
- Three months later, when
bisectpoints at afixcommit, 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.21refactor(order): extract pricing into PricingService # old tests passfeat(order): support percentage vouchers # only the new logicWhy
- Vouchers compute wrong →
git reverton 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 -pbefore committing.
Push and rebase
Section titled “Push and rebase”git push --force after a rebase
Section titled “git push --force after a rebase”Force push overwrites a teammate's commits
Command people type
git rebase origin/maingit push --force# + 9046029...3ab78af feature -> feature (forced update)# commit D that Binh just pushed to feature disappears from the remoteCommand to type
git rebase origin/maingit push --force-with-lease --force-if-includes# ! [rejected] feature -> feature (stale info)# → git fetch, rebase onto Binh's commit, push againWhy
--forcechecks 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-leaserefuses if the remote differs from theorigin/featureyou last saw.- An IDE silently running
git fetchupdatesorigin/featureand defeats the lease.--force-if-includescatches this case (tested:rejected ... (remote ref updated since checkout)).
Rebasing a branch several people use
Section titled “Rebasing a branch several people use”Rebasing develop and force pushing
Common
# An, on develop (Binh has committed B on top of A)git rebase maingit 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 M1What to do
# Shared branch: integrate main with a merge, no rewritegit switch developgit merge origin/maingit push
# Rebase only your OWN branchgit switch feature/vouchergit rebase origin/maingit push --force-with-lease --force-if-includesWhy
- Rebase creates new commits (
A') and abandons the old ones (A). Anyone who built onAmerges both copies into history. - Consequences: duplicate commits, repeated conflicts,
bisectandrevertbecome 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).
Recovery
Section titled “Recovery”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
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 mainWhat to do
git revert <sha-of-bad-commit> # creates an inverse "Revert ..." commitgit 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.revertis 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 script returns the wrong exit code
Section titled “Bisect script returns the wrong exit code”Bisect counts 'command not found' as bad
Script people write
#!/bin/sh# test.sh was renamed in some intermediate commits → sh returns 127sh ./test.shScript to write
#!/bin/sh[ -f ./test.sh ] || exit 125 # cannot test → skipnpm ci --silent || exit 125 # cannot build → skipsh ./test.sh # only the test reproducing the bug decides good/badWhy
git bisect runtreats1–127(except125) as bad. A missing file or command returns127, so the first commit lacking it is reported as the “first bad commit”.- Tested: the real bug is in commit
c7, but bisect concludesc2— the commit that renamedtest.sh. - Git 2.54 rechecks when it sees
126/127: if the good commit also returns that code, bisect stops withbogus 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
git revert -m 1 <sha-merge> # remove feature X from main# ... fix the bug on the feature branch, add commit Ygit merge feature # only Y lands on main, X is still goneWhat to do
git revert <sha-of-revert-commit> # "revert the revert" → X comes backgit merge feature # then bring in YWhy
git revertdocs: 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.
Secrets and PRs
Section titled “Secrets and PRs”Removing a secret with a new commit
Section titled “Removing a secret with a new commit”Thinking a commit that deletes the file is enough
Common
git rm .envecho ".env" >> .gitignoregit 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_abcWhat to do
# 1. Rotate/revoke the key at Stripe IMMEDIATELY# 2. Check access logs for the exposure window# 3. Untrack the file, add the ignoregit rm --cached .env && echo ".env" >> .gitignore# 4. Only if needed (cannot rotate): rewrite historygit filter-repo --sensitive-data-removal --invert-paths --path .envWhy
- A delete commit only removes the file from the latest snapshot. Old commits still contain the secret;
git log -p -Sorgit show <sha>:.envreads 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).
Giant PRs
Section titled “Giant 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 flagWhy
- 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.