Home > Software design >  How is it possible that I can checkout a non-existant branch in Git?
How is it possible that I can checkout a non-existant branch in Git?

Time:11-23

There is a lot I don't know about Git. I hope to learn how this can happen:

  • 2 month ago I had an active branch: feature/branch-1
  • The branch was merged into my default branch: develop
  • It was removed in the remote repository (bit-bucket)
  • It was removed from the local file system: git branch -d feature/branch-1
  • git branch I don't seen the branch
  • Today I was surprised to find that I could do: git checkout feature/branch-1
  • git branch I can see the branch

Is git finding out the merge point (when the branch was merged) and checking out that commit?

CodePudding user response:

Both git checkout and git switch (which in this case do the same thing) have built into them a special feature. The way they handle the request to switch to a string that resembles a branch name name is this:

  1. Check to see if the given string, e.g., foo or feature/branch-1 or 5a73c, already exists as a branch name. If so, that's the name of the branch to check out.

  2. Check to see if the given string, e.g., 5a73c, can be turned into a valid hash ID for a commit. If so, that's the commit to check out, as a detached HEAD. (Here git checkout will just do that, and git switch will die with a fatal error unless you used --detach, in which case it's fine and will just do that too.)

  3. Assuming we've gotten this far, if --guess is in effect (see below), use the guessing code. In older versions of Git this is called "DWIM mode", where DWIM stands for Do What I Mean. (DWIM has a long history going back to Lisp in the 1960s, even before I used computers: I didn't start messing with hardware and then software until the 1970s.)

The --guess option first became formal (and properly documented) in commit ccb111b342f472d12baddbfa5b5281, first released in Git 2.23.0, but since it defaults to on and was always there before that, is on unless you explicitly turn it off, which requires a Git version of at least 2.23. So it's almost always on.

The way it works is to scan each of the remote-tracking names in your own repository. These names are created and updated at git fetch time, including most of the git fetch operations run by git pull operations. They are, by default, not deleted unless you explicitly run git fetch --prune or git remote prune, or for the special case when you specifically use git push to delete a branch from a remote for which you currently have a corresponding remote-tracking name.

Your remote-tracking names are the names that resemble, e.g., origin/foo or origin/feature/branch-1. You're unlikely to have an origin/5a73c since nobody would use that as a branch name: your remote-tracking names are your Git's copies of someone else's branch names, and the someone else would be crazy1 to use that as a branch name. But it can happen by accident with an occasional four or more letter word2 that's made up entirely of valid hexadecimal digits: branch names such as deed or efface or faded can trigger weirdness here.

In any case, assuming we get into step 3—the --guess code—in the first place, Git scans your remote-tracking names. You typed in, e.g.:

git checkout feature/branch-1

when you have no feature/branch-1 branch, so step 1 failed; feature/branch-1 cannot be turned into a valid hash ID because it contains non-hexadecimal characters such as t and the forward slash; and so we reach step 3. Git now scans all your origin/* names: is one of them origin/feature/branch-1?

In this case: yes, one is. Git will also scan all other remote-tracking names, such as upstream/*, at this point, to find all candidates. The list of all such candidates then enters one last set of tests:

  • Is the list empty? If so, the guessing fails.

  • Is the list exactly one element long? If so, that's the remote-tracking name you want Git to guess.

  • Otherwise (more than one entry in the list), the guessing fails due to the contest between the matches, unless you use a feature introduced in Git 2.19), checkout.defaultRemote. This feature lets you pick a particular remote that "wins" such contests.

In this case, you got exactly one match: origin/feature/branch-1. That enabled --guess to guess that rather than:

git checkout feature/branch-1

you meant:

git checkout -b feature/branch-1 --track origin/feature/branch-1

and so that's what git checkout did. (While git switch spells this with -c, git switch behaves the same way here, using the same control knobs: --guess on the command line and checkout.defaultRemote for handling ambiguous multiple matches.)

One potential lesson here is that it may be wise to run git fetch -p or git remote prune often, or even set fetch.prune to true in your personal Git configuration. Otherwise you can have a lot of stale remote-tracking names, and with people being people, names you invent for your new feature might collide with some old name someone invented for their new feature. Or, instead of that lesson, perhaps the one to take is to disable guessing (with the new-in-Git-2.30 config.guess setting, perhaps). Note that if you want to use remote-tracking name origin/foo to create local branch foo, you can type in:

git switch -t origin/foo

(the -c foo part is implied). This works with the old git checkout too, of course.


1There may be a method to their madness, or perhaps just a madness to their method.

  • Related