How Git Actually Thinks (And Why Most Developers Have It Wrong)

Published: (March 15, 2026 at 04:25 AM EDT)
8 min read
Source: Dev.to

Source: Dev.to

Part 1 of the Git Mastery Series

Here’s a conversation that happens on every development team, roughly once a month:

  • Someone runs git reset --hard when they meant something else.
  • They rebase and the history looks completely wrong.
  • They merge a branch and can’t figure out why certain changes didn’t come through.

And then they type something into a chat: “I think I broke Git.”

You can’t break Git. But you can absolutely confuse yourself when you’re working with a mental model that doesn’t match what Git is actually doing.

Most Git tutorials teach you commands. Very few teach you how Git thinks. That’s the gap this article closes — because once the model clicks, the commands stop being incantations you copy from Stack Overflow and start being decisions you make intentionally.

This is the most important thing to understand about Git, and it’s the thing most tutorials either skip or bury in chapter 10.

When you run git commit, Git does not store “what changed since last time.” It stores a complete snapshot of every tracked file in your project at that moment. If a file didn’t change, Git doesn’t duplicate it — it just points to the same content from the previous snapshot. Conceptually, each commit is a full picture of your project, not a list of changes.

Why does this matter?

Because it explains almost everything that confuses people.

  • When you cherry‑pick a commit, Git isn’t “moving” changes — it’s applying the diff between that commit and its parent onto your current state.
  • When you rebase, Git isn’t “moving commits” — it’s replaying a series of diffs on top of a new base.
  • When you reset, you’re moving a pointer to a different snapshot. Nothing is actually deleted until Git’s garbage collector runs.

This is why Git is so powerful — and why operations that sound destructive usually aren’t.

The three areas of Git

AreaWhat it isWhat it does
Working DirectoryYour actual files — what you see in your editor, what you can run and test.Git is aware of this area but doesn’t manage it directly.
Staging Area (Index)Where you prepare your next commit.git add moves changes into a waiting room, saying “include this in the next snapshot.”
Repository (.git folder)Where commits live permanently.git commit takes whatever is in the staging area and wraps it into a new snapshot.

Why this matters

Most developers use git add . and git commit -m "…" as a single motion, without thinking about the staging area at all. That works fine until you need to:

  • Commit part of a file
  • Undo only part of your changes
  • Figure out why your commit contains something you didn’t intend

At that point, the model saves you.

See the difference between all three areas at once

git diff           # Working directory vs staging area
git diff --staged  # Staging area vs last commit
git status         # Overview of all three areas

Run these after making some changes. Read the output carefully. You’ll immediately see which changes are “in limbo” and which are committed.

Branches are just pointers

A Git branch sounds like a complex thing — a parallel universe of code, a separate track of development. The implementation is almost comically simple: a branch is a text file containing a 40‑character commit hash. That’s it.

  • A branch is a pointer to a commit.
  • When you make a new commit on a branch, the pointer moves forward to the new commit.
  • When you create a branch, Git copies that pointer to a new file. No copying of code, no parallel universe — just a pointer.

See exactly what a branch is

cat .git/refs/heads/main
# Output: a3f8c9d1e2b4f6a8c0d2e4f6a8b0c2d4e6f8a0b2

That hash is your entire branch. The branch name is just a human‑readable alias for that commit.

Consequences

  • Creating a branch is free — it’s instantaneous because you’re just writing a file.
  • Switching branches is fast because you’re just moving a pointer and updating your working directory to match the target snapshot.

“I’ll create a branch for this” should never feel like a heavy decision.

HEAD: where you are right now

HEAD is one file. It contains either a branch name or a commit hash. It answers one question: “Where am I right now?”

When you’re on main and you run git log, you see the history of main because HEAD points to main, which points to its latest commit. When you commit, HEAD moves forward automatically.

cat .git/HEAD
# Output: ref: refs/heads/main

When HEAD contains a branch name, you’re in normal mode.
When HEAD contains a commit hash directly — not a branch name — you’re in detached HEAD state.

Detached HEAD sounds alarming, but it just means you’re looking at a specific commit in history rather than a branch. If you make commits in this state, they’ll eventually be garbage‑collected because no branch pointer moves with you. To keep work done in detached HEAD, create a branch:

git switch -c my-new-branch

Commits and their internal structure

Every commit (except the very first) points to its parent. That’s how Git knows what “came before.” A commit is an object containing:

  • A pointer to a snapshot (tree)
  • A pointer to the parent commit(s)
  • The commit message
  • Author and timestamp

See the raw contents of any commit

git cat-file -p HEAD
# Output:
# tree 8a3f2d9c...
# parent 1b4e7a2f...
# author Shakil  1709123456 +0530
# committer Shakil  1709123456 +0530
#
# feat(auth): add OTP login

The chain of parent pointers is your history. When you run git log, Git starts at HEAD and follows the parent chain backward. When you run git merge, Git finds the common ancestor by walking backward through both chains.

This is why Git operations that seem magical — finding merge conflicts, showing blame, running bisect — are actually mechanical. Git is just traversing a linked list.

Recoverability

Most Git anxiety comes from not knowing what’s recoverable. Here’s the truth: almost everything is recoverable. Commits don’t get deleted when you reset — they become unreferenced. They still exist in the object database until Git’s garbage collector prunes them.

(The original text ended abruptly here; the core message is that Git retains data until it is explicitly cleaned up.)

Recovering Deleted Commits

git reflog shows every position HEAD has ever been at, including commits that no branch currently points to. You have roughly a 90‑day window to recover anything before garbage collection runs.

# See everything HEAD has ever pointed to
git reflog

Sample output

a3f8c9d HEAD@{0}: commit: fix login bug
1b4e7a2 HEAD@{1}: reset: moving to HEAD~1
c5d2f8a HEAD@{2}: commit: add OTP login

The commit you “deleted” with git reset --hard is still present at HEAD@{1}. Restore it:

git reset --hard HEAD@{1}

The Mental Shift

Git operations stop feeling fragile once you view Git as a database of snapshots:

  • Commits – immutable snapshots that know their parent(s).
  • Branches & HEAD – just pointers into that database.
  • Working directory – your view of one snapshot.
  • Staging area – where you compose the next snapshot.

When you understand that:

  • Commits are permanent.
  • Branches are movable.
  • HEAD is merely “where you are now”.

…Git becomes a tool you can reason about rather than a collection of scary commands.

All Git commands manipulate snapshots, pointers, or the three areas (working tree, index, repository). Commands that often confuse people—rebase, reset, cherry-pick, reflog—make immediate sense once you can picture their effect on the graph.

Hands‑On Exercise

Open a new folder (or a test project) and follow the steps below, reading each output carefully:

# Initialise a repository
git init

# Create a file
echo "hello" > file.txt

# Show untracked file
git status                    # → untracked file

# Stage the file
git add file.txt

# Show staged file
git status                    # → staged file

# Commit it
git commit -m "first commit"

# View the commit
git log --oneline             # → see the commit hash and message

Create and inspect a branch

git branch feature            # create branch
cat .git/refs/heads/feature    # → just a hash
cat .git/HEAD                  # → shows current ref (e.g., ref: refs/heads/master)

git switch feature            # switch to the new branch
cat .git/HEAD                  # → now points to refs/heads/feature

Make a change and explore diffs

echo "world" >> file.txt
git diff                      # working directory vs. index (unstaged changes)

git add file.txt
git diff                      # nothing – changes are staged

git diff --staged             # index vs. last commit

Commit on the feature branch

git commit -m "add world"
git log --oneline --all --graph  # visualize the branch split

Doing this deliberately—reading each command’s output and asking what changed and why—builds a solid mental model faster than any amount of passive reading.

Next: Part 2 – Committing with Intention: The Art of a Good Commit

If you found this useful, I’ve turned the whole series into a 23‑page PDF reference that includes:

  • Checklists
  • Hook templates
  • 80+ useful commands
  • Deep dives on reflog & bisect
  • A recovery playbook for 12 real‑world emergencies

Git Mastery Field Guide → (link placeholder)

0 views
Back to Blog

Related posts

Read more »

Branching Without Fear

Part 3 of the Git Mastery Series ← Part 2: Committing with Intentionhttps://dev.to/itxshakil/committing-with-intention-the-art-of-a-good-commit-p90 | Part 4: C...