There are various types of refs in git, some of the most common of which are branches (stored in .git/refs/heads
), remote-tracking branches (.git/refs/remotes
), and tags (.git/refs/tags
).
But it's also possible to create and use arbitrary non-standard refs that live elsewhere under .git/refs
. This can be useful for storing custom metadata in the repository that you don't expect users will want to interact with directly. For example, GitHub uses these kinds of refs to expose references to pull request branches, and the Emacs git client Magit uses them to save uncommitted changes periodically, when the appropriate setting is enabled. Such refs would generally need to be manipulated using the so-called "plumbing" commands of git, since the user-facing "porcelain" commands don't know about or support them.
I was playing around with non-standard refs using the plumbing command git update-ref
and found some odd behavior:
$ git init foo && cd foo
$ touch a && git add a && git commit -m init
$ tree .git/refs
.git/refs
├── heads
│ └── master
└── tags
2 directories, 1 file
$ git update-ref refs/foo/bar/baz HEAD
$ tree .git/refs
.git/refs
├── foo
│ └── bar
│ └── baz
├── heads
│ └── master
└── tags
4 directories, 2 files
$ git update-ref -d refs/foo/bar/baz
$ tree .git/refs
.git/refs
├── foo
├── heads
│ └── master
└── tags
3 directories, 1 file
When I created the ref refs/foo/bar/baz
, git update-ref
created the necessary parent directories. When I deleted the ref, it was smart enough to remove the parent directory bar
, which had now become empty. However, it wasn't smart enough to remove the "grandparent" directory foo
, which was also now empty after removing bar
.
Is this a bug?
No, it's by design. Here is a comment from the source code:
/*
* Remove empty parent directories associated with the specified
* reference and/or its reflog, but spare [logs/]refs/ and immediate
* subdirs. flags is a combination of REMOVE_EMPTY_PARENTS_REF and/or
* REMOVE_EMPTY_PARENTS_REFLOG.
*/
static void try_remove_empty_parents(struct files_ref_store *refs,
const char *refname,
unsigned int flags)
{
If I'd have nested my non-standard ref one level deeper, e.g. refs/foo/bar/baz/xyzzy
, I'd have noticed that both the parent and grandparent directories were removed, but the great-grandparent wasn't, making it more obvious that this behavior is intentional.
I guess the idea is that the top level subdirectories under .git/refs/
(like foo
in my example) represent a type of ref rather than being part of the name of the ref, so treating them differently from directories further down the tree (like bar
in my example) makes sense.