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?
- 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
- If I remote into a machine I want to know my normal workflow
functions the same, whether or not that machine has
v1
orv2
of git installed - 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 runninggit push --force
while on main by accident - 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
-
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. ↩︎