Git Refs (References)
TL;DR: The .git/objects/ database is a collection of immutable, content-addressed files named after 40-character SHA-1 hashes. Refs (References) are simple, human-readable text files stored in .git/refs/ that map names (like main, feature, or v1.0.0) to specific commit hashes, shielding you from having to memorize SHA-1 strings.
If you look at the raw object database of a Git repository, it is a massive web of SHA-1 hashes. Every commit, tree, and blob is given an address like a1b2c3d4e5f6.... Because humans are not designed to memorize 40-character hexadecimal strings, Git uses a simple mapping system: References, or Refs.
Under the hood, a Git reference is nothing more than a file containing a commit hash. When you run commands like git branch or git tag, you are simply interacting with tiny text files in the .git/ directory.
In this tutorial, we will explore the internal mechanics of references, understand the difference between branches and tags, look at the special role of HEAD, and learn how to manage and recover references using low-level plumbing commands.
What Do Git References Actually Do?
To understand how Git references work, it helps to look at them as “bookmarks” or “aliases” in your version control system. Instead of telling Git to checkout 8c474d28d..., you tell it to checkout main. Git looks up the alias, resolves it to the correct hash, and performs the checkout.
There are three primary categories of references in Git:
graph TD
HEAD[HEAD: Symbolic Ref <br> ref: refs/heads/main] --> Branch[Branch Ref: refs/heads/main <br> Contains: 8c474d2...]
Branch --> Commit[Commit Object: 8c474d2...]
Tag[Tag Ref: refs/tags/v1.0 <br> Contains: 8c474d2...] --> Commit
Remote[Remote Ref: refs/remotes/origin/main <br> Contains: 8c474d2...] --> Commit
1. Branches (Local References)
In Git, a branch is not a folder or a heavy copy of your codebase. It is a lightweight, mutable pointer.
- Mutable: A branch reference changes automatically every time you commit. If you make a new commit on
main, Git updates themainreference file with the new commit’s hash. - Location: Local branch files are stored inside
.git/refs/heads/.
2. Tags (Stationary References)
A tag is a permanent bookmark to a specific point in your history.
- Immutable: Unlike branches, tags do not move. Once you tag a commit as
v1.0.0, that tag points to the same commit forever, unless you explicitly delete and recreate it. - Types: There are two types of tags: lightweight tags (which are simple references containing a commit hash) and annotated tags (which point to a full tag object in the
.git/objects/database, as described in git-objects). - Location: Tags are stored inside
.git/refs/tags/.
3. Remote-Tracking References
These references track the state of branches on remote repositories (like GitHub or GitLab).
- ReadOnly locally: You cannot write to these references directly. They are updated automatically when you run
git fetch,git pull, orgit push. - Location: Remote references are stored inside
.git/refs/remotes/[remote-name]/(e.g.,.git/refs/remotes/origin/main).
4. HEAD: The Special Symbolic Reference
HEAD is the reference that tells Git which branch is currently checked out and where your next commit should go.
- Symbolic Ref: Unlike standard references,
HEADdoes not usually contain a commit hash. Instead, it contains a pointer to another reference (e.g.,ref: refs/heads/main). - Detached HEAD: If you check out a specific commit hash, a remote branch, or a tag instead of a local branch,
HEADbecomes “detached”. In this state, theHEADfile directly contains a raw commit hash instead of a symbolic pointer.
What Happens Inside .git/ (Hands-on Exploration)
Let’s build a repository step-by-step and inspect how the files inside .git/refs/ are created and modified.
Step 1: Initialize a Repo and Create a Commit
First, initialize a new Git repository:
$ mkdir git-refs-demo
$ cd git-refs-demo
$ git init
Initialized empty Git repository in /path/to/git-refs-demo/.git/
Immediately after initialization, let’s look inside .git/HEAD:
$ cat .git/HEAD
ref: refs/heads/main
(Depending on your Git configuration, it might say ref: refs/heads/master.)
Notice that .git/refs/heads/main does not exist yet! Git sets up HEAD to point to refs/heads/main so that it knows where to write the reference once the first commit is made.
Now, let’s create a file and commit it:
$ echo "Refs are simple" > readme.txt
$ git add readme.txt
$ git commit -m "Initial commit"
[main (root-commit) a4b3c2d] Initial commit
1 file changed, 1 insertion(+)
create mode 100644 readme.txt
Now let’s check the contents of .git/refs/heads/main:
$ cat .git/refs/heads/main
a4b3c2d58547cb826ef89b3bc01b4431e21b8f1d
(The file contains exactly one line: the 40-character SHA-1 hash of the commit we just created.)
We can resolve references to hashes using the plumbing command git rev-parse:
$ git rev-parse main
a4b3c2d58547cb826ef89b3bc01b4431e21b8f1d
$ git rev-parse HEAD
a4b3c2d58547cb826ef89b3bc01b4431e21b8f1d
Step 2: Create a Branch
Now, let’s create a new branch named feature using git branch:
$ git branch feature
Let’s see what happened inside .git/refs/heads/:
$ ls -la .git/refs/heads/
-rw-r--r-- 1 user group 41 Jul 7 10:30 feature
-rw-r--r-- 1 user group 41 Jul 7 10:30 main
If we view the contents of the feature file, we see it is identical to main:
$ cat .git/refs/heads/feature
a4b3c2d58547cb826ef89b3bc01b4431e21b8f1d
Creating a branch in Git is incredibly fast because it only involves creating a 41-byte text file containing a 40-character hash followed by a newline character. No code files are duplicated.
Step 3: Switch Branches (Updating HEAD)
Let’s switch to our new branch using git checkout:
$ git checkout feature
Switched to branch 'feature'
If we read .git/HEAD now:
$ cat .git/HEAD
ref: refs/heads/feature
HEAD has been updated to point to the new branch reference.
Step 4: Make a Commit on the Branch
Let’s make a new commit on our feature branch:
$ echo "Working on feature" >> readme.txt
$ git commit -am "Implement feature"
[feature c9d8e7f] Implement feature
1 file changed, 1 insertion(+)
Let’s check the contents of both branch references:
$ cat .git/refs/heads/feature
c9d8e7f849b28a2a0bc4049d5bf1c38fa85b19d2
$ cat .git/refs/heads/main
a4b3c2d58547cb826ef89b3bc01b4431e21b8f1d
The feature reference file was overwritten with the hash of the new commit c9d8e7f.... The main reference file remains completely untouched.
Step 5: Create a Tag
Let’s create a tag on our current commit:
$ git tag v1.0.0-alpha
Now let’s check .git/refs/tags/:
$ cat .git/refs/tags/v1.0.0-alpha
c9d8e7f849b28a2a0bc4049d5bf1c38fa85b19d2
The tag is a file containing the same commit hash. If we make more commits on the feature branch, the feature ref will move forward, but the v1.0.0-alpha ref will remain pointed at c9d8e7f... forever.
Detailed Command Options & Advanced Usage
Git has a set of low-level plumbing commands specifically designed to read and write references. Using these commands, you can bypass porcelain commands entirely.
1. Manipulating References with git update-ref
You don’t need git branch or git tag to manage references. You can do it directly using git update-ref.
- Create or update a ref:
This creates a new branch named$ git update-ref refs/heads/manual-branch c9d8e7f849b28a2a0bc4049d5bf1c38fa85b19d2manual-branchpointing to the specified commit, exactly as if we had rungit branch manual-branch c9d8e7f. - Delete a ref:
This deletes the$ git update-ref -d refs/heads/manual-branchmanual-branchref file from the system.
2. Inspecting Symbolic Refs with git symbolic-ref
If you want to read or write the symbolic link of HEAD programmatically without editing files manually, you use git symbolic-ref.
- Read a symbolic ref:
$ git symbolic-ref HEAD refs/heads/feature - Point HEAD to another branch:
This changes your active branch to$ git symbolic-ref HEAD refs/heads/mainmainwithout updating your working directory files (which can lead to index inconsistencies; therefore, porcelain commands likegit checkoutorgit switchare preferred for workspace operations).
3. Packed References (packed-refs)
When a repository accumulates hundreds or thousands of branches and tags, looking up references across thousands of individual loose files can degrade disk I/O performance. To optimize this, Git automatically packages loose refs into a single file.
If you run git gc (garbage collection), Git deletes the individual files in .git/refs/heads/ and .git/refs/tags/ and writes their data into a single text file located at .git/packed-refs.
The .git/packed-refs file looks like this:
# pack-refs with: peeled fully-peeled sorted
a4b3c2d58547cb826ef89b3bc01b4431e21b8f1d refs/heads/main
c9d8e7f849b28a2a0bc4049d5bf1c38fa85b19d2 refs/heads/feature
^c9d8e7f849b28a2a0bc4049d5bf1c38fa85b19d2 refs/tags/v1.0.0-alpha
(The line starting with ^ indicates the peeled commit hash that the annotated tag object points to.)
When you modify a packed branch, Git writes a loose file back to .git/refs/heads/branch-name. Loose files always override entries in .git/packed-refs.
Real-World Scenario
Scenario A: Recovering a Deleted Branch
A developer is cleaning up old local branches and runs git branch -D feature-login, only to realize they had unmerged work on that branch. Since the branch is deleted, git log no longer shows it, and the file .git/refs/heads/feature-login is gone.
Step-by-Step Recovery:
- Every time a reference changes, Git logs the transaction in the reflog. Let’s look at the reflog:
$ git reflog a4b3c2d HEAD@{0}: checkout: moving from feature-login to main f9c8d7e HEAD@{1}: commit: Implement user authentication - The reflog shows that before switching to
main, we were at commitf9c8d7eon thefeature-loginbranch. - We can reconstruct the branch reference using
git branchorgit update-ref:$ git branch feature-login f9c8d7e - The branch is fully restored, complete with all its commits, because the commit objects themselves in
.git/objects/were never deleted.
Scenario B: Resolving a Detached HEAD State
A developer runs git checkout origin/main to check remote work. They see a warning: You are in 'detached HEAD' state. They write several commits, only to realize their commits are not on any branch.
Under the Hood:
In a detached HEAD state, .git/HEAD contains a raw commit hash instead of a path:
$ cat .git/HEAD
f9c8d7e35b7190849202bb04c8f2a3bc6e01a88b
Any commits made here will have parent pointers linking backward, but no branch points to them. If the developer checks out main, these new commits will become dangling.
The Recovery:
Before leaving the detached HEAD state, create a new branch:
$ git checkout -b feature-saved
This updates .git/HEAD to point to a new local branch file:
$ cat .git/HEAD
ref: refs/heads/feature-saved
The commits are now safely anchored to a named reference.
Common Pitfalls and Mistakes
1. Manually Editing Refs in a Text Editor
The Mistake: Opening .git/refs/heads/main in an editor and manually changing the commit hash.
Why it fails: This bypasses Git’s safe transactional locks. If you make a typo or reference a non-existent commit hash, you will corrupt the reference. Git will throw a fatal: bad revision error.
The Fix: Always use git update-ref or standard branch/reset commands to update references.
2. Confusion: Deleting a Branch Does Not Delete Code
The Mistake: Deleting a branch (git branch -d branch-name) and believing the code changes have been instantly wiped from the hard drive.
Why it’s wrong: Deleting a branch only deletes the tiny reference file (the pointer). The actual commit, tree, and blob objects remain inside .git/objects/ intact. They will not be deleted until Git runs garbage collection (git gc), which typically happens weeks later. You can recover the branch at any point before garbage collection.
3. Detached HEAD Commit Loss
The Mistake: Working in a detached HEAD state, making commits, and checking out another branch (git checkout main) without saving.
Why it happens: Because no branch ref points to the commits made during the detached state, they become orphaned. They will not show up in git-log.
The Fix: Check git reflog immediately to find the hash of the last commit made in the detached state, and create a branch pointing to it.
Command Quick Reference
| Command | Category | Description |
|---|---|---|
git rev-parse <ref> | Plumbing | Resolves a reference (like HEAD or a branch) to its 40-character SHA-1 hash. |
git symbolic-ref <ref> | Plumbing | Reads or writes a symbolic reference like HEAD. |
git update-ref <ref> <hash> | Plumbing | Creates or updates a reference to point to a specific commit hash. |
git show-ref | Plumbing | Lists all references in the repository along with their hashes. |
git reflog | Porcelain | Shows a log of all movements of HEAD and branch references. |
git branch <name> <hash> | Porcelain | Creates a branch pointing to the specified commit hash. |
FAQ (People Also Ask)
What is the difference between a branch and a reference in Git?
A branch is a specific type of reference located in the refs/heads/ directory that automatically moves forward when you create a new commit while active on it. A reference is a generic term for any pointer that maps a human-readable name to a Git object hash. This includes branches, tags, remote-tracking refs, and notes.
What is a symbolic reference (symref) in Git?
A symbolic reference is a special reference that points to another reference rather than directly to a commit hash. The most common symbolic reference is HEAD, which points to the active branch (such as refs/heads/main). When Git reads HEAD, it resolves the symbolic link to find the active branch, and then reads that branch to find the commit hash.
How does Git know which branch is currently active?
Git knows which branch is active by reading the .git/HEAD file. If the file contains ref: refs/heads/feature, then the active branch is feature. If you switch branches, Git overwrites the content of .git/HEAD with the path to the new branch.
What happens when a branch is deleted?
When you delete a branch, Git simply removes the text file associated with that branch from .git/refs/heads/ (or deletes its line inside .git/packed-refs). The commits and historical files remain in the object database until they are automatically purged by Git’s garbage collection process.
Test Your Knowledge
Loading quiz…