Lesson 23 of 30
git push --force doesn't add history, it replaces it — and on a shared branch, 'replaces' can mean 'deletes a teammate's work without telling anyone.'
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
git push normally only succeeds if your local branch is a fast-forward of the remote — meaning every commit already on the remote is still an ancestor of what you're pushing. That check exists specifically to stop you from accidentally erasing commits someone else pushed while you were working locally.
git push --force disables that check. It tells the remote: "stop arguing, make your branch point exactly where mine does, no matter what." If your local history and the remote's history have diverged — say, you rebased and rewrote commits that a teammate already based their own work on top of — a force push doesn't merge the two histories. It overwrites the remote branch pointer, full stop. Any commit that was only reachable through the old pointer becomes unreachable from that branch. It isn't instantly deleted from the server's object database (that's what makes reflog-style recovery possible for a while), but nobody fetching the branch normally will ever see it again, and if your teammate hasn't pushed a copy of their own commits anywhere else, their work is effectively gone from their perspective the next time they pull.
This is exactly why force-pushing to a shared branch like main is treated as close to a firing offense on most teams, while force-pushing to your own feature branch — one nobody else has based work on — is completely normal and even encouraged (cleaning up a PR's commit history before merge, for instance).
Picture this sequence: you rebase your feature branch locally to clean it up. Meanwhile, a teammate pushed one more commit to that same remote branch that you don't have yet.
$ git push origin feature/checkout
To github.com:team/app.git
! [rejected] feature/checkout -> feature/checkout (non-fast-forward)
error: failed to push some refs
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart.
Git is protecting you here — your rebase rewrote history, so your local branch and the remote have diverged. A plain --force push would blow away your teammate's newest commit without so much as a warning:
$ git push --force origin feature/checkout
To github.com:team/app.git
+ a1b2c3d...e4f5g6h feature/checkout -> feature/checkout (forced update)
That succeeded — and their commit is now unreachable from feature/checkout. --force-with-lease closes exactly this gap. It still force-pushes, but first checks that the remote branch is still at the commit you last saw when you fetched it:
$ git fetch origin
$ git push --force-with-lease origin feature/checkout
To github.com:team/app.git
! [rejected] feature/checkout -> feature/checkout (stale info)
error: failed to push some refs
hint: Updates were rejected because a pushed branch tip is behind its remote
It refuses — because the remote moved since your last fetch, exactly the situation where a raw --force would silently destroy work. You fetch, see the new commit, rebase on top of it, and push again — now it's a fair fast-forward-equivalent update, not a blind overwrite.
--force-with-lease isn't foolproof without a preceding fetch. It compares against your last-known remote state, which comes from your local tracking ref. If you haven't fetched recently, your "lease" is stale and the check is weaker than you think — run git fetch right before, or use --force-with-lease=<branch>:<expected-hash> for the strict version.The rejection Git shows here is the identical mechanism behind why git push normally requires a merge or rebase before it'll accept diverged history — Git refusing to silently discard commits it can't prove are still reachable. --force is you explicitly telling Git to skip that guard; --force-with-lease is a version of "skip the guard" that still checks one thing first: that nobody moved the target while you weren't looking.