Recently I started to notice that on one of my git repositories, any git command I run, I get the following warning:
warning: symbolic ref is dangling: refs/remotes/origin/HEAD
What does this mean and how do I fix it?
Because this particular symbolic ref is not useful anyway, I recommend just deleting it:
git remote set-head origin --delete
If you like having this useless symbolic ref, you can have your Git auto-update it instead:
git remote set-head origin --auto
This will have your Git call up the other Git over at origin
, ask it what its current branch is now, and update your symbolic-ref origin/HEAD
appropriately.
If updating it like this doesn't fix it, don't worry: it's probably not actually broken and will "self heal" eventually. In any case, it is, in my opinion, useless anyway, so even if it is broken, this doesn't hurt anything.
Let's start by noting what a ref is:
A ref, or reference, is a name. There are some restrictions on the name (see the git check-ref-format
documentation, but it's mostly just a string of letters and digits and such, with what we can hope spell out a name that means something to you.
For instance, a branch name is a ref. So is a tag name.
Most of these names actually start with refs/
. The only ones that don't are not always called refs (Git is not completely consistent about this): they're the special names HEAD
, ORIG_HEAD
, CHERRY_PICK_HEAD
, and so on. (They are all either HEAD
itself, which is the most special of all, or end in _HEAD
. They should always be spelled out in all uppercase like this.)
Refs are divided into namespaces. Two that you use all the time are the branch namespace, which is every ref whose spelling starts with refs/heads/
, and the tag namespace, which is every ref whose name starts with refs/tags/
. There is a third one you also use all the time: remote-tracking names are refs whose name starts with refs/remotes/
. After the word remotes
and its trailing slash, Git adds the name of the remote in question, such as origin
, and another trailing slash.
Hence ref is simply the general form of any branch or tag or remote-tracking name, or any other name, in Git. There are some less-frequently-used ones, and internal ones, like refs/stash
for the git stash
command, refs/bisect/
for git bisect
, and so on; normally you don't see these at all.
You do see branch, tag, and remote-tracking names, but Git normally strips off the "namespace" part of the name:
master
is really refs/heads/master
, but Git stripped off the refs/heads/
part.v1.2
is really refs/tags/v1.2
, but Git stripped off the refs/tags/
part.origin/master
is really refs/remotes/origin/master
, but Git stripped off the refs/remotes/
part.Sometimes Git strips only the refs/
part from remote-tracking names. It's not at all clear why, but git branch -a
does this. That's why you see remotes/origin/master
here. Compare with git branch -r
, where you see only origin/master
. In any case, this is a valid way to express each name, because there is a six-step rule for resolving any name you give, as shown in the gitrevisions documentation, and three of the steps involve trying to stick refs/remotes
or refs/heads
or refs/tags
in front of any name you give. We'll get back to this in a moment.
Most refs store exactly one thing: a hash ID. For branch names and remote-tracking names, that hash ID must be a commit hash ID. For tags, it's usually either a commit hash ID, or the hash ID of an annotated tag object, which then stores the commit hash ID. This extra step—going through the tag object—allows you to write an annotated tag, or a tag with a message.
But some refs are symbolic refs. What this means is that instead of storing a hash ID, they store the name of some other ref. The most common one—in fact, the only useful one, in my opinion—is the very special HEAD
ref.1
HEAD
normally contains the name of a branch. That is, if you look at the actual file .git/HEAD
, you'll see, e.g.:
$ cat .git/HEAD
ref: refs/heads/master
In other words, the name HEAD
simply holds the branch name master
. What this means is that master
is the current branch. This is how Git implements an attached HEAD. Hence a detached HEAD is just the case where HEAD
holds a commit hash ID instead of a branch name. That's really all there is to attached vs detached HEAD. Git just asks itself: Does HEAD
hold a branch name? If so, that branch is the current branch and the commit hash ID stored in that branch name is the current commit. If not, HEAD
holds a commit hash ID, and that hash ID is the current commit.
1HEAD
is very special because, among other things, if the file goes missing, Git will stop believing that the repository is a repository! Never remove .git/HEAD
. If, after a computer crash, you need to recover a repository whose HEAD
file has gone missing, you can try creating one containing ref: ref/heads/master
and see if that allows you to proceed. This happens surprisingly often after a crash because HEAD
is such a relatively active file, and one method computer systems use to recover after a crash is simply to remove any damaged file.
In general, a ref must resolve to a commit or other hash ID, for some valid, existing, internal Git object.2 For instance, a branch name like master
is only allowed to exist if it holds the hash ID of some existing, valid commit.
If a ref holds the name of some other ref, that other ref needs to exist, and resolve to a valid hash ID.3 If the other ref doesn't exist, though, we can say that the symbolic ref is dangling.
That's what this means:
warning: symbolic ref is dangling: refs/remotes/origin/HEAD
Your existing refs/remotes/origin/HEAD
is a symbolic ref—all remote-tracking remote/HEAD
names are symbolic refs after all—but whatever name it contains, that name doesn't exist.
Note: There is a situation in which your own HEAD
is a dangling symbolic reference. This occurs normally in a new, totally-empty repository. HEAD
must exist, so it does. It should contain the name of a branch, so it contains ref: refs/heads/master
. It is a symbolic ref, saying that you are on branch master
. But this is a new, totally-empty repository. There are no valid commits in it! The branch name master
cannot exist, because a branch name is required to hold the hash ID of a valid, existing commit, and there are none. So master
itself, the branch name, simply doesn't exist. This makes HEAD
a dangling symref.
Making the first commit creates the name master
, resolving the situation. So it resolves all on its own, and there's nothing wrong with this situation. Hence a dangling symbolic ref is not harmful. This is especially true of HEAD
(where Git won't complain about the dangling symbolic ref), but it's true of refs/remotes/origin/HEAD
too: these are all harmless. Unlike the special case for HEAD
, though, they're not useful.
2This is usually a commit, but tags can point to any of Git's four internal object types. They need to be able to point to annotated tag objects, of course, so that you can have annotated tags. Git just takes away all restrictions here though, which lets them point to tree or blob objects too.
3Note that the introduction of symbolic references, which are resolved at reference-look-up-time, also introduces the possibility of loops. The name A might refer to the name B, with the name B referring to C, and then C could refer back to A. How will you resolve this?
Exercise: how does git resolve this? It's relatively easy to detect a self-loop: if the name tumbolia says my answer is obtained by looking up the name "tumbolia", you can immediately see, while looking up "tumbolia", that you'll loop or recurse forever looking yourself up over and over. But what about long chains of names?
Exercise: Your operating system may provide symbolic links or symlinks, which are files whose contents are other files' names. If so, how does your OS prevent or detect symbolic link loops? (Git used to implement symbolic refs via symlinks. This changed when Git added Windows support, since many Windows systems don't offer symlinks.)
Let's go back to the six-step process Git uses for resolving a name that you type in. For instance, suppose you run:
git rev-parse master
Try this out. Try running git rev-parse
with various names, and also with ranges like master..develop
, and with names with operators added, like master~2
. Then go look at the gitrevisions documentation again, and find this:
- If
$GIT_DIR/<refname>
exists, that is what you mean (this is usually useful only forHEAD
,FETCH_HEAD
,ORIG_HEAD
,MERGE_HEAD
andCHERRY_PICK_HEAD
);- otherwise,
refs/<refname>
if it exists;- otherwise,
refs/tags/<refname>
if it exists;- otherwise,
refs/heads/<refname>
if it exists;- otherwise,
refs/remotes/<refname>
if it exists;
Three of these five rules make it possible to just write master
or v1.2
or origin/master
. Rule #5 takes care of origin/master
: since refs/heads/origin/master
exists, if you run git rev-parse origin/master
and rules 1 through 4 don't work, rule 5 works and that's where git rev-parse
stops with a valid commit hash ID. So that's what git rev-parse
prints.
Rule #4 handles branch names. Rule #3 handles tag names. Note that rule #3 means that if you create a tag named master
, the tag overrides the branch, in most cases.4 If you're in this situation, you can make use of rule #2, by writing heads/master
or tags/master
.
But that leaves out one last rule. Here it is:
- otherwise,
refs/remotes/<refname>/HEAD
if it exists.
This particular rule means that you can run:
git rev-parse origin
Git will try the other five steps, all of which will fail, and then will try parsing origin
as if you'd written refs/remotes/origin/HEAD
. If this name exists and can be resolved to a hash ID, that's the hash ID you will get.
What all this boils down to in the end is that you can use the name of a remote as shorthand for the value of the remote-tracking name that my Git has in its symbolic ref under that remote's HEAD
. That is, if you have remotes named r1
, r2
, and r3
, you can use the name r2
instead of writing out refs/remotes/r2/HEAD
(which works without needing the six-step process) or r2/HEAD
(which works by using step #5).
How often do you, or anyone, actually use this? That's how useful these symbolic refs are. No one I know uses it, ever, so as far as I can tell, it's not useful.
4Commands that assume, or first try, a name as a branch name will pick the branch meaning instead. For instance, git checkout master
will check out the branch. But git rev-parse master
doesn't assume a branch name, so it parses the tag, finding it in step 3 and never moving to step 4.
It's therefore unwise to use the same name as both a branch and tag name. Git has rules that will pick one or the other, but they may not match your personal expectations!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With