Git Basics #2: add, commit, status — What the Staging Area Means

8 min read

In the last post we built the two core models of Git: a commit is a snapshot, and a file is recorded by passing through the three areas — working directory, staging area, repository. This post verifies that model with your own hands. We create real files in the hello-git repository from last time, run status, add, commit, and diff in order, and finish by using .gitignore to keep certain files out of history.

It runs in six parts.

  • #1 What Git is — the snapshot model and the three areas
  • #2 add, commit, status — what the staging area means ← this post
  • #3 Branching and merging — fast-forward and 3-way
  • #4 Remotes — clone, fetch, pull, push, and what origin really is
  • #5 Undoing changes — restore, reset, and revert
  • #6 GitHub and your first Pull Request

By the end of this post you will be able to run Git’s most basic loop on your own: edit, pick, record — one full cycle.

Creating the first file #

Start inside the hello-git directory. Create a file.

Creating the first file
cd hello-git
echo "print('hello git')" > app.py

We only created a file — no Git command has run yet. Let us first check how Git sees this situation.

git status — reading the state #

git status summarizes the current state of the three areas. Whatever you are working on, this will be the command you run most often.

git status — untracked
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	app.py

nothing added to commit but untracked files present (use "git add" to track)

app.py appears under Untracked files. Untracked means a new file that exists in the working directory but that Git is not yet tracking. Git never records any file on its own without being told to, so every new file starts out untracked.

Helpfully, the output already tells you the next command. Put the file under tracking with git add.

Staging
git add app.py
git status
git status — staged
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   app.py

This time it moved under Changes to be committed. The file is now staged — placed on the staging area, the list of what will go into the next commit. It is not recorded yet; it is scheduled to be recorded.

The third state, modified, only appears once at least one commit exists, so let us make the commit first and come back.

git commit — the first snapshot #

First commit
git commit -m "Add greeting script"
# [main (root-commit) 3f2b1a9] Add greeting script
#  1 file changed, 1 insertion(+)
#  create mode 100644 app.py

The string after -m is the commit message. With this commit, what was on the staging area has been permanently recorded into the repository as a snapshot. The 3f2b1a9 in the output is the leading part of the commit’s unique identifier (hash); you will see a different value every time.

Commit messages are worth getting right from the start. Two basic principles apply.

  • Summarize what you did in a one-line subject. Messages like “fix stuff” or “wip” carry no information when you browse the history later.
  • Put one change in one commit. If a bug fix and a new feature share a commit, undoing just one of them later becomes hard. #5 will show exactly why this principle pays off.

Now edit the file once to see the third state.

Editing a committed file
echo "print('bye git')" >> app.py
git status
git status — modified
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   app.py

no changes added to commit (use "git add" and "git commit -a" to use)

Changes not staged for commit — this is the modified state. A file Git is tracking has changed since the last commit but has not been staged yet. To sum up, a file cycles through these states: a new file starts as untracked, becomes staged after git add, and returns to a clean state once recorded with git commit. Edit it again and it becomes modified; add it again and it is staged once more.

Note
A file can be staged and modified at the same time. If you edit the same file again after adding it, the content at the moment of add stays on the staging area while the later edits remain in the working directory, and status shows the file on two lines. It is a good demonstration that the staging area holds the content at the moment of add, not the file itself.

Why does the staging area exist? #

Recording everything you changed would seem simpler, so why the add step in between? The answer: to design the unit of a commit.

In real work, changes of different kinds pile up in the working directory. Say you fix a bug, correct a typo you happened to notice along the way, and also touch a config file. Put all three into one commit and the one-change-per-commit principle breaks. With a staging area you can add only the bug-fix files and commit, then add only the typo fix and commit — picking what goes in. History stays cleanly split by change, which makes both undoing (#5) and reviewing far easier.

You can pick at a finer level than whole files. git add -p walks through the changes inside a file hunk by hunk, asking about each one.

Staging changes hunk by hunk
git add -p app.py
# @@ -1 +1,2 @@
#  print('hello git')
# +print('bye git')
# (1/1) Stage this hunk [y,n,q,a,d,e,?]? y

y stages the hunk, n skips it. Useful when two different pieces of work ended up in one file.

git diff — what actually changed #

If status tells you which area holds what, diff shows you how the content changed. Two forms with different comparison targets need to be kept apart.

CommandComparesQuestion it answers
git diffworking directory ↔ staging areaWhat have I edited but not added yet?
git diff --stagedstaging area ↔ last commitWhat will be recorded if I commit now?

Make it a habit to check git diff --staged right before committing. Confirming with your own eyes what is about to be recorded prevents most accidents where unintended changes slip into a commit.

Commit the remaining edit and check the history.

Commit and history
git add app.py
git commit -m "Add farewell line"
git log --oneline
# 8c4d2e7 (HEAD -> main) Add farewell line
# 3f2b1a9 Add greeting script

git log shows the commit chain, newest first. --oneline compresses each commit to a single line, and in this output you can watch the chain of snapshots from the last post actually growing.

.gitignore — files that should not be recorded #

A project folder also collects files that do not need to be in history, or must never be. Three typical groups stand out.

  • Things you can rebuild — build artifacts (dist/, build/), caches (__pycache__/)
  • Things you can download again — dependency directories (node_modules/, .venv/)
  • Things that must never be shared — secret keys, tokens, local environment files (.env)

Create a file named .gitignore at the project root and write patterns in it. Matching files no longer appear in the untracked list and are not swept up by bulk commands like git add ..

.gitignore example
# dependencies
node_modules/
.venv/

# build artifacts and caches
dist/
build/
__pycache__/

# secrets and local environment
.env
*.pem

# OS and editor artifacts
.DS_Store

.gitignore itself is committed and shared with the whole team. One caveat: a file that is already tracked keeps being tracked even after you add it to .gitignore. In that case run git rm --cached <file> to untrack it, then commit.

Tip
Once a secret is committed even once, it stays in the history even if you delete it in a later commit. The most reliable prevention is a fixed order of operations: register it in .gitignore before ever committing.

Wrapping up #

In this post we completed one full cycle of Git’s basic loop.

  • git status — read the three states: untracked, staged, modified.
  • git add — pick the changes for the next commit and place them on the staging area. The staging area exists to design commit units.
  • git commit — record the content of the staging area as a snapshot. One change per commit.
  • git diff and git diff --staged — inspect content before add and before commit.
  • .gitignore — keep artifacts, dependencies, and secrets out of history.

In the next post, “Git Basics #3: Branching and Merging — Fast-forward and 3-way”, we cover branches — the place where Git’s snapshot model really pays off. We will see why a branch is nothing more than a single pointer, and how two diverged lines of work come back together.

X