datarekha

Staging & commits

How to stage changes, write good commit messages, and build a history that makes future debugging sane.

8 min read Beginner Git Lesson 4 of 15

What you'll learn

  • What a commit really is: a snapshot, a parent pointer, and a SHA hash
  • How to stage exactly what you want with git add, git add ., and git add -p
  • Writing atomic commits and imperative messages that teammates (and future-you) will thank you for

Before you start

The last lesson promised two things this one delivers: that staging can be surgical — down to individual lines of a single file — and that a commit message can be written so your future self thanks you for it. Both rest on understanding what a commit really is, so start there, then work outward to the commands.

What a commit actually is

A commit is a complete snapshot of every tracked file at a point in time, plus three pieces of metadata that turn it into a node in a graph:

  • A SHA-1 hash (usually shown as the first 7 characters, like a1b2c3f) — a unique address derived from the content itself. Change anything and the hash changes.
  • A parent pointer — the SHA of the commit that came before this one. That chain of pointers is your project history.
  • Author, timestamp, and message — who made the change, when, and why.

Here is a three-commit chain. Each box is a commit. The arrow means “my parent is”:

a1b2c3fAdd login pageC3 (HEAD)9f3e1a2Add auth middlewareC27c4d8b0Init projectC1
C3 → C2 → C1. Each commit knows its parent; Git reads this chain as history.

The staging area: why it exists

Before a commit, files pass through the staging area (also called the index). Think of it as a draft tray: you choose exactly which changes go in this commit, even if you have edits across a dozen files.

git status
On branch main
Changes not staged for commit:
  modified:   src/auth.py
  modified:   src/models.py
Untracked files:
  notes.txt

You decide only auth.py belongs in this commit. You stage it:

git add src/auth.py
git status
Changes to be committed:
  modified:   src/auth.py

Changes not staged for commit:
  modified:   src/models.py
Untracked files:
  notes.txt

models.py and notes.txt are untouched — they will not be in this commit.

Staging commands

Stage a single file — the most surgical choice:

git add src/auth.py

Stage everything in the working tree — fast but broad:

git add .

Stage selected hunks inside a file (-p for patch mode) — the most precise option:

git add -p src/auth.py

Git shows each hunk (a contiguous block of changes) one at a time and asks what to do:

@@ -12,6 +12,10 @@ def authenticate(user, password):
+    if not user:
+        raise ValueError("user required")
     return check_hash(password, user.password_hash)

Stage this hunk [y,n,q,a,d,s,e,?]?

Type y to stage it, n to skip it, s to split it into smaller hunks, q to quit. This lets one file’s changes land in two separate, focused commits — invaluable when you fix a bug and refactor in the same session.

Inspect before you commit

Making the commit

Short message — fine for a small, obvious change:

git commit -m "Raise ValueError when user is missing in authenticate"

Long message — the preferred form for anything non-trivial. Running git commit without -m opens your editor. The convention is a subject line under 72 characters, a blank line, then a body:

Add input validation to authenticate()

Previously the function would call check_hash with a None user,
producing a cryptic AttributeError deep in the hash library.
Now it raises ValueError immediately with a clear message.

Fixes #214.

Writing good messages

Use imperative mood for the subject: “Add”, “Fix”, “Remove”, “Refactor” — not “Added”, “Fixing”, “Removes”. Git itself uses this convention (Merge branch 'main', Revert "Add login page"), so your messages read naturally alongside it.

The subject says what. The body says why — the context that will not be obvious from the diff alone. What problem did this solve? What alternative did you consider and reject? That reasoning is exactly what reviewers and future debuggers need.

Atomic commits

An atomic commit captures one logical change. Not one file, not one function — one idea.

Why it matters in practice:

  • Reverting is clean. git revert <sha> undoes exactly one thing without collateral damage.
  • Bisecting works. git bisect steps through commits to find which one introduced a bug. If each commit is one change, bisect narrows it down fast.
  • Code review is faster. A reviewer can read a 30-line atomic commit in two minutes. A 600-line “weekly dump” commit takes an afternoon.

If you find yourself writing “and” in the subject line (“Fix login bug and update styles”), split it into two commits.

The -am shortcut

git commit -am "Fix null check in authenticate"

The -a flag automatically stages every already-tracked file before committing, skipping git add. It is a handy shortcut when you are iterating on existing files and you want to stage everything.

Its limit: -a does not add brand-new untracked files. Those still require an explicit git add. If you have a new file that must go with this commit and you use -am, the new file will be silently left out.

Putting it together

A typical workflow for a focused change:

git status                    # see what changed
git add -p src/auth.py        # stage only the relevant hunks
git diff --staged             # confirm what's about to be committed
git commit -m "Raise ValueError when user is missing in authenticate"
git status                    # working tree should be clean (or show remaining work)
On branch main
nothing to commit, working tree clean

In one breath

A commit is a content-addressed snapshot — a SHA hash of everything in it, a parent pointer to the previous commit (that chain is your history), plus your name, time, and message. You build each one through the staging area: git add file for one file, git add . for everything (risky — it sweeps in secrets and junk, so glance at git status first), or git add -p to stage individual hunks within a file. Verify with git diff --staged, then commit with an imperative subject (“Add”, “Fix”) under 72 characters and a body that explains why. Keep commits atomic — one logical idea each — because that is exactly what makes revert, bisect, and code review fast instead of painful.

Practice

Before the quiz, design the commit sequence: in one session you fixed a null-check bug in auth.py and, in the same file, also reformatted an unrelated function — and you created a brand-new helpers.py. You want the bug fix and the reformat as two separate commits, and helpers.py in a third. Which staging commands get you there, and why would a quick git commit -am quietly leave helpers.py out entirely?

Quick check

0/3
Q1What does a commit's SHA-1 hash represent?
Q2You edited three files but only want two of them in this commit. Which command is most precise?
Q3Your teammate bisects a regression to a commit titled 'misc fixes and refactors (2 weeks of work)'. What does this reveal about the commit strategy — and what should have been done instead?

A question to carry forward

You can now build a clean, atomic history — each commit a single idea with a message that explains itself. But a history you cannot read is just an archive. The moment a bug appears, the questions come fast: what changed in the last ten commits? Who touched this exact line, and in which commit? What did this file look like three commits ago? Git has recorded every answer — but recording is not the same as showing. So how do you actually interrogate the history you have been so carefully building: log it, diff it, trace a single line back to the commit that wrote it? That is the next lesson.

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Related lessons

Explore further

Cheat sheets
Skip to content