The Difference Between Git Add, Commit, and Push That Nobody Explains Clearly
by Eric Hanson, Backend Developer at Clean Systems Consulting
Why This Confusion Persists
New developers learn git add ., git commit -m "stuff", git push as a ritual — three incantations that make code "go to GitHub." Senior developers who learned Git this way often don't think about the staging area at all, because git add . skips it entirely. Then they wonder why their commits always contain debug logs, commented-out code, and unrelated changes they didn't notice.
The three-stage model is the foundation of everything useful in Git. Understanding it changes how you work.
The Three Stages
Git tracks your files across three distinct areas:
The working tree — your filesystem. Where you actually edit files. Changes here exist only on your local machine and have no relationship to Git yet.
The index (staging area) — a proposed next commit. When you run git add, you're moving changes from the working tree into the index. Nothing has been committed. Nothing has been shared. The index is a scratchpad for constructing your next commit.
The local repository — the commit graph stored in .git/. When you run git commit, Git takes a snapshot of the index and creates a new commit object in the local repository. Still nothing shared.
The remote repository — a repository on another machine (GitHub, GitLab, Bitbucket, your company's self-hosted Gitea). When you run git push, you're transmitting your local commits to the remote.
Working Tree →[git add]→ Index →[git commit]→ Local Repo →[git push]→ Remote Repo
What git add Actually Does
git add copies the current state of a file (or a portion of a file) into the index. It does not track changes going forward — it takes a snapshot at that moment.
# You modify auth.py
# At this point, git status shows auth.py as "modified" (not staged)
git add auth.py
# Now git status shows auth.py as "staged for commit"
# You then modify auth.py again
# git status shows auth.py as BOTH staged (old version) and modified (new changes)
# git add auth.py again to capture the new changes
This is a common source of confusion. git add is not "track this file." It is "snapshot this file's current state into the index." If you modify a file after staging it, you need to stage it again.
git add . stages everything in the current directory and below. It is convenient and imprecise. Prefer git add -p (patch mode) when you want to stage selectively.
# Stage individual hunks interactively
git add -p auth.py
# Git shows each changed hunk and asks:
# Stage this hunk [y,n,q,a,d,e,?]?
# y = yes, stage it
# n = no, skip it
# s = split into smaller hunks
# e = edit the hunk manually
This is how you commit one logical change from a file that has two unrelated changes in it.
What git commit Does (And Does Not Do)
git commit creates a permanent record of whatever is currently in the index. It does not look at the working tree. It does not care what files you've modified. It takes the staged snapshot and makes it a commit.
# What's in the index is what goes into the commit
git diff --staged # shows what will be committed
git diff # shows what's in working tree but NOT staged
The --all / -a flag on git commit -am "message" is a shortcut that stages all tracked modified files before committing. It does not stage new (untracked) files. It is the closest Git gets to collapsing add and commit into one operation.
The commit lives in your local repository only. Your team cannot see it. Your CI pipeline cannot see it. The remote has no knowledge of it.
# Local commits not yet on remote
git log origin/main..HEAD --oneline
# Shows: your commits that exist locally but not remotely
What git push Does
git push transmits your local commits to a remote repository and updates the remote branch reference. That's it.
# Full form, rarely needed
git push origin feature/payment-retry
# Shorthand when tracking is set up
git push
# Set upstream tracking on first push
git push -u origin feature/payment-retry
# After this, plain 'git push' knows where to go
When a push is rejected ("non-fast-forward"), it means the remote branch has commits your local branch doesn't have. Git won't let you overwrite them. You need to git pull (or git fetch + git rebase) first to integrate the remote changes, then push again.
Force pushing (git push --force) overwrites the remote branch with your local history. On a personal feature branch, this is fine. On a shared branch, it is destructive — it rewrites history that others may have based work on. Use git push --force-with-lease instead, which fails if someone else has pushed since you last fetched.
The Practical Implications
Understanding these three stages explains several things that confuse developers:
Why can't I commit this specific change but not that one from the same file? You can. Use git add -p to stage specific hunks.
Why did my push get rejected? The remote has commits you don't. Fetch and integrate first.
Why are my credentials/secrets in this commit even though I deleted them? Because they were in the index when you committed. Deleting them from the working tree after staging doesn't un-stage them. You needed to git reset HEAD <file> to unstage, or git add -p to stage only the non-sensitive parts.
Why does my colleague not see my committed changes? Because committed doesn't mean pushed. Local commits are local until pushed.
The Workflow That Uses All Three Stages Well
# Make changes across multiple files
# Examine what you have:
git status
git diff
# Stage selectively:
git add -p src/payment/processor.py # stage specific hunks
git add tests/test_processor.py # stage entire test file
# Verify what's staged:
git diff --staged
# Commit with intent:
git commit -m "fix(payment): add idempotency key to prevent duplicate charges"
# Repeat for the next logical change in the same working directory
git add -p src/payment/processor.py # stage the remaining unrelated change
git commit -m "refactor(payment): extract charge amount validation to method"
# Push both commits to remote
git push
Two clean commits from one messy working directory session. That's the power of the staging area when you actually use it.