Skip to content

Exploring Git Reference Logs

Photo by Hunter Harritt on Unsplash

Up until this point, in this series of articles, we've seen both how Git stores information about a repository, and how it tracks branches and merges. Today, I'm going to explore another aspect of how Git tracks the history of what happened with a repository: reference logs.

A couple weeks ago, we discovered that Git created two files related to the HEAD reference; the actual reference, and a log file:

$ find  .git -name HEAD
.git/HEAD
.git/logs/HEAD

If we look at the /logs directory now, we'll find that there are some other files as well:

$ find .git/logs/ -type f
.git/logs/HEAD
.git/logs/refs/heads/german-translation
.git/logs/refs/heads/master
.git/logs/refs/heads/spanish-translation

All of then are related to a Git reference (either a branch, or HEAD). Git uses that to track how these references evolved over time. they look like this:

$ cat .git/logs/HEAD
0000000000000000000000000000000000000000 9496b59087c06604c2e62f3a74f372e2840b2540 ...  commit (initial): Say hello to the world
9496b59087c06604c2e62f3a74f372e2840b2540 9496b59087c06604c2e62f3a74f372e2840b2540 ...  checkout: moving from master to german-translation
9496b59087c06604c2e62f3a74f372e2840b2540 bec460cdd4eef813c46d8a5438495a2282762d18 ...  commit: Create German translation
bec460cdd4eef813c46d8a5438495a2282762d18 9496b59087c06604c2e62f3a74f372e2840b2540 ...  checkout: moving from german-translation to master
9496b59087c06604c2e62f3a74f372e2840b2540 bec460cdd4eef813c46d8a5438495a2282762d18 ...  merge german-translation: Fast-forward
bec460cdd4eef813c46d8a5438495a2282762d18 9496b59087c06604c2e62f3a74f372e2840b2540 ...  checkout: moving from master to spanish-translation
9496b59087c06604c2e62f3a74f372e2840b2540 93ee64a4c5075c0e39929f6c91705c736ac9d714 ...  commit: Add Spanish translation
93ee64a4c5075c0e39929f6c91705c736ac9d714 bec460cdd4eef813c46d8a5438495a2282762d18 ...  checkout: moving from spanish-translation to master
bec460cdd4eef813c46d8a5438495a2282762d18 c7af8843b8297ebb8c7f51a430cccf61e297f795 ...  merge spanish-translation: Merge made by the 'recursive' strategy.

$ cat .git/logs/refs/heads/master
0000000000000000000000000000000000000000 9496b59087c06604c2e62f3a74f372e2840b2540 ...  commit (initial): Say hello to the world
9496b59087c06604c2e62f3a74f372e2840b2540 bec460cdd4eef813c46d8a5438495a2282762d18 ...  merge german-translation: Fast-forward
bec460cdd4eef813c46d8a5438495a2282762d18 c7af8843b8297ebb8c7f51a430cccf61e297f795 ...  merge spanish-translation: Merge made by the 'recursive' strategy.

$ cat .git/logs/refs/heads/german-translation
0000000000000000000000000000000000000000 9496b59087c06604c2e62f3a74f372e2840b2540 ...  branch: Created from master
9496b59087c06604c2e62f3a74f372e2840b2540 bec460cdd4eef813c46d8a5438495a2282762d18 ...  commit: Create German translation

$ cat .git/logs/refs/heads/spanish-translation
0000000000000000000000000000000000000000 9496b59087c06604c2e62f3a74f372e2840b2540 ...  branch: Created from master
9496b59087c06604c2e62f3a74f372e2840b2540 93ee64a4c5075c0e39929f6c91705c736ac9d714 ...  commit: Add Spanish translation

Each line of those files represents a change in the reference pointer. It starts with the hash of the commit where the reference was (0000000... to represent a "null" commit on ref creation) followed by the commit has the reference moved to. After that, we have committer infor and the timestamp of the change. Lastly, we have a description of why the reference changed. There are several options here:

  • commit: A new commit was created in the branch. In this case, the first line of the commit message is included.
  • branch: All the branches that are not the default one for the repository start with this action. The details include the name of the branch reference that was copied for the creation of the new branch.
  • checkout: The HEAD reference changed from pointing to one branch to another. The details include both origina and destination branch names.
  • merge: A branch was merged into another. the details tell us if it was a fast-forward merge or the name of the strategy used for the merge otherwise ("recursive" by default).

The common Git command to see this information is git reflog:

$ git reflog
c7af884 (HEAD -> master) HEAD@{0}: merge spanish-translation: Merge made by the 'recursive' strategy.
bec460c (german-translation) HEAD@{1}: checkout: moving from spanish-translation to master
93ee64a (spanish-translation) HEAD@{2}: commit: Add Spanish translation
9496b59 HEAD@{3}: checkout: moving from master to spanish-translation
bec460c (german-translation) HEAD@{4}: merge german-translation: Fast-forward
9496b59 HEAD@{5}: checkout: moving from german-translation to master
bec460c (german-translation) HEAD@{6}: commit: Create German translation
9496b59 HEAD@{7}: checkout: moving from master to german-translation
9496b59 HEAD@{8}: commit (initial): Say hello to the world

This command shows the important information of how the HEAD ref changed in an abbreviated format. It shows only the commit the branch pointed by HEAD moved to, not where it was. It also has markers for when the branch pointed by HEAD matches any of the current branches.

There are indicators of how we can abbreviate references to these points in time. HEAD@{0} represents the current status of the HEAD ref. HEAD@{1} marks the commit HEAD was pointing to one commit ago, and so on. These numbers obviously change as we keep performing operations. For instance:

$ git branch test-branch
$ git checkout test-branch
Switched to branch 'test-branch'

$ git reflog
c7af884 (HEAD -> test-branch, master) HEAD@{0}: checkout: moving from master to test-branch
c7af884 (HEAD -> test-branch, master) HEAD@{1}: merge spanish-translation: Merge made by the 'recursive' strategy.
bec460c (german-translation) HEAD@{2}: checkout: moving from spanish-translation to master
93ee64a (spanish-translation) HEAD@{3}: commit: Add Spanish translation
9496b59 HEAD@{4}: checkout: moving from master to spanish-translation
bec460c (german-translation) HEAD@{5}: merge german-translation: Fast-forward
9496b59 HEAD@{6}: checkout: moving from german-translation to master
bec460c (german-translation) HEAD@{7}: commit: Create German translation
9496b59 HEAD@{8}: checkout: moving from master to german-translation
9496b59 HEAD@{9}: commit (initial): Say hello to the world

We can alos see that the git reflog command shows information in reverse chronological order (most recent entries first), whereas the log file is stored in chronological order (new entries are added at the end). This is for efficiency reasons, as keeping the entries stored in reverse order would require Git to move the existing entries down on each change, rewriting the whole file each time. Usually, we're interested in looking at the most recent information, since it's more relevant to our workflow.

The power of history

So far, all this is very interesting, but seems to have no practical value in our Git workflow. Well, that's not always the case. There are situations where having more information about the recent history of our repository allows us to remediate problems. Here are some examples of scenarios where I found reflog information useful (in no particular order):

  • I know I have worked on a particular proof of concept in a branch that was not merged, but has since been deleted.
  • I used git reset to move the active branch back in history and made new commits in a diverging branch, and then discover that I lost a change I made in the old branch version.
  • I made a mistake while resolving conflicts during a merge a few commits ago, and now just realize it.
  • I did a git rebase prior to pushing work to a remote repository to keep a cleaner history and when trying to push I discover that the rebase includes commits that were already pushed.

In all these scenarios, I want to be able to go to a previous state of my local repository, so that I can fix the issue. That essentially means that I want to identify one or more particular commits, but those commits might be unreachable from the existing branches. Let's simulate the first scenario in our new "test-branch":

$ echo "Proof of concept" > poc.txt
$ git add poc.txt
$ git commit -m "Create PoC"
[test-branch 5a990bc] Create PoC
1 file changed, 1 insertion(+)
create mode 100644 poc.txt

$ git checkout master
Switched to branch 'master'

$ git branch -D test-branch
Deleted branch test-branch (was 5a990bc).

Here we did some work in a branch that was never merged, and later went back to "master". At some point we deleted the PoC branch (using the -D flag instead of -d to force the deletion. Git will complain that the branch is unmerged otherwise). At the moment, Git let us know the commit hash the deleted branch was pointing to. but suppose that some time goes by and you want to recover some change made in that branch. We no longer have access to the commit through our regular history, as it does not appear there at all:

$ git log --oneline
c7af884 (HEAD -> master) Merge branch 'spanish-translation'
93ee64a (spanish-translation) Add Spanish translation
bec460c (german-translation) Create German translation
9496b59 Say hello to the world

However, we know that HEAD was pointing to the deleted branch at some point in time (as we always work on the branch pointed by HEAD), so we can look into the reflog for HEAD to search for the commit ID we're interested into:

$ git reflog
c7af884 (HEAD -> master) HEAD@{0}: checkout: moving from test-branch to master
5a990bc HEAD@{1}: commit: Create PoC
c7af884 (HEAD -> master) HEAD@{2}: checkout: moving from master to test-branch
c7af884 (HEAD -> master) HEAD@{3}: merge spanish-translation: Merge made by the 'recursive' strategy.
bec460c (german-translation) HEAD@{4}: checkout: moving from spanish-translation to master
93ee64a (spanish-translation) HEAD@{5}: commit: Add Spanish translation
9496b59 HEAD@{6}: checkout: moving from master to spanish-translation
bec460c (german-translation) HEAD@{7}: merge german-translation: Fast-forward
9496b59 HEAD@{8}: checkout: moving from german-translation to master
bec460c (german-translation) HEAD@{9}: commit: Create German translation
9496b59 HEAD@{10}: checkout: moving from master to german-translation
9496b59 HEAD@{11}: commit (initial): Say hello to the world

There we can see that the PoC commit is "5a990bc". Now we can create a new branch poiting to that commit if we want, using git branch.

$ git branch poc-branch 5a990bc

Alternatively, this command could be written as:

$ git branch poc-branch HEAD@{1}

If we look at the current HEAD reflog, we'll see that, even though HEAD didn't change, the new branch points to the right commit in its history:

$ git reflog
c7af884 (HEAD -> master) HEAD@{0}: checkout: moving from test-branch to master
5a990bc (poc-branch) HEAD@{1}: commit: Create PoC
c7af884 (HEAD -> master) HEAD@{2}: checkout: moving from master to test-branch
c7af884 (HEAD -> master) HEAD@{3}: merge spanish-translation: Merge made by the 'recursive' strategy.
bec460c (german-translation) HEAD@{4}: checkout: moving from spanish-translation to master
93ee64a (spanish-translation) HEAD@{5}: commit: Add Spanish translation
9496b59 HEAD@{6}: checkout: moving from master to spanish-translation
bec460c (german-translation) HEAD@{7}: merge german-translation: Fast-forward
9496b59 HEAD@{8}: checkout: moving from german-translation to master
bec460c (german-translation) HEAD@{9}: commit: Create German translation
9496b59 HEAD@{10}: checkout: moving from master to german-translation
9496b59 HEAD@{11}: commit (initial): Say hello to the world

Parting thoughts

Even though we've been looking only at the HEAD reflog, we saw at the beginning that Git maintains a log for each reference in our repository, so we can also look at the contents of other reflogs:

$ git reflog master
c7af884 (HEAD -> master) master@{0}: merge spanish-translation: Merge made by the 'recursive' strategy.
bec460c (german-translation) master@{1}: merge german-translation: Fast-forward
9496b59 master@{2}: commit (initial): Say hello to the world

You can see that the <refname>@{<n>} format applies to all references, not only HEAD.

Another important thing to consider is that reflogs are maintained only locally. They represent the history of the references in your local repository and are not shared with remote repositories, so they are only useful to traverse local history. This also means that in a fresh clone of a repository, all reflogs will be empty.

Finally, the lifespan of a reflog is limited, and old entries will get regularly pruned by Git's hosekeeping process. They typically last several weeks (Git's documentation states that the default is 90 days, though it can be configured to other values). For this reason, they are not reliable as a means of finding really old commits.

I hope this analysis of reference logs and their usage has been useful to you. Until next week!

Published inTools