Git Basics #2: add, commit, status — What the Staging Area Means
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.
cd hello-git
echo "print('hello git')" > app.pyWe 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.
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.
git add app.py
git statusOn branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: app.pyThis 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 #
git commit -m "Add greeting script"
# [main (root-commit) 3f2b1a9] Add greeting script
# 1 file changed, 1 insertion(+)
# create mode 100644 app.pyThe 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.
echo "print('bye git')" >> app.py
git statusOn 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.
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.
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,?]? yy 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.
| Command | Compares | Question it answers |
|---|---|---|
git diff | working directory ↔ staging area | What have I edited but not added yet? |
git diff --staged | staging area ↔ last commit | What 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.
git add app.py
git commit -m "Add farewell line"
git log --oneline
# 8c4d2e7 (HEAD -> main) Add farewell line
# 3f2b1a9 Add greeting scriptgit 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 ..
# 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.
.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 diffandgit 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.