On current Git, is there a material difference between git push --force-with-lease origin +somebranch
, git push --force-with-lease origin somebranch
This is a good question; the documentation is a bit ambiguous, and the source is very confusing (the actual application of force flags is widely scattered).
One answer is clear enough. Here is what the git push documentation says, with my boldface added:
--[no-]force-with-lease
--force-with-lease=
--force-with-lease=
: Usually, "git push" refuses to update a remote ref that is not an ancestor of the local ref used to overwrite it.
This option overrides this restriction if the current value of the remote ref is the expected value. "git push" fails otherwise.
Imagine that you have to rebase what you have already published. You will have to bypass the "must fast-forward" rule in order to replace the history you originally published with the rebased history. If somebody else built on top of your original history while you are rebasing, the tip of the branch at the remote may advance with her commit, and blindly pushing with --force will lose her work.
This option allows you to say that you expect the history you are updating is what you rebased and want to replace. If the remote ref still points at the commit you specified, you can be sure that no other people did anything to the ref. It is like taking a "lease" on the ref without explicitly locking it, and the remote ref is updated only if the "lease" is still valid.
--force-with-lease alone, without specifying the details, will protect all remote refs that are going to be updated by requiring their current value to be the same as the remote-tracking branch we have for them.
--force-with-lease=
, without specifying the expected value, will protect the named ref (alone), if it is going to be updated, by requiring its current value to be the same as the remote-tracking branch we have for it. --force-with-lease=
: will protect the named ref (alone), if it is going to be updated, by requiring its current value to be the same as the specified value (which is allowed to be different from the remote-tracking branch we have for the refname, or we do not even have to have such a remote-tracking branch when this form is used). Note that all forms other than --force-with-lease=
: that specifies the expected current value of the ref explicitly are still experimental and their semantics may change as we gain experience with this feature. "--no-force-with-lease" will cancel all the previous --force-with-lease on the command line.
Hence, if the compare-and-swap option1 is supported by the transport, and you have written --force-with-lease
and not --no-force-with-lease
, then all updates, forced or not, use the lease mode.
The --no-force-with-lease
, however, clears out stored-up push_cas_option
structure, and it's not immediately obvious to me when those stored-up value are applied to each refspec.
Using an explicit
also clearly protects only the one reference, regardless of any force flag set for it.
Precisely what happens when the underlying transport lacks support for compare-and-swap is also not clear to me. Fortunately GitHub's Git servers support it, making this just a distraction if you're specifically referring to GitHub.
1Internally, the Git source code uses the macro CAS_OPT_NAME
: the function of force-with-lease is inspired by modern CPUs' compare-and-swap instructions, which atomically test whether some variable2 is set to a predicted value, replacing it with a new value if so, and also returning, in some form, the actual value found in the variable.
This may set condition codes, if the CPU architecture uses condition codes, but in most if not all cases you get the old value so that you can retry the compare-and-swap if appropriate. For instance, to implement atomic add-one, you can loop with: load r1,(r0); label: add r1,1,r2; cas r1,r2,(r0); bne label
; to implement atomic-test-and-set of bit 2: load r1,(r0); label: or r1,4,r2; cas r1,r2,(r0); bne label
; and so on. This method is used on Intel Pentium and SPARC systems, for instance.
Some CPUs use the cache machinery instead. If the closest-to-CPU cache has shared vs exclusive modes (e.g., MESI or MOESI) we can use a "load linked" or "load locked" instruction followed by a "store conditional" instruction. The conditional store succeeds only if the cache line is still exclusively owned by the current CPU. In this case, we must re-do the initial locked-load of the variable, and our loops look more like: label: ll r1,(r0); add 1,r1; sc (r0),r1; bne label
. This is used on PowerPC and MIPS architectures.
2Typically the variable in question is a memory location, often with alignment constraints, even on CPUs that nominally support unaligned memory. For instance, on the Intel Haswell, a compare-and-swap-8-byte instruction will run to completion on a 4-byte boundary, but it will not actually be atomic. I discovered this the hard way when a colleague's memory allocator provided only 4-byte alignment. :-)