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:
Check to see if the given string, e.g.,
foo
orfeature/branch-1
or5a73c
, already exists as a branch name. If so, that's the name of the branch to check out.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. (Heregit checkout
will just do that, andgit switch
will die with a fatal error unless you used--detach
, in which case it's fine and will just do that too.)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.