2.6 References (branches and tags)

Using the head refs .git/refs/heads we can create named branches. In fact we have done so already, .git/refs/heads/master holds the reference to the latest commit (head) of the master branch.

There is nothing special about the master branch other than convention and that Git treats this as the default branch name in a new repository6.

We can create a new branch very easily, we just create a new .git/refs/heads entry.

1git update-ref refs/heads/test_branch 0715e7 
2git log --stat test_branch 
3git log --stat
git log --stat test_branch
1commit 0715e707b906d30c9e395448ddc9e96acd89d5f7 
2Author: vagrant <vagrant@debian-10.7-amd64> 
3Date:   Wed Mar 10 18:11:12 2021 +0000 
4 
5    Second commit 
6 
7 another_file.txt | 1 + 
8 dir1/file11.txt  | 1 + 
9 file1.txt        | 2 +- 
10 3 files changed, 3 insertions(+), 1 deletion(-) 
11 
12commit f871b58596491e15ee1da91eaf0a4a6c1da3e573 
13Author: vagrant <vagrant@debian-10.7-amd64> 
14Date:   Wed Mar 10 18:07:13 2021 +0000 
15 
16    First commit 
17 
18 file1.txt | 1 + 
19 1 file changed, 1 insertion(+)

PIC

Figure 2.11:New test_branch

We create the new branch named test_branch from the second commit by creating the new refs/heads/test_branch. Now when we log that branch we see only the first and second commit, while logging the current default (‘master’) we see all three commits.

git log --stat
1commit e27aaa8c158e6f261f4c03aaaf173a149ad61d81 (HEAD -> master) 
2Author: vagrant <vagrant@debian-10.7-amd64> 
3Date:   Wed Mar 10 18:13:55 2021 +0000 
4 
5    Third commit 
6 
7file1.txt | 2 +- 
81 file changed, 1 insertion(+), 1 deletion(-) 
9 
10commit 0715e707b906d30c9e395448ddc9e96acd89d5f7 
11Author: vagrant <vagrant@debian-10.7-amd64> 
12Date:   Wed Mar 10 18:11:12 2021 +0000 
13 
14    Second commit 
15 
16 another_file.txt | 1 + 
17 dir1/file11.txt  | 1 + 
18 file1.txt        | 2 +- 
19 3 files changed, 3 insertions(+), 1 deletion(-) 
20 
21commit f871b58596491e15ee1da91eaf0a4a6c1da3e573 
22Author: vagrant <vagrant@debian-10.7-amd64> 
23Date:   Wed Mar 10 18:07:13 2021 +0000 
24 
25    First commit 
26 
27 file1.txt | 1 + 
28 1 file changed, 1 insertion(+)

2.6.1 HEAD

How does Git know that our currently active branch is master? There is a special file in .git called HEAD (we saw this in §2.5) that tells Git where the current default head commit is located. The HEAD therefore indicates which commit object will be the parent to the next commit object created. In this way Git will add the next commit object to the end of the currently active branch.

1cat .git/HEAD 
2cat .git/refs/heads/master
cat .git/HEAD
1ref: refs/heads/master
cat .git/refs/heads/master
1e27aaa8c158e6f261f4c03aaaf173a149ad61d81

Normally, as in this case, HEAD is an indirect reference to one of the refs/heads files, which is in turn a reference to the actual commit hash that we are to use as the current head (the current situation is illustrated in Figure 2.9).

We can change the branch to which HEAD refers (and consequently the branch on which we are working).

1git log --oneline 
2echo "ref: refs/heads/test_branch" > .git/HEAD 
3git log --oneline

I’ve switched to using the --oneline option on log to keep the output short (I don’t think outputting the entire --stat output each time is adding any value here.).

git log --oneline
1e27aaa8 (HEAD -> master) Third commit 
20715e70 (test_branch) Second commit 
3f871b59 First commit
git log --oneline
10715e70 (HEAD -> test_branch) Second commit 
2f871b59 First commit

Before the change log outputs the master branch (HEAD -> master), after changing the content of .git/HEAD log outputs the test_branch (HEAD -> test_branch), we have effectively changed the default branch by changing the ref to which HEAD refers.

PIC

Figure 2.12:Shifting to the test_branch

As with changing refs/heads files, manually editing the HEAD file is not ideal and Git provides the symbolic-ref command to make this safer.

1git symbolic-ref HEAD 
2git log --oneline 
3git symbolic-ref HEAD refs/heads/master 
4git symbolic-ref HEAD 
5git log --oneline
git symbolic-ref HEAD
1refs/heads/test_branch
git log --oneline
10715e70 (HEAD -> test_branch) Second commit 
2f871b59 First commit
git symbolic-ref HEAD
1refs/heads/master
git log --oneline
1e27aaa8 (HEAD -> master) Third commit 
20715e70 (test_branch) Second commit 
3f871b59 First commit

First we view the current value of the symbolic reference HEAD, then we change that reference; note that we specify the path of the actual ref file (refs/head/master).

2.6.1.1 Detached HEAD

You may occasionally encounter a ‘detached HEAD’ error. This seems to cause much confusion online but is actually a very simple issue.

In some circumstances the HEAD symbolic reference will contain a hash value directly (i.e. not a reference to one of the refs/heads). This can arise for a number of reasons, among which the most common are:

  • checkout of a commit directly using its hash
  • checkout of a remote (more on these later)
  • checkout of a tag (which we look at next).

We examine checkout in detail in §??, in the following it is simply a way to ask Git to ‘get’ a commit object’s content and, more importantly, update the HEAD file.

1git checkout f871b5 
2git symbolic-ref HEAD 
3cat .git/HEAD
git checkout f871b5
1Note: checking out 'c1bf'. 
2 
3You are in 'detached HEAD' state. You can look around, make experimental 
4changes and commit them, and you can discard any commits you make in this 
5state without impacting any branches by performing another checkout. 
6 
7If you want to create a new branch to retain commits you create, you may 
8do so (now or later) by using -b with the checkout command again. Example: 
9 
10  git checkout -b <new-branch-name> 
11 
12HEAD is now at f871b59 First commit

Line 3 announces that we are in the ‘detached HEAD’ state.

git symbolic-ref HEAD
1fatal: ref HEAD is not a symbolic ref

We cannot look at the ‘symbolic-ref’ because it is no longer there.

cat .git/HEAD
1f871b597ef5160ab19556e42e8a5264d092ad2bc

In fact the .git/HEAD file contains only the hash of the commit we checked out.

1git checkout 0715e70 
2git symbolic-ref HEAD
git checkout 0715e70
1Previous HEAD position was f871b59 First commit 
2HEAD is now at 0715e70 Second commit
git symbolic-ref HEAD
1fatal: ref HEAD is not a symbolic ref

If we checkout the second commit directly (using its hash) we are simply informed that we updated the hash and attempting to examine the symbolic-ref still results in an error.

1git checkout test_branch 
2git symbolic-ref HEAD
git checkout test_branch
1Switched to branch 'test_branch'
git symbolic-ref HEAD
1refs/heads/test_branch

Checking out the test_branch (which is the same commit but now referred to by the refs/heads/test_branch) we are ‘switched’ to that branch’s HEAD (the very same commit is being checked out, but the reference is now a symbolic-ref).

2.6.2 tags

It would be very useful to have a method of recalling a commit by name, for example when we release versions of our project it would be good to be able to say “this commit is version 1.0 of my project”. Fortunately Git has tag references for just this purpose.

Tag references come in two types:

  1. Lightweight—these tags are similar to the symbolic references used above, they are simple records in the .git/refs/tags directory that point to specific commit objects (much as we have just seen .git/refs/heads do). Lightweight tags are typically private temporary names assigned by the user.
  2. Annotated—these are a new type of object, a tag object, that contains some metadata associated with the tag. An entry is then made in the .git/refs/tags directory referencing this tag object. Annotated tags are used for more public permanent tags, such as release commits.

To create a new lightweight tag we use update-ref.

1git update-ref refs/tags/v1.0 f871b5 
2cat .git/refs/tags/v1.0
cat .git/refs/tags/v1.0
1f871b58596491e15ee1da91eaf0a4a6c1da3e573

PIC

Figure 2.13:Lightweight tag

We can now refer to the commit object with hash f871b5 using the tag name v1.0. These lightweight tags are useful to assign ‘human readable’ names to Git objects we may be interested in, but we can create an annotated tag that includes additional information.

1git tag -a v2.0 0715e7 -m "The second version" 
2cat .git/refs/tags/v2.0 
3git cat-file -t v2.0 
4git cat-file -p v2.0
cat .git/refs/tags/v2.0
1a7edafabd57c0a3dc7788d021359083ae31d3826
git cat-file -t v2.0
1tag
git cat-file -p v2.0
1object 0715e707b906d30c9e395448ddc9e96acd89d5f7 
2type commit 
3tag v2.0 
4tagger vagrant <vagrant@debian-10.7-amd64> 1616259961 +0000 
5 
6The second version

PIC

Figure 2.14:Annotated tag

Here we use the git tag command with the -a (for annotate) option to tag commit object ac21f9 and add a comment with the -m option7. This creates a new object of type tag. Unlike other commands that we have seen that create objects, the tag command does not return the new object’s hash. This is not a problem as the tag is now a proxy for that tag object’s hash. We can specify either the hash or the new tag to the cat-file to examine the new tag object. Looking inside that tag object we see that it is referencing the object ac21f9 (the one we tagged), this object is a commit object and the tag object is for tag v2.0. The last text block is the comment provided in the -m option.

The fact that the tag tracks the type of the object being tagged should be a clue that we can tag any object we like.

1git tag -a Meta v2.0 -m "Meta tagging dude" 
2git cat-file -p Meta
git cat-file -p Meta
1object a7edafabd57c0a3dc7788d021359083ae31d3826 
2type tag 
3tag Meta 
4tagger vagrant <vagrant@debian-10.7-amd64> 1616260030 +0000 
5 
6Meta tagging dude

PIC

Figure 2.15:Annotated tag of a tag

Here we have created a tag (Meta) of a tag (v2.0). What is more Git will do what you might expect, it follow this meta-tag down until an object of the type expected by the command is found.

1git checkout master 
2git log --oneline 
3git log --oneline Meta
git checkout master
1Switched to branch 'master'
git log --oneline
1d17958c (HEAD -> master) Third commit 
20715e70 (tag: v2.0, tag: Meta, test_branch) Second commit 
3f871b58 (tag: v1.0) First commit

Notice that the tags associate with each commit object are also shown.

git log --oneline Meta
10715e70 (tag: v2.0, tag: Meta, test_branch) Second commit 
2f871b58 (tag: v1.0) First commit

Switching back to the master branch we can see the log history from the master HEAD contains three commits. Specifying the Meta tag as the revision from which we want to log we see only the two commits from Meta—even though Meta is actually a tag object (50945f) that refers to a tag object (a7edaf) that finally refers to a commit object (0715e7).

6In mid-2020 Git 2.8.0 provided the ability to change the default branch name (using the configuration setting init.defaultBranch). In October 2020 GitHub started using main rather than master in new repositories, GitLab announced a similar change in March 2021. This in response to sensitivity about the use of ‘master’ in all forms due to its tangential association with slavery. Etymology is not the strong suit of the over-sensitive. Thankfully the use of master is not forbidden so this change can be largely ignored.

7The -a option is implied when -m is specified without -a,-s or -u, see man git-tag(1).