The Difference Between Git Add, Commit, and Push That Nobody Explains Clearly

by Arif Ikhsanudin, Backend Developer

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.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Securing Microservices Is Harder Than Securing a Monolith

A monolith has one trust boundary. Microservices have as many as you have services, and every internal network call is an attack surface if you treat your internal network as trusted.

Read more

Berlin Has a Backend Developer Shortage. Remote Contractors Fill the Gap

You've been hiring for three months. The role is still open. The gap in your backend isn't waiting for you to find the perfect candidate.

Read more

What If Frontend Engineers Were Treated Like Backend? Chaos Would Start on Day One

Imagine frontend engineers starting a sprint with no design, no specs—just vibes and pressure. Now add extra responsibilities that have nothing to do with UI. That’s where things break fast.

Read more

Your API Is Slower Than It Needs to Be and Pagination Is Probably Why

Unbounded list endpoints are one of the most common performance problems in production APIs — and one of the most preventable.

Read more