Skip to content

Git’s Staging Area — What Actually Happens Between `git add` and `git commit`

Edit a file, run git add ., then git commit -m "..." — for most people these two commands feel like a single operation performed in two keystrokes. But why are they separate commands at all? What happens if you edit the file again after git add but before git commit? Many developers who use Git daily can’t answer either question cleanly. This article walks through the staging area, the mechanism sitting between those two commands.

The three-area mental model

Note: A common way to understand Git is the three-layer model — the working directory, the staging area (also called the index), and the repository. A change has to pass through all three, in order, before it becomes part of the commit history.

The working directory is where you actually edit files in your editor. From Git’s point of view, changes here are just filesystem differences — they have no effect on history at all until something moves them forward.

The repository is the committed history you see with git log. Once a commit exists, its content is fixed unless you explicitly rewrite it with something like git commit --amend or git reset.

Sitting between those two is the staging area. It’s a temporary holding place for exactly the parts of your working-directory changes that you want included in the next commit — nothing more, nothing less. git add copies changes from the working directory into the staging area; git commit takes a snapshot of whatever is currently in the staging area and permanently records it in the repository. That’s the entire division of labor.

What git add actually does

Under the hood, git add does two things. First, it takes the content of the changed file and stores it as a blob object under .git/objects/, keyed by a content hash. Second, it writes a mapping into a binary file called .git/index: “this path now points to the blob with this hash.”

The key detail is that .git/index records a snapshot of the file’s content at the exact moment git add ran — nothing more current than that. If you edit the file again after staging it, the working directory changes, but the staging area still points to the old blob. Run git status in that state and the same file shows up under both “changes to be committed” and “changes not staged for commit” at once. That’s not a bug — it’s the direct consequence of the staging area being a frozen copy rather than a live reference.

$ echo "v1" > file.txt
$ git add file.txt
$ echo "v2" > file.txt
$ git status
Changes to be committed:
        modified:   file.txt      ← the index still holds v1

Changes not staged for commit:
        modified:   file.txt      ← the working directory now has v2

What git commit actually does

git commit builds a tree object out of whatever is currently in the staging area (the index), then wraps that tree together with a pointer to the parent commit and metadata (author, timestamp, message) into a commit object. What it reads from is always the index — never the live state of the working directory. Anything you never staged, or staged and then edited again afterward, is simply not part of the commit.

git commit -a is a shortcut that makes this look like a single step: internally, it silently runs git add first, but only for files Git is already tracking (newly created files are excluded). If you don’t know that automatic staging is happening behind the scenes, you can be caught off guard by a commit that’s missing a brand-new file you clearly just created.

Why split it into two steps at all

A version-control system that committed the working directory directly, with no intermediate step, would still work in principle. The reason Git inserts a staging layer is to let you control what goes into a single commit independently of how you actually edited the files.

A common scenario: you make two unrelated changes to the same file in one editing session — say, a bug fix and an incidental formatting cleanup. git add -p file.txt lets you stage changes at the granularity of individual hunks rather than whole files, so you can pick “just this part” interactively. That makes it possible to commit the bug fix on its own and leave the formatting change for a separate commit — a kind of after-the-fact reorganization that would otherwise require manually copying and diffing files by hand.

The other practical use is reviewing exactly what’s about to be committed with git diff --cached (equivalently git diff --staged). While plain git diff shows the difference between the working directory and the index, git diff --cached shows the difference between the index and the last commit — in other words, the exact content that’s about to become the next commit. Making a habit of running this after git add and before the actual commit gives you one last checkpoint to catch a stray debug print statement, or a file that shouldn’t be part of the commit at all, before it becomes permanent history.

A small habit worth building

In workflows where multiple people — or an automated agent — are creating commits, staging specific files by name tends to be safer than a blanket git add -A, followed by a git status check before committing. git add -A stages every change under the current directory indiscriminately, which means it can just as easily pick up a stray temp file, or a credentials file someone accidentally left in the working tree. Having a staging area — a deliberate holding pen between “edited” and “committed” — is what makes it possible to insert that kind of mechanical check at the one moment it can still do any good: after git add, before git commit.

Summary

git add and git commit being separate commands isn’t a historical accident — it’s a deliberate design choice to decouple “what you’ve edited” from “what goes into the next commit.” The staging area is the temporary snapshot that makes that decoupling possible, and both hunk-level staging with git add -p and pre-commit review with git diff --cached only exist because of this intermediate layer. Even if your day-to-day workflow is “just add and commit everything,” understanding what these two steps actually do gives you more room to maneuver the next time you need to untangle a set of changes before they become permanent.