0

I'm new to Git and GitHub. I just began learning. This most probably is a very dumb problem, and I'm sorry for asking such a question. So, here's my problem --

I'm confused about what <refname> and <expect> mean in the following commands:

  1. git push --force-with-lease=<refname>
  2. git push --force-with-lease=<refname>:<expect>

I know that git push --force-with-lease, on default/without any specified parameter, will refuse to update branch unless the remote-tracking branch (local branch) and the remote branch points to the same commit references.

But, I'm unable to understand how git push --force-with-lease works when these parameters are used with it since I don't understand what these parameters exactly mean.

So, I would really appreciate it if someone could demonstrate some examples of the use of the command with these parameters and also explain what these parameters exactly mean.

LeGEC
  • 46,477
  • 5
  • 57
  • 104
Fateen
  • 9
  • 8
  • 1
    First off, it's not `force-with-leash` - that option does not exist, typo? Second, did you read the [documentation](https://git-scm.com/docs/git-push#Documentation/git-push.txt---no-force-with-lease)? – fredrik Dec 04 '20 at 07:34
  • @fredrik I rather like "force with leash". Force it, but keep it on a leash. They should change it to this! – matt Dec 05 '20 at 00:42
  • @fredrik yes, sorry that was a typo. – Fateen Dec 05 '20 at 07:56

2 Answers2

2

VonC's answer shows a way to use --force-with-lease with the expect part; here are the technical details. Note: internally, the name for "force with lease" is "CAS", which is short for Compare And Swap. CAS instructions in computer programming (assembler languages, usually) are one way that CPUs implement atomicity. If you're familiar with the use of CAS instructions, the rest should be immediately obvious.

The --force flag itself, and --force-with-lease, are relatively simple to explain. Normally, a git push operation ends with one or more sets of instructions of the form:

  • Please set reference _______ (fill in the blank with a reference name) to _______ (fill in the blank with a raw hash ID). Let me know whether this request was accepted or rejected.

The --force-with-lease option changes these requests to one of the form:

  • I believe refname _______ (fill in the blank with a reference name) is currently set to _______ (fill in the blank with a raw hash ID). If so, set it to _______ (fill in the blank with a raw hash ID)! But if not, reject this attempt. Let me know whether this request was accepted or rejected.

The --force option is simpler: rather than expanding the request as above, it just changes the please part to a forceful command:

  • Set reference _______ (fill in the blank with a reference name) to _______ (fill in the blank with a raw hash ID)! Let me know if that worked.

So these are the various kinds of requests or commands that git push can send, at the end, to finish off the push operation.

What goes in the blanks

A reference name or refname is simply a branch name, or tag name, or other such name. Internally, it's best to spell these out completely: a branch name B is spelled out as refs/heads/B, while a tag name T is spelled out as refs/tags/T. The git push command itself can fill in the refs/heads/ part if you leave it out, by realizing that some name like main or master or topic is a branch name, not a tag name, for instance.

A hash ID is one of those big ugly hexadecimal numbers you see in git log output, such as faefdd61ec7c7f6f3c8c9907891465ac9a2a1475. For our purposes (setting and/or checking branch names) these are always commit hash IDs (but there are three other types of internal Git object).

A normal, un-forced git push, which sends over one of those "please, if it is OK" kind of requests, requires that the new value for some existing branch name simply "extend the branch", as it were. Technically the requirement is that for each request, the new commit must be a descendant of the current commit, as found by that branch name. Such a request is a fast-forward, in Git terminology, and will be accepted.

A non-fast-forward occurs when this kind of request does not meet the descendant test. To make the Git that is receiving the push request actually execute such a request, it must take the form of a forceful command, without the default "please, if it is OK" part. Using --force executes just such a command. The receiving Git can still reject it, but will not reject it simply because it is not a fast-forward.

The danger in using a plain force operation is that you might assume that the receiving Git repository has some particular set of commits. That is, suppose the receiving Git has, on December 1st, these commits:

...--G--H   <-- main

That is, the last commit in their main branch has hash ID H. But there is a bug in commit H. So, several days later, we decide we should remove commit H. We send them a forceful command: Set your main branch to remember commit G, instead of commit H! We believe this will result in them having:

       H   ???
      /
...--G   <-- main

Commit H will be pushed aside and eventually forgotten entirely (deleted from the repository). But here it is December 4th, not December 1st, and unbeknownst to us, their repository now looks like this:

...--G--H--I--J   <-- main

Commit I fixes the bug in commit H, and commit J adds a crucial new feature. We now send the forceful command to them and they obey, resulting in:

       H--I--J   ???
      /
...--G   <-- main

The bug is gone, but so is the crucial new feature. Now everyone gets mad because we destroyed the feature. If we didn't pick up commit J, and nobody else has it, and the Git repository that received our forceful push command cleans out forgotten commits quickly, that commit may be gone entirely by the time we realize our mistake.1 We may have to spend days reproducing the feature.

Using --force-with-lease, however, we can make our forceful update safe. Instead of simply telling them set your main to G, we send them a forceful request that starts with I think your main identifies H. Since it doesn't identify H, the forceful command simply fails. We then run git fetch to obtain the actual information from their repository, discover that commits I and J exist, discover that I fixes the bug,2 and discover that J adds the important feature.

Hence, --force-with-lease is never any worse than plain --force, and usually much better. It would be better if Git only had the "with lease" version of --force in the first place. But it was invented long after Git was popular, so for backwards compatibility, it is not the default.

You can spell --force-with-lease without an optional =<refname>:<expect>. If you do so, Git will fill in the refname from the next argument, and fill in the <expect> part from the remote-tracking name that goes with the refname you use. Sometimes, as VonC noted, your system may behave badly (cough Microsoft VSC cough) in the name of "convenience", and make the expect part important to use manually.


1This is why having multiple copies of the repository, and/or backups, can be a good idea. Since Git is distributed, there are usually and automatically multiple copies of the repository, on different machines. See if you can find the lost commits on one of those machines.

2If we discover that commit I does not fix the bug, we can now come up with some commit K that adds on to J that does fix the bug, rather than simply removing H entirely. We can then git push this new commit without --force.

Note that we could, all along, have invented a commit I that fixed the bug but added on to H, and pushed that. Such a commit is called a revert in Git, and adding a revert, rather than subtracting a commit, is often the way to go. That's because Git is built to add new commits, not to take some existing ones away. Taking commits away can only be a good idea when nobody depends on those commits (and even then there are those who argue against taking commits away).

torek
  • 448,244
  • 59
  • 642
  • 775
  • `CAS`: "Compare and Swap": I forgot to mention it indeed. I did look it up in https://stackoverflow.com/a/52937476/6309: "name which nobody liked because it was too technical.". Then "`lockref`". Then finally "`force-with-lease`". – VonC Dec 05 '20 at 00:40
  • Thank you very much! Your detailed answer with reference to @VonC 's example made it a lot simpler and very understandable. I've just one more confusion -- When I use `git push --force-with-lease` without an optional `=:`, is the `` part filled with the local repo's base or the latest commit by default, and then that particular commit (base/latest commit) will go with the refname? If not, then what is the correct process? – Fateen Dec 05 '20 at 07:53
  • Yes. To take an example (which is probably the easiest way to show how it works): `git push --force-with-lease origin master` will send, to `origin`, a request for them to update their `master` to the hash ID you get with `git rev-parse master`, using the expected hash ID you see if you run `git rev-parse origin/master`. That is, the "expect" part gets filled in from your remote-tracking name (`origin/master`) corresponding to the branch name (`master`) that you're asking them to set. – torek Dec 05 '20 at 08:38
1

I mention the --force-with-lease=<refname>:<expect> syntax in "push --force-with-lease by default"

The main issue if you don't provide an expected value is that editors like Microsoft's VSC have a feature to auto-fetch in the background, which bypasses the protections offered by --force-with-lease & --force-with-lease=<refname>

That is especially the case when you edit files from multiple repositories, which means multiple remotes "origin" (one per repository)

As an example, to be extra-safe:

git fetch              # update 'master' from remote
git tag base master    # mark our base point
git rebase -i master   # rewrite some commits
git push --force-with-lease=master:base master:master

That will

  • create a base tag for versions of the upstream code that you've seen and are willing to overwrite,
  • rewrite history,
  • force push changes to master if the remote version is still at base, regardless of what your local remotes/origin/master has been updated to in the background.
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • Thank you! If it's okay, can you please clarify the function of `master:master` in `git push --force-with-lease=master:base master:master`? Particularly, what's `master:master`'s role in `git push --force-with-lease :`? – Fateen Dec 05 '20 at 07:51
  • @Fateen `master:master` is for asking `git push` to replace the remote `master` history with your local rewritten `master` (provided the remote `master` is still based on "`base`", thanks to the `--force-with-lease=master:base`) – VonC Dec 05 '20 at 11:55