skip to content

What does git push --force-with-lease do that git push --force does not?

level: middleimportance: must knowfreq 65%

answer

  1. conditional versus unconditional write
  2. compare-and-swap on the remote ref
  3. the expectation comes from origin/<branch>
  4. stale info means someone else pushed
  5. the lease can be renewed behind your back

basics

~20 s

It makes the forced update conditional: the push succeeds only if the remote branch still points where your remote-tracking ref says it does. If someone else pushed in between, the update is rejected instead of silently discarding their commits.

solid answer

~40 s

`git push --force` sets the remote ref unconditionally — whatever was there is replaced. `git push --force-with-lease` turns the same operation into a compare-and-swap: Git sends the value it expects the remote ref to have, and the remote applies the update only if the ref still has that value. The expected value defaults to your remote-tracking ref, `refs/remotes/origin/<branch>` — the last state you actually observed. So the common accident, where a teammate pushed after your last fetch and your rebase silently drops their commits, becomes a rejection you have to look at. You can also pin the expectation explicitly with `--force-with-lease=<refname>:<expected-sha>`, the version that survives anything updating your tracking refs behind your back. It is the sane default for republishing a rewritten branch.

code

bash · 3 lines
bash
git fetch origin
git rebase origin/main
git push --force-with-lease origin feature/login

go deeper

for a junior

Recall that --force-with-lease is the safer flag to type and that it fails when someone else has pushed since your last fetch.

for a middle

Explain it as a compare-and-swap whose expected value defaults to refs/remotes/origin/<branch>, and describe what a stale-info rejection means and how to resolve it.

for a senior

Name the blind spot — anything refreshing tracking refs renews the lease — and show the mitigations: an explicit expected value, --force-if-includes, and server-side refusal of non-fast-forwards on shared branches.

for a principal

Decide the standard: whether bare --force is permitted at all, how the safe form becomes what people actually type, and where enforcement must live so a client flag cannot bypass it.

## The mechanism Both flags ask the remote to move a ref to a commit that is not a descendant of the current one. The difference is what is sent alongside the request. - `--force` sends only the new value. The remote overwrites the ref. - `--force-with-lease` sends the new value **and** the value the client believes is currently there. The remote compares, and rejects the update if the actual value differs. This is a compare-and-swap on the ref, applied on the remote side, so there is no window in which someone else's push slips between the check and the update. The metaphor in the name is a lease: you took out a lease on that ref when you last observed it, and you are asserting nobody else has touched it since. ## Where the expected value comes from With no argument, the expected value is your remote-tracking ref for that branch — `refs/remotes/origin/<branch>`, the ref `git fetch` updates and `git log origin/main` reads. That is exactly "the last state of the remote I actually saw", which is why the default is usually right. The explicit form is `--force-with-lease=<refname>:<expected>`, for example `git push --force-with-lease=main:abc1234 origin main`. Here you name the value yourself, so nothing that happens to your tracking refs can change what you are asserting. ## Why this matters in practice The scenario it defends against is mundane. You fetch, rebase your feature branch, and step away. A teammate pushes a fix to the same branch. You come back and force-push. With `--force`, their commit is gone from the branch with no message of any kind. With `--force-with-lease`, the remote sees that the ref no longer holds the value you leased and rejects the push; you fetch, look at what arrived, and integrate it deliberately. ## What it does not protect against The lease is only as good as your remote-tracking ref, and that ref is updated by anything that fetches — a background fetch, a `git fetch --all`, an editor polling the remote, a script. If the tracking ref advances to include a teammate's commit while you were not looking, the lease is renewed against a state you never inspected, and a force-with-lease push will happily discard the commit. So `--force-with-lease` protects against *staleness*, not against *not having looked*. Git 2.30 added `--force-if-includes` specifically for that gap, and pinning an explicit expected value is the other way to close it. It also does not make the push safe socially: a successful lease check only means nobody else moved the ref. Everyone who already fetched the old history still has to deal with the rewrite. ## Make it the default you type Because `--force` is shorter, people type it out of habit. Two habits fix that: an alias (`git config --global alias.pushf 'push --force-with-lease'`), and, on repositories where it matters, refusing non-fast-forward updates on shared branches at the receiving end so no client flag can bypass the policy. ## The crisp interview answer "`--force` is an unconditional write; `--force-with-lease` is a compare-and-swap against the value my remote-tracking ref last saw, so a push that would discard a commit I have not seen gets rejected instead of succeeding. Its blind spot is anything that refreshes my tracking refs without me looking, which is what `--force-if-includes` and an explicit expected value address."

  • What exactly does a rejection reading 'stale info' tell you?
    That the remote ref no longer holds the value your remote-tracking ref recorded, so somebody pushed after your last fetch. Fetch, inspect what arrived with `git log origin/<branch>`, integrate it into your rewritten branch, then push again.
  • When would you pin the expected value explicitly instead of using the default?
    Whenever something other than you may update your remote-tracking refs — a background fetch from an editor, a scripted `git fetch --all`, a wrapper tool. Passing `--force-with-lease=<ref>:<sha>` asserts a value you chose, so an unnoticed fetch cannot renew the lease for you.
  • Does a successful force-with-lease push mean the rewrite is safe for the team?
    No. It only means nobody moved the ref since you looked. Everyone who already fetched the branch still holds the pre-rewrite commits and must reset or rebase onto the new tip rather than merge, or the old commits come back.

saying these in an interview costs you the question

  • Calls it just a politer alias for --force
  • Thinks it compares local and remote commit contents
  • Believes it prevents any history rewrite
  • Assumes it protects against changes you never inspected
  • Never fetches before pushing and blames the flag

context