How Git Actually Works: Repositories, the Index, Commits, and References

Git mastery · Stage 1 of 12

Git becomes much easier once you stop thinking of it as a collection of commands and start thinking of it as a database of snapshots connected by references.

If you already know git init, git clone, git add, git commit, and git push, you already have enough practical experience to begin understanding what Git is actually doing underneath those commands.

This stage builds that mental model. The goal is not to memorize more commands. The goal is to understand the objects and relationships that make the commands work.

Prerequisites

You should already be comfortable using git init, git clone, git add, git commit, and git push.

What you will learn

  • What a Git repository actually contains
  • The difference between the working tree, index, and commits
  • What git add actually does
  • What a commit contains
  • How Git represents files internally
  • How commits form a graph
  • What branch references are
  • What git clone and git push actually do

1.1 What is a repository, really?

A Git repository is not simply "the folder containing your project." A typical repository has two closely related parts:

  • The working tree: the files and directories you currently see and edit.
  • The .git directory: Git's local database containing its history, references, configuration, and other repository data.

The working tree is where you work. The .git directory is where Git keeps the information it needs to track that work.

Delete .git and your files remain, but Git no longer treats the directory as a repository. If the repository's objects are intact, however, Git can reconstruct previously committed versions of the project without needing the working tree.

This gives git init and git clone a different meaning:

git init
→ creates the repository database in .git

git clone URL
→ creates a local repository from another repository
→ creates a working tree containing a checked-out snapshot

Neither operation fundamentally depends on GitHub or another hosting service. Git itself is a local version-control system. A remote repository is an additional layer.

Diagram showing the working tree and .git directory.

1.2 The working tree

The working tree is the version of the project currently present on disk. When you open app.py in your editor and change a line, you are changing the working tree.

At that point, Git has not automatically created a commit or updated the index. You have simply changed a file on disk.

This is why git status is so useful. It reports the differences between the working tree, the index, and HEAD, as well as other repository state such as untracked files.

The three states you need to distinguish

  1. Working tree: what is currently on disk.
  2. Index: the content currently selected for the next commit.
  3. HEAD: the commit currently checked out, whose snapshot provides the baseline for comparison.

Once these three states are distinct in your head, git add and git commit become much easier to reason about.

Working tree
     |
     | git add
     v
   Index
     |
     | git commit
     v
   Commit

1.3 The index: what git add really does

The index is also called the staging area. It is stored by default in .git/index.

When you run:

git add file.txt

Git updates the index using the current contents of file.txt in the working tree. It does not commit the file. It prepares that version of the file to be included in the next commit.

Working tree             Index                  Commit
┌──────────────┐        ┌──────────────┐       ┌──────────────┐
│ file.txt     │        │ file.txt     │       │ file.txt     │
│ current      │ ─────► │ selected     │ ─────►│ recorded     │
│ contents     │ add    │ contents     │commit │ snapshot     │
└──────────────┘        └──────────────┘       └──────────────┘

This also explains a common situation:

git add file.txt

# edit file.txt again

git status

The index still contains the version that existed when you ran git add. Your working tree now contains a newer version. Git can therefore report the same file as both staged and modified.

To stage the newer changes, run git add file.txt again.

Why have an index at all?

The index lets you construct the next commit deliberately. Suppose you changed three files:

auth.py
map.py
README.md

You might want to commit only the authentication work. The index lets you select exactly what belongs in the next snapshot.

You can even stage individual parts of a file with:

git add -p
Common misconception

git add does not mean "save my changes." Your editor or operating system has already saved the file to the working tree. git add updates Git's index with the version you want to prepare for the next commit.

1.4 Commits: snapshots, not diffs

A useful mental model is that a commit represents a complete snapshot of the tracked project at a particular point in time.

Git does not necessarily store a completely independent physical copy of every file for every commit. It reuses identical objects and stores the objects necessary to represent each snapshot. The logical model, however, is a complete project snapshot.

A commit object contains information such as:

  • a reference to a tree representing the project snapshot
  • the author and committer metadata
  • a parent commit reference, or multiple parent references for a merge
  • the commit message

Git then identifies the resulting object using an object ID, historically a SHA-1 hash and, in repositories configured for SHA-256, a SHA-256 hash.

The object ID is not an extra field stored inside the commit. It is derived from the object's contents. Because the commit refers to its parent, changing an earlier commit changes the data that follows it and therefore changes the resulting object IDs.


Diagram showing a commit and its metadata, tree and parent.

1.5 A glimpse under the hood: Git objects

You do not need to understand Git's storage implementation yet, but seeing the basic data model helps explain what commands such as git cat-file are showing you.

Git's main object types are:

  • Blob: stores file contents.
  • Tree: represents a directory structure by associating names and modes with blobs and other trees.
  • Commit: points to a tree and records metadata and parent commit references.
  • Tag: an object used for annotated tags.

A simplified representation looks like this:

commit
  |
  +---- tree
          |
          +---- README.md → blob
          |
          +---- src/      → tree
                           |
                           +---- main.py → blob

So when you inspect a commit, you can follow the references:

commit → tree → blob

The blob contains the actual file content. The tree tells Git which name points to that blob and how the directory structure is arranged. The commit points to the tree and connects that snapshot to the history.

Git objects are immutable. Once an object exists, changing its contents would produce a different object ID.

1.6 Commit ancestry: the commit graph

Every normal commit points to its parent. This means Git history is naturally represented as a graph rather than simply as a list.

A ← B ← C ← D

A = first commit
D = newest commit

The arrows point backward because each commit stores a reference to the commit that came before it.

The first commit has no parent because nothing came before it in that repository's history. It is the root commit.

A commit can also have multiple parents. This is what happens with a merge commit, which will become important in Stage 4.


Commit graph diagram showing parent relationships.

Notice that nothing about this graph requires branches. Commits have parents regardless of whether a branch name points to them.

Branches are references into this graph. That is the subject of Stage 2.

1.7 What git init actually creates

Create an empty directory and run:

git init

You can then inspect the .git directory.

Path Purpose
objects/ Git's object database.
refs/ References such as branches and tags.
HEAD Identifies the current checkout, normally through a symbolic reference to a branch.
index The staging area used to construct the next commit.
config Repository-specific configuration.

Immediately after git init, there may be no commit objects and no branch commit for refs/heads/ to point to. The exact files created inside .git can vary by Git version and repository configuration, so do not treat the directory listing above as an exact fixed structure.

The important point is that the repository exists locally. Nothing has been sent to GitHub or any other server.

1.8 What git clone actually does

When you run:

git clone <url>

Git creates a new local repository based on the source repository and checks out an initial working-tree state. A normal clone gives you local Git objects representing the history available to the clone, along with a remote configuration and remote-tracking references.

It also records the source repository as a remote named origin by default.

After cloning, your local repository is independent. You can create commits without network access. Commands such as git add, git commit, and git log operate locally.

Network communication becomes necessary when you need to exchange information with a remote, such as when fetching or pushing.

Common misconception

Cloning a repository does not leave you editing files "on GitHub." You have your own local repository. GitHub or another hosting service is a remote repository that you can synchronize with.

1.9 What git push actually sends

A useful way to think about git push is as an operation on objects and references.

Suppose your local history looks like this:

Remote:
A ← B

Local:
A ← B ← C ← D

Your local repository has commits C and D that the remote does not have. A push transfers the objects required by the remote and asks the remote to update its corresponding reference.

Conceptually:

Before push:

remote main → B

After push:

remote main → D

You are therefore not simply "uploading the current folder." Git is transferring repository objects and updating a reference.

This also explains why a push can be rejected. If the remote branch has advanced in a way that your local history does not contain, Git may refuse to move the remote reference because doing so could discard commits already present on the remote.

The exact mechanics of fetching, fast-forward updates, non-fast-forward pushes, and merging will be covered in Stage 3 and Stage 4.

1.10 References: what a branch name actually is

A reference, or ref, is a named pointer into Git's object graph.

For example, a branch such as main is represented by a reference under refs/heads/main.

refs/heads/main
        |
        v
    commit D
        |
        v
    commit C
        |
        v
    commit B

In a traditional loose-ref representation, .git/refs/heads/main contains the object ID of the commit at the tip of the branch. Git can also store references in packed form, so do not assume every reference will always appear as an individual file in .git/refs.

This is why the phrase "a branch is a pointer" is useful. A branch is not a container holding a separate copy of your commits. It is a name that identifies a point in the existing commit graph.

HEAD normally identifies the branch you currently have checked out. In that common case, it is a symbolic reference to something like refs/heads/main.

Stage 2 will build the entire branching model from this idea.


Diagram showing HEAD, a branch reference and the commit graph.

The Git mental model

At this point, you should be able to reduce much of Git to four concepts:

  1. Working tree: the files you are currently editing.
  2. Index: the snapshot you are preparing for the next commit.
  3. Objects and commits: the data Git stores to represent project history.
  4. References: names that point into that history.

The basic flow is:

Edit files
    ↓
Working tree
    ↓ git add
Index
    ↓ git commit
Commit
    ↓
Commit graph
    ↑
References such as branches and tags

Once this model is clear, Git commands stop looking like unrelated operations. Most commands simply manipulate one of these components or move information between them.

Stage 1 labs

Do these exercises in a scratch repository you do not mind deleting afterward. The purpose is to observe Git rather than merely read about it.

Lab 1: What does an empty repository actually contain?

Problem: You have run git init many times but never inspected what it creates.

Do:

mkdir git-stage1
cd git-stage1
git init

Now inspect the .git directory using your file browser or:

ls -la .git

On Windows PowerShell, you can use:

Get-ChildItem -Force .git

Observe:

  • Find HEAD.
  • Find objects/.
  • Find refs/.
  • Inspect the contents of HEAD.

Explain: Why are there no commits yet? What would a branch reference point to if no commit exists?

Lab 2: Watch the three states diverge

Problem: You want to see the working tree and index contain different versions of the same file.

Do:

echo "hello" > hello.txt
git status

git add hello.txt
git status

echo "world" >> hello.txt
git status

Observe:

The same file should appear in two different categories. The index contains the version that was staged. The working tree contains the newer version.

Explain: Why can the same file simultaneously be staged and modified?

Lab 3: Look at a commit as data

Problem: You want to see what a commit actually contains instead of treating it as a black box.

Do:

git add hello.txt
git commit -m "first commit"
git log --oneline

Copy the full commit ID and inspect it:

git cat-file -p <commit-id>

You should see information similar to:

tree 8a7f...
author Your Name <you@example.com> ...
committer Your Name <you@example.com> ...

first commit

There is no parent because this is the first commit.

Copy the tree ID and inspect it:

git cat-file -p <tree-id>

Find the blob associated with hello.txt and inspect it:

git cat-file -p <blob-id>

Explain: Which object contains the actual text of hello.txt?

Lab 4: Build a commit graph

Problem: You want to see history as a graph rather than a sequence of messages.

Do:

echo "second line" >> hello.txt
git add hello.txt
git commit -m "add second line"

echo "third line" >> hello.txt
git add hello.txt
git commit -m "add third line"

echo "fourth line" >> hello.txt
git add hello.txt
git commit -m "add fourth line"

git log --oneline
git log --oneline --graph

Observe:

* d4e5f6g add fourth line
* c3d4e5f add third line
* b2c3d4e add second line
* a1b2c3d first commit

Explain: Why does the first commit have no parent while every later commit does?

Lab 5: Inspect a branch reference

Problem: You want to verify that a branch name points to a commit.

Do:

git log --oneline -1
git rev-parse HEAD

If your repository uses a loose reference for main, you can also inspect:

cat .git/refs/heads/main

On Windows:

type .git\refs\heads\main

Note that the direct file may not exist if the reference has been packed or if your branch has another name. In that case, git rev-parse main is the safer way to ask Git where the branch points.

Explain: What changes about the branch reference when you create a new commit while on that branch?

Lab 6: Clone something and inspect what arrived

Problem: You want to understand what a clone gives you before making any changes.

Do:

git clone <public-repository-url>
cd <repository-directory>

git log --oneline
git remote -v
git branch -r

Inspect .git/config and find the remote named origin.

Explain: Why can you inspect the repository's history immediately after cloning, even if you disconnect from the network?

Lab 7: Push and watch a reference move

Problem: You want to connect git push to Git's object-and-reference model.

Do:

Create an empty repository on GitHub or another Git hosting service. In a local repository containing a couple of commits:

git remote add origin <repository-url>
git push -u origin main

Compare the latest local commit:

git log --oneline -1

with the commit shown by the remote repository.

Explain: What does the matching commit ID tell you about what Git transferred?

Stage 1 self-assessment

Before moving to Stage 2, answer these without looking back at the tutorial.

  1. In your own words, what is the difference between the working tree, index, and commit?
  2. What does git add do to the index?
  3. Why can a file be both staged and modified at the same time?
  4. What information does a commit contain?
  5. Why does changing a commit or its parent change its object ID?
  6. What is a blob?
  7. What is a tree?
  8. What is refs/heads/main conceptually?
  9. What does HEAD normally identify?
  10. Why can git add and git commit work without a network connection?
  11. What happens conceptually when you run git push?
  12. Why can a push be rejected when the remote branch has advanced?
The test

If you can explain these concepts without relying on command memorization, you are ready for branches and HEAD.

What's next: branches and HEAD

Stage 2 starts with a practical problem:

I need to work on a feature without changing the production branch.

The solution becomes much simpler once the Stage 1 model is clear.

A branch is a movable reference into the commit graph. HEAD identifies the branch or commit you are currently working from. Creating and switching branches therefore becomes a matter of moving references and changing which snapshot is checked out, rather than copying an entire project.

That is where Stage 2 begins.

Git mastery

Stage 1 of 12: Git foundations

Next: Branches and HEAD