Force a Git push: the one case where it does no harm

The push came back rejected with the word non-fast-forward in it, and the hint at the bottom suggested integrating the remote changes first. The branch is a private one, the rewrite was deliberate, and integrating anything would undo the work just finished. So the next thing typed is git push --force, which works, and which on a shared branch quietly deletes somebody else's commits. The distance between those two situations is the whole subject. The manual is unusually blunt about where the line sits, and there are two flags that hold the line without requiring anyone to remember which branch is which.

Why Git refused the push

The rejection is not about permissions or about conflicts. It is about a single structural rule, stated in the push manual: an update that moves a ref from commit A to commit B is a fast forward if and only if B is a descendant of A. In a fast forward, the commits A was built on are a subset of the commits B is built on, so no history is lost. A non fast forward update loses history, and the command refuses it by default to prevent exactly that loss.

Two situations produce the rejection, and they need opposite responses.

The first is the collaborative one. Two people start from commit X, one builds a history ending at A and pushes it, the other builds a history ending at B. The second push would replace A with B, and the changes in A would disappear from everyone's next clone. The manual's answer is to bring both histories together first: a git pull creates a merge commit joining A and B, or git pull --rebase replays the local work on top of A, and either result then fast forwards.

The second is the solitary one. A commit is pushed, then amended or rebased, then pushed again. Nothing was lost by anyone else, because nobody else was involved. The rejection here is Git noticing that the new commit is not a descendant of the old one, which is true and also irrelevant.

The reason both look identical on screen is that Git cannot tell them apart. It can only see the shape of the graph, not who has fetched what.

The one case where forcing does no harm

The manual draws the boundary in a single sentence, in the section on fast forwards. After describing the amend case, it says that only if you are certain that nobody in the meantime fetched your earlier commit and started building on top of it can git push --force be used to overwrite it. Then it adds the line worth keeping: git push --force is a method reserved for a case where you do mean to lose history.

So the safe case is narrow and describable. The branch is one nobody else pushes to. The commits being replaced are ones nobody has based work on. The history being discarded is history whose loss is the intention, usually because it was a draft: fixup commits folded together, a rebase onto a moved base, a commit message corrected, a file with a credential in it removed from a branch that has not been reviewed yet.

Everything outside that description is a different job. A change that has to be removed from a branch other people already have is a git revert, which adds a commit that undoes an earlier one rather than rewriting anything. That distinction is the useful one to hold: forcing is for drafts, reverting is for published work, and the question of which applies is a question about who else holds the branch, not about how bad the mistake was.

The flags to type instead of --force

Two options replace the judgement call with a check the machine performs.

Flag What it checks Fails when
--force nothing never, which is the problem
--force-with-lease the remote ref still holds the value expected locally someone else pushed since the last fetch
--force-if-includes the remote tip is reachable from the local branch's reflog the local branch was rewritten without seeing the remote's update

--force-with-lease works like taking a lease on the ref without locking it. It overrides the must fast forward rule only if the current value of the remote ref matches the expected value, and the push fails otherwise. Used alone, without details, it protects every remote ref about to be updated by requiring its current value to equal the remote tracking ref held locally. In practice that means a rewrite is allowed if the last fetch is still accurate, and refused if the remote moved.

git fetch origin
git push --force-with-lease origin feature/parser-cleanup

The manual names the way this protection is defeated, and it is worth reading before relying on it. Anything that implicitly runs git fetch in the background, such as a scheduled job or an editor integration that polls the remote, updates the remote tracking ref, and the lease then reflects a state nobody actually looked at. The protection is against changes the local work was not based on, and a background fetch trivially removes it.

--force-if-includes closes that gap. It verifies that the tip of the remote tracking ref is reachable from one of the reflog entries of the local branch, which is a way of asking whether the remote's update was actually integrated locally before the rewrite happened. A background fetch does not create a reflog entry on the branch, so the check survives it. The manual notes that the option is a no-op if passed without --force-with-lease, so the two belong together:

git push --force-with-lease --force-if-includes origin feature/parser-cleanup

Both can be made the default with a Git alias, which is the practical way to stop typing --force out of habit.

What --force touches that was not intended

There is a second hazard in --force that has nothing to do with other people. The manual points out that the flag applies to all the refs that are pushed, so with push.default set to matching, or with multiple destinations configured through remote.*.push, it can overwrite refs other than the current branch, including local refs that are strictly behind their remote counterpart.

The narrow form avoids this. A + in front of a refspec forces a push to that one branch and nothing else:

git push origin +feature/parser-cleanup

This matters most on machines configured years ago, where push.default may predate the current default of simple. Checking the value costs one command, and it answers whether a bare git push --force is a one branch operation or a repository wide one.

git config --get push.default
git config --get-regexp '^remote\..*\.push$'

An empty answer to both means the default applies and only the current branch is pushed, which is the reassuring result. A configured remote.*.push line is the one to read carefully, because it is the mechanism that turns one typed command into several ref updates, each of which the force flag applies to.

When the remote refuses anyway

Even a correctly formed force push can be rejected by the server, and the reasons are worth knowing in advance rather than discovering at the end of a rebase.

Branch protection blocks it by default. The documentation for protected branches states that each protection rule disables force pushes to the matching branches and prevents those branches from being deleted, and that when force pushes are enabled, the permission can be granted either to everyone with write access or to specific people and teams. Enabling them does not switch off the other rules: a branch requiring a linear commit history still refuses a force pushed merge commit.

The same documentation lists what the block is protecting against, which reads like a description of the bad case: a force push may remove commits that other collaborators based their work on, people may get merge conflicts or corrupted pull requests, and the mechanism can be used to point a branch at commits that were never approved in a pull request.

There is one legitimate case where the documentation points at a force push rather than away from it. On a branch that requires signed commits, a contributor who pushed an unsigned commit has to rebase the commit so that it carries a verified signature, and then force push the rewritten commit to the branch. The rewrite is the fix, and the force push is how the fix arrives.

On a self hosted or bare repository, the equivalent switch is receive.denyNonFastForwards. When it is set to true, the receiving side denies any ref update that is not a fast forward, even if the push is forced, and the setting is applied when initialising a shared repository. A force push that fails against such a remote has not half succeeded, so there is nothing to clean up.

Getting back what a force push overwrote

An overwritten branch is usually recoverable, and the window is measured in weeks rather than minutes.

Every ref update in a clone is recorded in that clone's reflog, which is why HEAD@{2} means where HEAD was two moves ago and main@{one.week.ago} means where main pointed a week ago in this repository. A rebase or a reset also sets ORIG_HEAD to the tip of the branch before the operation, which makes the first recovery attempt short:

git reflog show feature/parser-cleanup
git reset --hard ORIG_HEAD
git branch rescue 9f2c1ab

Creating a branch at the old hash, rather than resetting, is the safer first move, because it makes the lost work reachable again without changing where the current branch points.

The limits are set by expiry. Reflog entries older than 90 days are removed by default, and entries that are not reachable from the current tip are removed after 30 days, a deliberately shorter window because those entries are the ones produced by git commit --amend and by rebases. The recovery has to happen while the entries exist, and it has to happen in a clone that holds them, since reflogs are local and are never pushed. If the rewrite happened on a colleague's machine, their clone has the reflog entry and yours does not.

Where the risk actually comes from on a Mac

Almost every damaging force push has the same shape: the wrong branch was checked out, or the shell was pointed at a different repository than the file list on screen. The command was correct for the branch it was meant for, and that branch was not the current one.

That is a layout problem rather than a Git problem. When the folder listing, the shell and an assistant share one working directory in a single window, the branch being pushed and the files on screen cannot disagree, because there is only one working directory in view. What that arrangement covers is set out on the Features page, and how it compares with dual pane browsers and terminal first tools is described on Compared with other file managers.

What to change first

Replace --force in muscle memory with --force-with-lease --force-if-includes, and set it as an alias so the safe form is the one that gets typed under pressure. If the underlying risk is that the shell and the file list are pointing at different repositories, that is the thing to fix, and Atriens is one example of that shape.

Frequently asked questions

Is git push --force always dangerous?

It is dangerous whenever someone else may have fetched the commits being replaced, and harmless when nobody has. The manual describes it as a method reserved for a case where losing history is the intention, which fits a private branch after a rebase or an amend and fits nothing that other people are building on.

What is the difference between --force and --force-with-lease?

--force performs no check at all, while --force-with-lease updates the remote ref only if it still holds the value the local clone expects. If someone pushed in the meantime, the lease has expired and the push fails instead of erasing their work. It is the same operation with a safety check attached.

Can a force push be undone?

Usually yes, from the clone that performed the rewrite. git reflog lists the previous positions of the branch, ORIG_HEAD points at where it was before the last rebase or reset, and creating a branch at the old hash makes the lost commits reachable again. Reflog entries expire, with defaults of 90 days for reachable entries and 30 days for unreachable ones.

Why is the force push rejected on the main branch?

Protected branches block force pushes by default, so the rejection comes from the server rather than from Git. Allowing them is a repository setting that can be granted to everyone with write access or to named people and teams, and enabling it does not switch off other rules such as requiring a linear history.

Do you need to force push after a rebase?

Yes, if the branch was already pushed, because a rebase replaces commits with new ones and the branch is no longer a descendant of what the remote holds. Use git push --force-with-lease for it, and prefer rebasing branches that nobody else has pulled so the force push is a private operation.

Back to all posts