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.
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 addactually does - What a commit contains
- How Git represents files internally
- How commits form a graph
- What branch references are
- What
git cloneandgit pushactually 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
.gitdirectory: 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.
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
- Working tree: what is currently on disk.
- Index: the content currently selected for the next commit.
- 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
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.
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.
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.
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.
The Git mental model
At this point, you should be able to reduce much of Git to four concepts:
- Working tree: the files you are currently editing.
- Index: the snapshot you are preparing for the next commit.
- Objects and commits: the data Git stores to represent project history.
- 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.
- In your own words, what is the difference between the working tree, index, and commit?
- What does
git adddo to the index? - Why can a file be both staged and modified at the same time?
- What information does a commit contain?
- Why does changing a commit or its parent change its object ID?
- What is a blob?
- What is a tree?
- What is
refs/heads/mainconceptually? - What does
HEADnormally identify? - Why can
git addandgit commitwork without a network connection? - What happens conceptually when you run
git push? - Why can a push be rejected when the remote branch has advanced?
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


.png)
.png)
.png)

Follow Me