How Commits Link — SHA Chain
Hash chain samjho, toh Git ki duniya samajh aa gayi.
Each commit object stores the SHA-1 hash of its parent commit(s). This is how Git forms history — not by storing a list, but by each commit remembering its predecessor.
A regular commit has exactly 1 parent — the commit that came immediately before it. A root commit (the first commit in a repo) has 0 parents. A merge commit has 2 or more parents — it joins two lines of development together.
This parent-linked structure forms a Directed Acyclic Graph (DAG). "Directed" because parent pointers go one way (child to parent, never the reverse). "Acyclic" because no commit can ever be its own ancestor — you cannot create a loop in Git history.
Branch names are just sticky notes (refs) attached to a specific commit in this graph. HEAD is a sticky note attached to a branch name. The entire history is defined by these parent pointers — nothing else.
# Follow the parent chain manually
git log --oneline
# e5f6g7h (HEAD -> main) third commit
# c3d4e5f second commit
# a1b2c3d first commit
# Inspect the latest commit
git cat-file -p e5f6g7h
# tree 9f8321...
# parent c3d4e5f... ← points to second commit
# author ...
# Inspect the second commit
git cat-file -p c3d4e5f
# tree 8e7210...
# parent a1b2c3d... ← points to first commit
# author ...
# Inspect the first (root) commit
git cat-file -p a1b2c3d
# tree 7d6109...
# (no parent line!) ← root commit has no parent
# author ...A commit's SHA-1 hash is computed from EVERY field in the commit object: tree hash, parent hash(es), author name + email + timestamp, committer name + email + timestamp, and the commit message.
Change ANY of these fields — even a single character in the message — and the hash completely changes. This is not a coincidence; it is the fundamental property of cryptographic hash functions.
This is why git commit --amend creates a new commit. The message (or tree, or parent) changed, so the hash changed. The old commit still exists in the object store but becomes unreachable from any branch.
This is why rebasing creates new commits. The parent hash changed, so every subsequent commit's hash must also change. The code (tree) may be identical, but the commit objects are brand new.
The hash is computed with a header: commit <size>\0<content>. This prefix prevents hash collisions between object types — a blob and a commit with the same content will have different hashes because their headers differ.
# See what goes into a commit hash
git cat-file -p HEAD
# tree 9f8321a...
# parent 2b4c67d...
# author Sai <sai@devinhyd.com> 1700000000 +0530
# committer Sai <sai@devinhyd.com> 1700000000 +0530
#
# add login feature
# ALL of this is hashed together
# Change "add login feature" to "Add login feature"
# → completely different hash
# This is why you cannot silently modify history
# Any change to ANY commit → different hash
# → all child commits also get different hashes
# → branch ref points to different chain
# → git push says "rejected, history diverged"
When you merge two branches, Git creates a merge commit that has TWO parent pointers.
Parent 1 is the commit you were on (main). Parent 2 is the commit you merged in (feature). This two-parent structure is how Git preserves the fact that two lines of development converged.
Octopus merges (3+ parents) are possible but rare — used for combining multiple branches at once. Most merges have exactly 2 parents.
The first parent convention: when you run git merge feature on main, main's previous commit is "first parent" and feature's tip is "second parent". This convention matters for commands like git log --first-parent and git revert -m.
git log --first-parent follows only first parents — it shows the "mainline" history without diving into feature branch details. This gives you a clean overview of what was merged into main and when.
# Create a merge commit
git checkout main
git merge feature
# Merge made by the 'ort' strategy.
# Inspect the merge commit — TWO parents
git cat-file -p HEAD
# tree 5a4b3c...
# parent 9f8321... ← main's previous commit (first parent)
# parent 2e7d6c... ← feature's tip commit (second parent)
# author ...
# See only the mainline history
git log --oneline --first-parent
# Ignores feature branch commits, follows only first parents
# Which commit is which parent?
git cat-file -p HEAD | grep parent
# parent 9f8321... ← first parent (main)
# parent 2e7d6c... ← second parent (feature)
# This matters for:
git revert -m 1 HEAD # revert by keeping first parent's versionA branch is just a text file containing a 40-character SHA-1 hash. Nothing more.
Branch file location: .git/refs/heads/main contains the hash of the latest commit on main. That is the entire definition of a branch — one hash in one file.
When you commit on a branch, Git updates the branch file to point to the new commit hash. When you create a branch, Git just writes the current commit hash to a new file.
Tags are similar but live in .git/refs/tags/ and do not move. Annotated tags are separate objects with their own hash, message, and tagger info. Lightweight tags are just pointers like branches, but they never update.
HEAD is usually a symbolic reference: .git/HEAD contains ref: refs/heads/main — meaning HEAD points to main, which points to a commit. This layered pointer system is how Git manages branches efficiently — creating a branch is just writing 41 bytes (the hash + newline).
# A branch is just a file with a hash
cat .git/refs/heads/main
# e5f6g7h3b2a1c4d5e6f7g8h9i0j1k2l3m4n5o6p
# Creating a branch = writing that hash to a new file
git branch feature
cat .git/refs/heads/feature
# e5f6g7h3b2a1c4d5e6f7g8h9i0j1k2l3m4n5o6p ← SAME hash as main!
# HEAD points to the branch name
cat .git/HEAD
# ref: refs/heads/main
# When you commit, Git updates the branch file
git commit --allow-empty -m "new commit"
cat .git/refs/heads/main
# f6g7h8i4... ← NEW hash! Branch file updated.
# Feature branch still points to old hash.
# A tag is similar but does not move
git tag v1.0
cat .git/refs/tags/v1.0
# f6g7h8i4... ← points to commit, never updatesWhen you rebase, amend, or reset, you are creating NEW commits with NEW hashes. The old commits still exist but become unreachable.
The parent chain means changing one commit invalidates ALL descendant commits. If commit C3's parent hash changes, then C4 (which stores C3's hash) must also be recreated, and C5 must be recreated, and so on to the tip of the branch.
This is why git push rejects rewritten history — your branch has different hashes than the remote. The remote sees that your history does not extend its history; it diverges from it.
Force push overwrites the remote's chain with your new chain. This is dangerous because other developers' branches may still point to the old chain. Their git pull will fail or create unexpected merges.
The reflog is a log of where HEAD has pointed — it is how you recover from bad rewrites. Every time HEAD moves (commit, checkout, reset, rebase), Git records it. Reflog entries expire after 90 days by default.
# Before rebase: linear chain
# A -- B -- C (main) -- D -- E (feature)
# Rebase feature onto main
git rebase main
# After rebase: D and E are RECREATED with new hashes
# A -- B -- C (main) -- D' -- E' (feature)
# \
# D -- E (old, unreachable, but still exists!)
# Old commits D and E still exist in object store
# They are unreachable from any branch but exist in reflog
# See the old chain
git reflog
# abc1234 HEAD@{0}: rebase finished: refs/heads/feature onto c3d4e5f
# def5678 HEAD@{1}: commit: E ← old commit E's hash
# Recover old chain if needed
git reset --hard def5678 # back to pre-rebase state
Lo kar liya — Key Points:
- ✅ Each commit stores the SHA-1 hash of its parent commit(s), forming a chain — this IS the history
- ✅ Regular commits have 1 parent, root commits have 0 parents, merge commits have 2+ parents
- ✅ A commit's hash is computed from ALL its fields — tree, parent(s), author, committer, message, timestamps
- ✅ Changing ANY field in a commit (even the message) creates a completely new hash
- ✅ Branches are just text files containing a commit hash — creating a branch writes 41 bytes
- ✅ HEAD usually points to a branch name, which points to a commit — this layered pointer system enables efficient branching
- ✅ Rewriting history (rebase, amend) creates new commits — old commits become unreachable but survive in reflog
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login