If you are frequently editing history in git, such as on your local topic branch, force pushing is probably something you are familiar with. And I’d argue there is nothing wrong with that, despite all the flac that force pushing receives, as long as you’re only altering the history of topic branches. I am not suggesting anyone should ever be force pushing to main or master. That said, there are some gotcha’s you should be aware of if this is a typical part of your workflow.

What does it mean to force push?

By default, push is an additive operation. Meaning it will succeed only if it’s appending to the history upstream. To “force” a push is to override this behaviour, allowing you to remove or change commits that already exist upstream.

The refspec

The syntax of git push (which you can verify by reading man git-push) is as follows:

$ git push [<opts>...] [<repository> [<refspec>...]]

NOTE: the square brackets here denote optional arguments

You can read more about the refspec in the git book, but the general syntax looks like so:

[+][<src>][:<dest>]

The optional “plus” does two things here:

  • it implies that the given refspec will be pushed forcefully
  • it indicates that you intend to create or update, not delete

<src> if often the branch you want to push, but can be arbitrary “SHA-1 expression”, and can even be empty if you wish to delete a remote branch:

$ git push <remote> :<remote-ref-to-delete>

<dest> is the remote ref you would like to push to, often a branch. If left empty it will default to the ref that <src> is configured to point to, or to the same ref as <src>. In most workflows this will mean

$ git push <remote> topic-branch

will have the same effect as

$ git push <remote> topic-branch:topic-branch

Finally if you were to use the special refspec, :, this would tell git to push all local refs that have a remote counterpart. Likewise, +: would have a similar effect except it would force push the same branches.

simple vs matching

In release 2.0 of git, the default setting for push.default was changed from matching to simple. The behaviour for matching is where things start to get scary. For example:

$ git config push.default matching
$ git push

Has a similar behaviour to:

$ git push <remote> :

And will push all matching branches. Which means the following:

$ git config push.default matching
$ git push --force

Has similar behaviour to:

$ git push <remote> +:

It will force push all matching branches. This has potential to be a very destructive operation as, if you don’t understand the behaviour, you could end up force pushing several branches you didn’t intend. Maybe even main/master.

Meanwhile, simple has the behaviour most of us expect when we execute these commands.

$ git config push.default simple
$ git push

Has similar behaviour to:

$ git push <remote> <current-ref>:

While the forced counterpart:

$ git config push.default simple
$ git push --force

Has similar behaviour to:

$ git push <remote> +<current-ref>:

--force-with-lease

Finally we have another option available to us, --force-with-lease. This is similar to --force, except it will first check to verify that the remote ref is in the same state it was when we last performed git fetch (which also occurs when we pull). If the remote ref has changed, our push will be rejected. This can be helpful if other people are also pushing to your branch to avoid overwriting their changes, or potentially your own if you work across multiple machines.

Conclusion

Personally, I mostly use git push <remote> +<src>. Why?

  1. I tend to work with multiple remotes frequently, so forcing me to write out which one I’m targeting is a helpful extra step to avoid pushing to the wrong one
  2. If I remote into a machine I want to know my normal workflow functions the same, whether or not that machine has v1 or v2 of git installed
  3. Writing out the ref I want to push is helpful in avoiding mistakes. I would never write out git push <remote> +main, whereas I could see myself running git push --force while on main by accident
  4. The additional safety of --force-with-lease is of little value to me, because I rarely (if ever) have others pushing to my branches. Plus that’s a lot of extra characters and I’m lazy1.

That said git push --force-with-lease <remote> <src> is probably the safest convenient option if you can stomach the extra characters. The flag can also be passed a value using --force-with-lease=<refname[:<expect>], which you can learn more about in the manpages:

$ man git-push
$ man gitrevisions

  1. I know I could use git aliases to make this easier, but aliases aren’t available when I ssh into another machine, and I’d rather ensure I understand and remember the underlying commands. ↩︎