Build the mental model
When Git refuses to do what you asked, the tempting move is to guess a fix and try again. That habit is how small problems turn into bigger ones. The real skill in Git troubleshooting is diagnosis: reading exactly what Git tells you before you touch anything else.
- `git status` -- what your working directory and staging area look like right now.
- `git log --oneline --graph --all` -- the real shape of history across every branch and remote-tracking branch you know about.
Git's error and status messages are unusually literal -- a rejected push, a merge conflict marker, a "detached HEAD" warning each name the actual state of the repository, not a vague complaint. Those two commands above answer the two questions you need before doing anything else: where am I, and how did history actually get here.
This lesson works through one concrete case: a `git push` that Git rejects because the remote has commits your local clone has never seen -- the classic symptom of two people working from the same branch without syncing first. You will reproduce the rejection for real, using a local bare repository as a stand-in for GitHub.
Read the hint lines
Git's `hint:` lines after a rejected push are not filler -- they describe the actual cause and a real path forward. Reading them before Googling the error saves real time.
The exact command that resolves a diverged branch, a detached HEAD, or a wrongly staged file differs case by case. The diagnostic habit -- read the message, check status, check history, then act -- is the one skill that transfers to every Git problem you will ever hit.
PUSH REJECTED: LOCAL MAIN VS ORIGIN/MAIN
----------------------------------------
origin/main (bare repo) your local "you" clone
------------------------ ------------------------
24e9b92 Initial commit 24e9b92 Initial commit
3e8f241 Add Contributing 44aff82 Add Installation
^ teammate pushed this ^ you committed this,
-- you don't have it origin doesn't have it
locally yet
git push origin main
|
v
! [rejected] main -> main (fetch first)
"remote contains work that you do not have locally"
AFTER: git fetch reveals the divergence, then merge + push
origin/main == local main == one new merge commitConnect it to a real scenario
To make this real, you will build a tiny two-person team using nothing but a local bare repository as a stand-in for a GitHub remote. A bare repo (`git init --bare`) has no working directory -- it exists purely to be pushed to and pulled from, exactly like `origin` on GitHub.
Play yourself
Clone the bare repo and push an initial README commit.
Play the teammate
With a different GIT_AUTHOR_NAME, clone the same bare repo again and push a second commit directly to it.
Back in your own clone, without fetching first, commit a change of your own and run `git push`. Git refuses -- not a bug, but Git protecting shared history from being silently overwritten.
Read the exact rejection text it prints, then use `git status` and `git fetch` to see precisely how your branch and `origin/main` have diverged before deciding what to do next. By the end you will have watched a genuine push rejection happen and resolved it the same way you would on an actual team.
Try the working example
# One-time setup
git config --global init.defaultBranch main
# Create a bare repo to stand in for a GitHub remote
git init --bare team-repo.git
# --- You: clone it and push the first commit ---
git clone team-repo.git you
cd you
export GIT_AUTHOR_NAME="Thuta Learner"
export GIT_AUTHOR_EMAIL="learner@example.com"
export GIT_COMMITTER_NAME="Thuta Learner"
export GIT_COMMITTER_EMAIL="learner@example.com"
export GIT_AUTHOR_DATE="2026-01-01T09:00:00"
export GIT_COMMITTER_DATE="2026-01-01T09:00:00"
echo "# Team Project" > README.md
echo "Initial setup." >> README.md
git add README.md
git commit -m "Initial commit: add README"
git push origin main
cd ..
# --- Teammate: clone the SAME bare repo, push a second commit ---
git clone team-repo.git teammate
cd teammate
export GIT_AUTHOR_NAME="Maung Maung"
export GIT_AUTHOR_EMAIL="maungmaung@example.com"
export GIT_COMMITTER_NAME="Maung Maung"
export GIT_COMMITTER_EMAIL="maungmaung@example.com"
export GIT_AUTHOR_DATE="2026-01-01T10:15:00"
export GIT_COMMITTER_DATE="2026-01-01T10:15:00"
echo "" >> README.md
echo "## Contributing" >> README.md
echo "See CONTRIBUTING.md for guidelines." >> README.md
git add README.md
git commit -m "Add Contributing section to README"
git push origin main
cd ..
# --- You again: commit locally WITHOUT fetching first, then push ---
cd you
export GIT_AUTHOR_DATE="2026-01-01T11:00:00"
export GIT_COMMITTER_DATE="2026-01-01T11:00:00"
echo "" >> README.md
echo "## Installation" >> README.md
echo "Run npm install to get started." >> README.md
git add README.md
git commit -m "Add Installation section to README"
git push origin main
# --- Diagnose: what does Git actually tell us? ---
git status
git log --oneline
git fetch origin
git log --oneline --graph --all
git statusThe final push is rejected:
To .../team-repo.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to '.../team-repo.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
Before fetching, `git status` says:
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
That is technically true but misleading -- it reflects the last fetch, not the remote as it is right now. After `git fetch origin`, `git log --oneline --graph --all` shows two branches that share the "Initial commit" ancestor and then split into separate commits, and `git status` now reports the real picture:
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.5-minute try-it
Your teammate merged their change first. You finish your own edit, commit it, and run `git push origin main` -- but instead of the usual `main -> main` success line, Git prints "! [rejected] main -> main (fetch first)" followed by a hint explaining that "the remote contains work that you do not have locally." Meanwhile `git status` still says "Your branch is ahead of 'origin/main' by 1 commit" -- which sounds like everything is fine, even though it is not telling the whole story. What command would show you what `git status` alone can't, and what should you check about the shape of history before deciding how to bring your branch and the remote back in sync?
One important caution
Reaching for `git push --force` to make the rejection go away can silently overwrite a teammate's commits.
Trusting `git status` alone right after a rejected push is misleading -- it only reflects your last fetch, not the remote's current state.
Pro Git -- Working with Remotes — Git & GitHub