git add and the Staging Area

Before a change you make to a file becomes part of your project’s permanent history, Git asks you to explicitly choose what goes into the next commit. That’s the job of git add: it moves changes from your working directory into a holding area called the staging area (or index), so you decide precisely what a commit contains rather than blindly snapshotting everything that changed. Understanding staging is the single most important mental model for using Git well — it’s the difference between committing exactly what you mean to and committing a confusing mess of unrelated edits.

Overview: What the Staging Area Actually Is

Git tracks three different views of your project at any moment: the working directory (the actual files on disk that you edit), the staging area (also called the index), and the repository (the committed history, stored as objects in .git). When you edit a file, that change exists only in the working directory. Running git add copies a snapshot of that file’s current content into the index. Running git commit then takes whatever is in the index — not whatever is on disk — and seals it into a new commit object.

This might sound like a redundant middle step, but it’s what gives Git its power to build commits deliberately. If you’ve changed five files but only three of those changes belong together logically, you can git add just those three, commit them with a message that accurately describes them, and then stage and commit the remaining two separately. Without a staging area, every commit would have to be “everything that’s currently different,” which makes for messy, hard-to-review history.

What Happens at the Object Level

Internally, when you run git add <file>, Git reads the file’s current contents, compresses them, and writes a new blob object into .git/objects, identified by a hash of its content (SHA-1 in most repositories today, SHA-256 in newer ones). The index file (.git/index) is then updated to record that this path now points at that blob’s hash, along with metadata like file mode and modification time. No tree object and no commit object are created yet — those only get built when you run git commit. At that point Git turns the current state of the index into one or more tree objects (a snapshot of the whole directory structure), wraps a reference to the top-level tree, the parent commit, the author, and the message into a new commit object, and moves the current branch pointer to point at that new commit.

This is also why staging is cheap even for content you’ve staged before: Git stores content, not diffs, at the blob level, so if the exact same file content has already been stored as an object, Git just reuses it and updates the index pointer instead of writing anything new.

Syntax

git add "<file-or-directory>"

Where the argument is one or more file paths, directory paths, or glob patterns. Common forms and flags:

Form What it stages
git add file.txt Stages that single file’s current content.
git add dir/ Stages every new, modified, or deleted file inside that directory, recursively.
git add . Stages new, modified, and deleted files in the current directory and below only.
git add -A / git add --all Stages new, modified, and deleted files across the entire working tree, regardless of your current directory.
git add -u / git add --update Stages modifications and deletions to files Git already tracks; never stages new, untracked files.
git add -p / git add --patch Interactively walks through each changed “hunk” and lets you choose to stage it, skip it, or split it further.
git add -n / git add --dry-run Shows what would be staged, without staging anything.

Examples

Example 1: Staging a New File

mkdir demo-repo
cd demo-repo
git init
echo "# Demo Project" > README.md
git status

Output:

On branch main

No commits yet

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

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

Git sees README.md but isn’t tracking it yet, so it’s listed under “Untracked files.” Now stage it:

git add README.md
git status

Output:

On branch main

No commits yet

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

README.md moved from “Untracked files” to “Changes to be committed” — it now lives in the index, ready for git commit -m "docs: add initial README" to seal it into history.

Example 2: git add . vs git add -A vs git add -u

git commit -m "docs: add initial README"
mkdir src
touch src/app.js
touch style.css
printf "# Demo Project\n\nA sample app.\n" > README.md
cd src

At this point three things have happened relative to the last commit: a new file exists at src/app.js, a new untracked file style.css sits at the repository root, and the already-committed README.md has been modified — also at the root. The current directory is src/. Now run:

git add .
git status

Output:

On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	new file:   app.js

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:   ../README.md

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

Because git add . was run from inside src/, its scope was limited to that directory and below — it staged only app.js. The modified README.md and the untracked style.css, both at the repository root, are outside that scope and appear as relative paths (../README.md, ../style.css) that remain unstaged. Running git add -A from anywhere in the repository would stage all three changes regardless of your current directory, while git add -u would stage the modified README.md but skip both untracked files, since -u only touches files Git already tracks.

Example 3: Staging Part of a File with git add -p

Suppose you fixed a real bug in app.js and, while you were in there, also cleaned up an unrelated leftover debug line. You want the bug fix in its own commit, separate from the cleanup:

git add -p app.js

Output:

diff --git a/app.js b/app.js
index 4b825dc..1a410ef 100644
--- a/app.js
+++ b/app.js
@@ -12,7 +12,7 @@ function calculateTotal(items) {
-  return items.reduce((a, b) => a + b);
+  return items.reduce((a, b) => a + b, 0);
Stage this hunk [y,n,q,a,d,s,e,?]?

Typing y stages just that hunk — the bug fix. When Git shows the next hunk (the debug-line cleanup), typing n leaves it unstaged so it can go into a separate, later commit. This is the main use of patch mode: turning one messy set of working-tree edits into several small, honestly-described commits.

How git add Works Step by Step

  1. Git reads the current content of each file matched by your pathspec, straight from the working directory.
  2. For each file, Git computes a content hash and, if an object with that hash doesn’t already exist in .git/objects, writes a new compressed blob there.
  3. Git updates .git/index — a binary file, not something you edit by hand — with an entry mapping that file’s path to the blob hash just written, plus metadata like file mode and timestamps used to detect future changes quickly.
  4. Nothing is written to your commit history yet. HEAD, the current branch pointer, and every existing commit are untouched.
  5. When you later run git commit, Git walks the current index, builds a tree object (and any subtree objects needed for subdirectories) representing that exact snapshot, creates a commit object pointing at that tree and at the current commit as its parent, and moves the branch pointer forward to the new commit.

This is also why git diff with no arguments compares the working directory against the index, while git diff --staged (equivalently git diff --cached) compares the index against the last commit — there are three distinct snapshots in play, and each Git command compares a specific pair of them.

Common Mistakes

Mistake 1: Staging Files You Didn’t Mean To

Running a broad git add . or git add -A out of habit, before setting up a .gitignore, can sweep in build output, editor files, or secrets:

git add .
git commit -m "feat: add login form"
# oops — .env and node_modules/ just got committed too

Why it’s wrong: without a .gitignore, Git has no way to know those files shouldn’t be tracked, so a wildcard add happily stages everything it finds, including files that were never meant to leave your machine.

The fix: unstage the unwanted files, add ignore rules, and only then stage and commit what you meant to:

git restore --staged .env node_modules
echo ".env" >> .gitignore
echo "node_modules/" >> .gitignore
git add .gitignore

If a secret was already committed — not just staged — unstaging is not enough. You would need to rewrite history with a tool such as git filter-repo and, just as importantly, rotate the leaked credential, since it still exists in anyone’s already-cloned copy of the old history.

Mistake 2: Forgetting to Stage a File Before Committing

git commit -m "fix: correct total calculation"

Output:

On branch main
Changes not staged for commit:
	modified:   app.js

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

Why it’s wrong: git commit without -a only ever commits what’s already in the index. Editing a file on disk does not automatically stage it.

The fix: stage the file first, or use git commit -a to auto-stage every already-tracked, modified file (note this still will not pick up brand-new, untracked files):

git add app.js
git commit -m "fix: correct total calculation"

Mistake 3: Confusing “Staged” With “Committed”

A common misunderstanding: after running git add and seeing a file listed under “Changes to be committed,” someone assumes the work is now safely saved to history.

Why it’s wrong: staged changes still live only in the index, not inside a commit object. They are not part of any commit’s snapshot yet, are not visible in git log, cannot be pushed, and can still be lost by a careless git reset --hard.

The fix: always finish the job with git commit, and use git status liberally so you always know whether something is merely staged or actually committed.

Best Practices

  • Run git status before and after every git add so you always know exactly what’s staged.
  • Prefer staging specific files or directories over a blanket git add . whenever unrelated changes are sitting in your working directory at the same time.
  • Review git diff --staged right before committing to see precisely what will go into the commit.
  • Set up a .gitignore at the very start of a project so build artifacts, dependencies, and secrets are never candidates for staging in the first place.
  • Use git add -p whenever a single file mixes an intentional change with unrelated edits, so each commit stays focused and its message stays honest.
  • Write staged-and-committed history using a consistent convention, such as Conventional Commits (feat:, fix:, docs:, refactor:), so the log stays easy to scan.
  • If you stage something by mistake, use git restore --staged <file> to unstage it without touching your working-tree edits.

Practice Exercises

  1. Initialize a new repository and create two files, index.html and notes.txt. Stage only index.html, confirm with git status that notes.txt is still untracked, then commit just the staged file with the message feat: add initial index page.
  2. In that same repository, edit index.html in two unrelated ways — for example, add a new heading, and separately fix an unrelated typo elsewhere in the file. Use git add -p to stage and commit each change as its own commit, with its own accurate message.
  3. Create a file named secrets.txt and a .gitignore that ignores it. Run git add -A and use git status to confirm secrets.txt was never staged. Then remove the ignore rule on purpose, stage the file by mistake, and practice recovering with git restore --staged secrets.txt before it gets committed.

Summary

  • git add copies a snapshot of a file’s current content from the working directory into the staging area (the index), preparing it for the next commit.
  • The index is a distinct, third snapshot alongside the working directory and the last commit; git diff and git diff --staged compare different pairs of these three.
  • git add . stages within the current directory and below; git add -A stages across the whole working tree; git add -u stages only files Git already tracks.
  • Staging lets you build focused, logically separated commits instead of one giant snapshot of everything you happened to change.
  • Use git status, git diff --staged, and git restore --staged to inspect and correct what’s staged before you actually commit.