1. Introduction
The Git versioning system uses a hierarchical organization of data in many cases. For instance, although branches, tags, and other refs (references) often exist separately from one another, we can organize them with a path-like syntax.
In this tutorial, we explore the use of the / forward slash character in branch and other ref (reference) names. First, we briefly refresh our knowledge about UNIX paths and link those to refs. After that, we see ways to list basic Git refs. Next, we turn to nested refs and how they get constructed. Finally, we explore pitfalls that relate to Git ref naming and structuring.
We tested the code in this tutorial on Debian 12 (Bookworm) with GNU Bash 5.2.15 and Git 2.39.2. Unless otherwise specified, it should work in most POSIX-compliant environments.
2. Paths
UNIX and Linux paths comprise elements separated by the / forward slash character:
/root/subpath/subsubpath/[...]
For obvious reasons, the only forbidden parts of these element names are the / slash and NULL characters. In particular, one is the separator, while the latter is used as a string terminator and enables safer path processing.
Although other operating systems leverage different approaches, the UNIX and Linux path standard has remained a standard in most portable applications.
There are several benefits to the use of paths in general:
- structure
- relation
- hierarchy
Further, UNIX and Linux paths enable the use of virtually any character within the elements.
This is the reason for its widespread use in most universal resource identifier (URI) schemes.
3. List Simple Git Refs
Continuing the idea of universality and structure, Git employs slashes for branch names and references in general.
Let’s see an example:
$ git show-ref | awk '{ print $2; }'
refs/heads/branch1
refs/heads/branch2
refs/heads/branch3
refs/heads/master
refs/tags/v0.1
Here, we use the show-ref subcommand to list all references and pipe to awk, so we only extract the second column that contains the ref paths.
Another view of the same ref structure can be seen on the filesystem:
$ tree .git/refs
.git/refs/
├── heads
│ ├── branch1
│ ├── branch2
│ ├── branch3
│ ├── master
└── tags
└── v0.1
4 directories, 6 files
Notably, all refs start at the refs root path. Under that, heads contains each branch head commit, while tags has all tags.
Considering the fact that refs/heads/ and refs/tags/ are default paths in any Git repository, we can ignore these first two parts of each ref:
$ git show-ref | awk '{ sub(/^[^/]*\/[^/]*\//, "", $2); print $2; }'
branch1
branch2
branch3
master
v0.1
In this case, we leverage the gsub() function of awk to remove the first two elements of each path.
Thus, we end up with fairly simple names. However, this isn’t always the case.
4. Nested Git Refs
To manually nest Git refs, we use / forward slashes as parts of their names:
$ git branch mini-branches/sub-branch-1
$ git show-ref | awk '{ sub(/^[^/]*\/[^/]*\//, "", $2); print $2; }'
branch1
branch2
branch3
master
mini-branches/sub-branch-1
v0.1
At this point, we have a branch with the name mini-branches/sub-branch-1.
Let’s check the structure of .git/refs now:
$ tree .git/refs/
.git/refs/
├── heads
│ ├── branch1
│ ├── branch2
│ ├── branch3
│ ├── master
│ └── mini-branches
│ └── sub-branch-1
└── tags
└── v0.1
4 directories, 6 files
As expected, sub-branch-1 is distributed under mini-branches in the refs hierarchy.
Of course, we can have several branches with the same prefix:
$ git branch mini-branches/sub-branch-2
Now, sub-branch-1 and sub-branch-2 are both at the same level below mini-branches.
In addition, we can expand the structure with another branch container:
$ git branch mini-branches/sub-sub-branches/b1
$ tree .git/refs/
.git/refs/
├── heads
│ ├── branch1
│ ├── branch2
│ ├── branch3
│ ├── master
│ └── mini-branches
│ ├── sub-branch-1
│ ├── sub-branch-2
| └── sub-sub-branches
│ └── b1
└── tags
└── v0.1
5 directories, 8 files
As we can see, mini-branches now contains the sub-branch-1 and sub-branch-2 refs as well as the sub-sub-branches container that holds b1.
Finally, we can see the output of show-ref as filtered by awk:
$ git show-ref | awk '{ sub(/^[^/]*\/[^/]*\//, "", $2); print $2; }'
branch1
branch2
branch3
master
mini-branches/sub-branch-1
mini-branches/sub-branch-2
mini-branches/sub-sub-branches/b1
v0.1
The paths reflect the tree output.
5. Git Ref Path Pitfalls
Although we can use as many slashes as required in Git ref paths, there are some limitations:
- any ref should contain at least one forward slash (usually fulfilled via the refs/ prefix)
- slashes can’t be part of names
- multiple consecutive slashes are forbidden (unless using –normalize)
Critically, Git enforces further restrictions on ref names in general, such as forbidding certain characters and combinations:
- dot prefix
- .lock suffix
- .. and @{
- control characters, Space, ~, ^, :, ?, [, *\*
- * (unless using –refspec-pattern)
In addition, filesystem path length limitations apply as well.
Of course, we have to be careful to not attempt nesting beneath an already-created branch:
$ git branch branch1/sub-branch-1
fatal: cannot lock ref 'refs/heads/branch1/sub-branch-1': 'refs/heads/branch1' exists; cannot create 'refs/heads/branch1/sub-branch-1'
This cannot lock ref error is often the result of incorrect organization of the branch structure and more rarely due to manual edits.
6. Summary
In this article, we talked about Git branches and references and their hierarchy along with ways to expand it.
In conclusion, Git ref structures can be built by naming branches and tags with forward slashes, resulting in hierarchical trees equivalent to those on the filesystem.