What actually happens inside .git when you commit, branch or rebase? This lesson opens the hood: Git’s object database, how branches and HEAD work, and how everyday commands map to simple internal operations.
📖 What You’ll Learn: the .git folder, the four object types and content addressing, exploring objects with cat-file, ls-tree and hash-object, references, branches and HEAD, detached HEAD, how commands map to internals, and packfiles and garbage collection.
🔬 Why Learn Git Internals?
You can use Git for years without looking inside. But understanding how Git stores data makes every command make sense: why branches are instant, why rebase changes hashes, why “lost” commits can be recovered, and what HEAD really is. It also answers some of the toughest interview questions.
📁 Inside the .git Folder
ls .git
| Item | What it stores |
|---|---|
HEAD | Which branch (or commit) you are on |
config | Repository settings (git config) |
objects/ | All content: files, folders and commits, as compressed objects |
refs/heads/ | Branches: one file per branch, containing a commit hash |
refs/tags/ | Tags |
refs/remotes/ | Remote-tracking branches such as origin/main |
index | The staging area (staging area) |
logs/ | The reflog (git reflog) |
hooks/ | Hook scripts (hooks) |
🧱 The Four Object Types
Git is a content-addressable database: every object is stored under the hash of its content.
| Object | Represents | Contains |
|---|---|---|
| blob | A file’s content | The raw bytes (no file name!) |
| tree | A directory | Names, modes and hashes of blobs and sub-trees |
| commit | A snapshot | A tree hash, parent commit hash(es), author, committer, message |
| tag (annotated) | A named release | The tagged object, tagger, date, message (tags) |
commit 8b1d4e2 ──▶ tree a1b2c3 ──▶ blob 5d6e7f (README.md)
│ └──────▶ tree 9a8b7c (tests/)
│ └──────▶ blob 3c4d5e (login.spec.ts)
▼
parent commit 3f2a91c
Hashes are SHA-1 by default (40 hex characters); newer Git versions can also create repositories that use SHA-256.
🔎 Explore Objects Yourself
git cat-file -t HEAD # type of an object
git cat-file -p HEAD # pretty-print the commit
tree a1b2c3d4e5f6...
parent 3f2a91c7d8e9...
author Asha Verma <asha.verma@example.com> 1793345000 +0530
committer Asha Verma <asha.verma@example.com> 1793345000 +0530
Add learning goals and initial test plan
git cat-file -p HEAD^{tree} # list the root tree
git ls-tree -r HEAD # every file in the commit with its blob hash
git cat-file -p <blob-hash> # the file content
echo "hello" | git hash-object --stdin # the hash Git would give this content
Because the hash depends on content, two identical files anywhere in history share one blob: Git stores each unique content only once.
🏷️ References: Branches Are Just Files
cat .git/HEAD
# ref: refs/heads/main
cat .git/refs/heads/main
# 8b1d4e2c7a9f0b1e2d3c4b5a69788f7e6d5c4b3a
- A branch is a tiny file containing one commit hash. Creating a branch writes about 41 bytes, which is why it is instant.
- Committing creates new objects and moves the current branch file to the new commit.
- HEAD usually contains
ref: refs/heads/<branch>. When it contains a hash directly, you are in detached HEAD (for example after checking out a tag or during bisect). - Packed refs may live in
.git/packed-refsinstead of individual files.
🔗 How the Commands Map to Internals
| Command | What happens internally |
|---|---|
git add | Creates blobs and updates the index |
git commit | Creates trees and a commit object; moves the branch ref |
git branch x | Creates refs/heads/x pointing to the current commit |
git switch x | Updates HEAD and the working directory from x’s tree |
git merge | Creates a commit with two parents (or moves the ref for a fast-forward) |
git rebase | Creates new commits (new parents, so new hashes) and moves the ref |
git reset --hard X | Moves the branch ref to X and updates index and files |
git gc | Packs objects into packfiles and removes unreachable ones after the reflog expires |
📦 Packfiles and Storage
New objects start as individual compressed files (“loose objects”). git gc (run automatically from time to time) packs them into packfiles that store similar objects as deltas, which keeps repositories small.
git count-objects -vH # number and size of loose and packed objects
git gc # pack and clean up (rarely needed manually)
📝 Assignment
- In
git-practice, runcat .git/HEADandcat .git/refs/heads/main(or look inpacked-refs). - Use
git cat-file -p HEADto follow your latest commit to its tree and to one blob’s content. - Create a branch and confirm a new file appeared in
.git/refs/heads/. - Use
git hash-objecton a file and find the same hash ingit ls-tree -r HEAD. - Check out a tag or an old commit, then
cat .git/HEADto see detached HEAD. - Run
git count-objects -vHbefore and aftergit gc.
❓ FAQ
🤔 How does Git store data internally?
As objects in a content-addressable database: blobs for file content, trees for directories, commits for snapshots and tags, each identified by the hash of its content.
🤔 What is HEAD in Git?
A reference to what you have checked out. Usually it points to a branch (ref: refs/heads/main); in detached HEAD it points directly to a commit.
🤔 What is a blob in Git?
The object that stores a file’s content. It does not store the file name; names live in tree objects.
🤔 Why are Git branches so cheap?
A branch is just a small file containing a commit hash, so creating one does not copy any files.
🤔 Why does git rebase change commit hashes?
A commit’s hash includes its parent. Rebasing gives commits new parents, so Git creates new commit objects with new hashes.
🎯 Next Steps
- 🔑 Personal Access Tokens and Authentication
- 🛡️ Branch Protection Rules and Rulesets
- 📘 Git and GitHub Tutorial: full course
🎬 Learn Git and GitHub step by step: watch the series on YouTube!
Subscribe to SqaToolsByDY • Practice on sqatools.in
2 thoughts on “Git Internals Explained: Objects, Trees, Commits, Refs and HEAD”