Safer Git Force Push with "Lease"

17.05.24    tools and tricks    version control

Git is a version control system and thus its primary purpose is to manage multiple states of a file hierarchy and keep them safe in the history. This is why normally the push command only allows sending purely additive changes to the history of a branch. Nothing is deleted and everything already committed to history is immutable.

When using advanced workflows involving history rewriting features1 like rebase or squash, sometimes a force push becomes necessary: By adding the --force flag to push, the aforementioned safety mechanism is disabled. Otherwise, the history rewriting effects could not be pushed to the destination. But this disables the safety completely and that becomes dangerous once one more than one person works on the repository (i.e. the case in the majority of situations).

Here is an example: Let's say Anna works on here branch A and pushes it to the shared repository. Bob finds a typo in Anna's work, fixes it and pushes a commit to the A branch. Anna has not yet pulled the commit. Instead, she decides to rebase her A branch onto the main branch. To push the branch, she then uses the --force option because without it, she would not be able to push. This silently drops Bob's fix from the history. Neither Bob nor Anna are made aware of something peculiar happening, but Bob's change cannot be found in the repository anymore2.

Fortunately, git offers a solution for this situation; a middle ground between the simple push and the dangerous push --force. The push command has an alternative option --force-with-lease. Like --force it allows pushing a rewritten history. But it introduces an alternative check: The new history is only pushed if the branch on the shared repository is in the same state as it was during last fetch / pull. If this is not the case, the push is cancelled. This way you cannot overwrite and thus delete parts of history which you have not yet seen.

In the example from above the problem would be avoided if Anna had used --force-with-lease instead of --force. Keep in my that Anna has not called pull in the meantime3. Her push with --force-with-lease would have cancelled making her aware of Bob's changes. If Anna calls pull first, she is made aware of the diverging history between her version of the A branch and Bob's. In either case no change is overwritten silently. Anna then might make the conscious decision to overwrite Bob's changes if she so wishes.

In my experience the vast majority of force pushes can be restricted to this safer option. For the goal of being safe by default and dangerous only when necessary, --force-with-lease should be preferred. The problem is the very verbose name of the option; the safer option should be easier to use. This is why I have defined my own alias:

# Define alias "fpush"
# It is not a prefix of "push" as "pushf" would be,
# so "push" doesn't get autocompleted into "pushf" by accident.
git config --global alias.fpush "push --force-with-lease"

I think this provides a nice balance where you usually use push; you switch to fpush (with some caution) if necessary; and you only escalate to push --force if you know what you are doing.


  1. This necessity for unsafe operations is why history rewriting itself is often shunned. In Mercurial (and other version control systems) history is commonly considered "sacred" and changes to shared commits are prohibited. Mercurial has a special mechanism to identify unshared history to restrict rewrites to only those commits. 

  2. Of course not all hope is lost yet. There is the reflog feature, which will preserve the lost commit for some time. But this is a separate topic. 

  3. It is important to note that Anna has made no mistake by not pulling. The time frame for the relevant pull between Bob's push and hers might have been seconds.