2.5 refs

So far we have:

  1. Created some hash objects.
  2. Created some tree objects that associate file pathname and mode with one or more hash objects
  3. Created some commit objects that associate metadata with tree objects and allows us to relate tree objects in a graph which is typically interpreted as a version graph where each parent is an earlier version (it should be noted that Git itself is completely unaware of this interpretation though).

So far, so good but it is still a bit cumbersome to use. For one thing we have to remember which commit object we last created so that we can use it as the parent for our next commit. We have seen this problem above, not only when using commit-tree but also using the log command where we needed to know the hash of the most recent commit object in our history.

refs (or ‘references’) to the rescue. A ref is a more human readable way to refer a commit object hash.

1tree -a .git/refs
tree -a .git/refs
1.git/refs 
2├── heads 
3└── tags 
4 
52 directories, 0 files

The refs directory contains two sub-directories:

  • heads—contains references to the head, or latest, commit object we want to name.
  • tags—contains references to any object we want to give a human readable name.

We can set the head of our master branch (the default branch5 on which Git works, more on branches later) to our latest commit object:

1echo "e27aaa8c158e6f261f4c03aaaf173a149ad61d81" > .git/refs/heads/master 
2git log --stat

We have to use the full hash when writing to the .git/refs/heads/master file.

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(+)

Issuing the log command without specifying the exact commit we are interested in causes Git to look up the refs entry of our current branch (actually it looks in .git/HEAD for the ‘latest’ commit and since we have not moved from the default branch this will be master, we look at .git/HEAD again in §2.6.1.)

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

The master file in the .git/refs/heads directory contains the hash of the commit object we want to call the ‘head’ of our master branch.

PIC

Figure 2.9:HEAD dereferencing

Editing refs files directly is not ideal (not least because we can’t abbreviate the hash) so Git provides the update-ref command.

1git update-ref refs/heads/master 0715e7 
2git log --stat
git log --stat
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(+)

After we update the reference to the penultimate commit in our repository (0715e7) we curtail the log at that point and Git will treat that commit as the latest on the master branch.

PIC

Figure 2.10:Penultimate change

We can easily restore the head of master using the update-ref command.

1git update-ref refs/heads/master e27aaa 
2git log --stat
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(+)

The update-ref command is doing several things for us. Firstly, it allows us to use short hashes (phew!), it checks that the hash we provide is valid too. Secondly you may have notices new directories and files appearing in the repository.

1tree -a
tree -a (truncated)
1└── .git 
2    ├── branches 
3    ├── config 
4    ├── description 
5    ├── HEAD 
6    ├── hooks 
7    ├── index 
8    ├── info 
9       └── exclude 
10    ├── logs 
11       ├── HEAD 
12       └── refs 
13           └── heads 
14               └── master

The new logs directory contains two new files; HEAD and refs/heads/master. These contain a record of each time we modify a reference using update-ref. Each log entry records the old hash, the new hash, the user who made the change, and a time stamp for when the change was made. These ‘logs of ref changes’ can be viewed (and manipulated) using the reflog command.

1git reflog
git reflog
1e27aaa8 (HEAD -> master) HEAD@{0}: 
20715e70 HEAD@{1}:

This shows the history of changes we made to the master branch refs/heads/master using update-ref. As with the log command, by default, more recent changes are shown first (reverse chronological order).

The output may look like gibberish but it’s actually simple enough. Let’s break down the first line.

  • e27aaa8—This is the new hash we set.
  • (HEAD -> master)—This tells us that this entry is about the HEAD reference and this currently refers to the master branch. (Actually, HEAD is an indirect reference that points us to the ‘latest commit on the active branch’, more on this shortly.)
  • HEAD@{0}—This is a commit reference, specifically is says that we are referring to the ‘zeroth’ (latest) change relative to the ‘HEAD’ reference.

Okay, we should now be able to read the simpler second line with ease.

  • 0715e70—The hash we set when this change was made.
  • HEAD@{1}—This line refers to the ‘first’ change to HEAD, counting back from the current value (HEAD@{0}).

2.5.1 Remote refs

There is one more type of ref we need to discuss, the ‘remote refs’. These are read only refs in the sense that one does not manipulate them directly but they are maintained through interaction with remote repositories. As these have such a specialised use I’m going to leave a complete discussion to §5.1, after we have discussed working with multiple repositories in Chapter 5.

5I tend to freely use ‘default branch’ to mean the ‘current default branch’ and ‘the branch Git will use absent any other branches’. This is lazy but I think context makes clear which is implied.