🔄 Git & GitHub Sync Issues: Understanding and Resolving Mismatches
Sooner or later, every developer types git push and gets a wall of red text back: "Updates were rejected." It feels like the tool is fighting you. It isn't — Git is protecting you from silently overwriting work. This reference decodes what "out of sync" really means and gives you a calm, repeatable playbook for putting local and remote back in agreement.
Reference & Extra Tutorials · Resources · Git Survival Kit
🎯 What This Covers
By the end of this reference, you will be able to:
- Explain the three places your code lives — working directory, local repo, and remote — and how they drift apart
- Read
git statusand tell whether you are ahead, behind, or diverged from the remote - Follow the golden rule — pull before you push — and know why it prevents most rejections
- Resolve a merge conflict from the conflict markers all the way to a clean commit
- Use
git push --force-with-leasesafely after rewriting history — and know when never to touch it - Adopt daily habits that keep sync problems from ever appearing
Estimated Time: 40 minutes
Practice: Simulate a two-machine conflict and resolve it end to end.
In This Reference
The Three-Place Mental Model
Most sync confusion evaporates once you realize your code lives in three separate places, and Git only moves it between them when you tell it to. Think of it like writing an essay: there's the paragraph you're typing right now, the saved draft on your laptop, and the copy you've emailed to your editor. They are not automatically the same.
| Place | What it is | How you move things into it |
|---|---|---|
| Working directory | The files you edit right now, on disk | Save in your editor |
| Staging area | The changes marked for the next commit | git add |
| Local repository | Your committed history on this machine | git commit |
| Remote (GitHub) | The shared history everyone pulls from | git push / git pull |
A "sync issue" is simply the last hop — between your local repository and the remote — being out of step. Everything below is about diagnosing and closing that gap without losing anyone's work.
💡 Git is distributed, not live
Unlike a shared Google Doc, Git does not update in real time. Your local history is a full, independent copy. It only learns about the remote when you fetch or pull, and the remote only learns about you when you push. That "snapshot" nature is exactly why drift happens.
Why Repos Drift Out of Sync
Picture GitHub as a shared group chat and your local repo as your private notes. If you jot things down without reading the chat, you fall behind. If you talk in the chat from another device, your notes fall behind. Drift comes from one of three everyday situations:
- You are ahead. You committed locally but haven't pushed. The remote is missing your work.
- You are behind. Someone else pushed (or you did, from another machine). Your local copy is missing their work.
- You have diverged. Both sides gained new commits independently — the dangerous, interesting case that a plain push will reject.
just push"] B -->|Only they committed| D["You are BEHIND
pull first"] B -->|Both committed| E["DIVERGED
pull, resolve, then push"]
The rejection message you dread — "failed to push some refs" — almost always means diverged. Git is refusing to fast-forward because doing so would drop the commits already on the remote. That refusal is a feature, not a bug.
Reading Ahead, Behind & Diverged
Before you change anything, ask Git where you stand. Two commands do almost all the diagnosis.
Start with git status
git status is you asking, "What's up with my repo right now?" It compares your working directory, staging area, and the remote-tracking branch Git last knew about.
git status
Output — you are ahead
On branch main
Your branch is ahead of 'origin/main' by 2 commits.
(use "git push" to publish your local commits)
nothing to commit, working tree clean
"Ahead by 2 commits" means the safe, happy case: just push. But that status only reflects what Git knew at your last fetch — the remote may have moved since. So refresh first.
Refresh your knowledge with git fetch
git fetch downloads the remote's latest commits without touching your working files. It's the "read the group chat before replying" step — pure information, zero risk.
git fetch origin
git status
Output — you have diverged
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 3 different commits each, respectively.
(use "git pull" to merge the remote branch into yours)
Now you know the truth: 1 local commit they don't have, 3 remote commits you don't have. This is the diverged case, and the next section is your recipe.
📖 fetch vs pull
git fetch only downloads and updates the remote-tracking branch (origin/main) — your files stay put. git pull is fetch plus an immediate merge into your current branch. When you want to look before you leap, fetch; when you're ready to integrate, pull.
The Standard Fix: Pull, Resolve, Push
For the overwhelming majority of sync problems, one ordered routine solves it. Memorize this rhythm.
Step 1 — Commit or stash your own work
Never pull on top of uncommitted changes you care about. Commit them, or set them aside with git stash.
git add .
git commit -m "Add contact form validation"
Step 2 — Pull the remote in
Always pull before you push. This brings the remote's commits into your branch so your eventual push is a clean fast-forward. It avoids the coding equivalent of two people showing up to a party in the same outfit.
git pull origin main
If the histories merge cleanly, Git either fast-forwards or creates a small merge commit, and you're done — skip to Step 4. If they conflict, Git pauses and hands the wheel to you (Step 3).
Step 3 — Resolve any conflicts
Covered in full in the next section. Edit the marked files, stage them, and commit.
Step 4 — Push the reconciled history
git push origin main
Output
Enumerating objects: 8, done.
To github.com:yourname/todo-app.git
a1b2c3d..e4f5g6h main -> main
Both sides now point at the same commit. You are in sync.
⚠️ Prefer a rebase pull for a tidy history
Plain git pull creates a merge commit each time. For a linear, easier-to-read history on a shared branch, many teams pull with a rebase instead: git pull --rebase origin main. It replays your local commits on top of the remote ones. Just don't rebase commits you've already pushed and shared.
Resolving Merge Conflicts
A merge conflict happens when both sides changed the same lines of the same file, and Git honestly can't decide which version wins. Rather than guess, it stops and asks you. That message looks scary but is really Git saying, "You know the intent — you choose."
Auto-merging src/app.js
CONFLICT (content): Merge conflict in src/app.js
Automatic merge failed; fix conflicts and then commit the result.
Read the conflict markers
Open the file. Git inserts three markers around the disputed region:
<<<<<<< HEAD
const greeting = "Hello there"; // your local version
=======
const greeting = "Hi, welcome!"; // the version from origin/main
>>>>>>> origin/main
- Between
<<<<<<< HEADand=======is your change. - Between
=======and>>>>>>> origin/mainis their change.
Edit to the version you actually want
Delete all three marker lines and leave the final, correct code. You can keep yours, keep theirs, or blend both — whatever the code should truly be:
const greeting = "Hi, welcome!"; // decided: keep the friendlier copy
Stage the resolved file and commit
git add src/app.js
git commit # opens an editor with a prepared merge message
# or supply your own:
git commit -m "Merge origin/main: keep friendlier greeting copy"
✅ Tools make conflicts painless
You rarely edit markers by hand in practice. Run git mergetool, or use VS Code's built-in three-way merge editor with its "Accept Current / Accept Incoming / Accept Both" buttons. And if you ever want to bail out and start over, git merge --abort returns you to exactly where you were before the pull.
Force Push: The Emergency Parachute
Sometimes you deliberately rewrite local history — you rebased, amended a commit, or squashed several into one. Now your local branch and the remote have genuinely different histories, and a normal push is rejected because Git won't discard remote commits. The only honest fix is to overwrite the remote:
# Safer force: refuse if the remote moved since you last fetched
git push origin main --force-with-lease
⚠️ Force push overwrites shared history
A force push replaces the remote branch with yours. If a teammate had commits there that you didn't have, you just erased them. Treat it like replacing a page in a shared notebook — only safe when you're certain no one else wrote on it.
Always prefer --force-with-lease over the blunt --force. The "with lease" variant checks that the remote is still where you last saw it and aborts if someone pushed in the meantime — a built-in safety catch that plain --force skips entirely.
| Situation | Safe to force-push? |
|---|---|
| Your own private feature branch, no one else on it | ✅ Yes, with --force-with-lease |
| A shared branch others are actively using | ❌ No — coordinate or open a new branch |
The main branch of a team repo | ❌ Almost never — usually protected on purpose |
A Real Team Scenario
You and a teammate are building a to-do list app. Walk through exactly what happens and what you type:
- You finish a feature and commit locally:
git commit -m "Add due-date field". - Meanwhile your teammate pushed a fix straight to GitHub. The remote now has a commit you lack.
- You try
git push— rejected. You've diverged. - You run
git pull origin main. Git reports a conflict inapp.jsbecause you both edited the same function. - You open
app.js, read the markers, keep the correct blend of both changes, remove the markers. - You stage and commit:
git add app.js && git commit. - You push again — accepted. You're both on the same page.
💡 The takeaway
Nothing here required force pushing or fancy commands. Ninety percent of "Git broke my repo" moments are just this loop: commit → pull → resolve → push. Run it calmly and the red text goes away.
Practice & Quiz
🏋️ Exercise 1: Manufacture and fix a divergence
Goal: Deliberately create an "ahead and behind" situation in a safe test repo, then reconcile it. This is the fastest way to make the concept stick.
💡 Hint
Create a repo on GitHub, clone it, then edit the same file in the GitHub web UI (that's your "teammate"). Back in your clone, commit a different change to that file and try to push. Then run git pull and resolve the conflict.
✅ Solution
# 1. In your local clone, make and commit a change
echo "local line" >> notes.txt
git add notes.txt
git commit -m "Local edit to notes"
# 2. (On github.com) edit notes.txt in the browser and commit — the "teammate"
# 3. Back locally, this push is rejected (diverged)
git push origin main # ! [rejected] ... fetch first
# 4. Pull, resolve the conflict, commit
git pull origin main # CONFLICT in notes.txt
# ...edit notes.txt, remove the <<<</====/>>>> markers...
git add notes.txt
git commit -m "Merge remote notes edit"
# 5. Now the push succeeds
git push origin main
🏋️ Exercise 2: Diagnose before acting
Goal: Given a fresh clone that may be behind, determine your exact position relative to the remote without changing any files.
✅ Solution
git fetch origin # download remote state, touch nothing
git status # "ahead", "behind", or "diverged"
git log --oneline --graph --all -n 15 # see both histories visually
Only after reading this do you decide whether to push, pull, or pull-then-resolve.
🎯 Quick Quiz
Question 1: Your push is rejected with "failed to push some refs." What should you almost always do next?
Question 2: What is the difference between git fetch and git pull?
Question 3: When rewriting shared history, which push is the safer choice?
Best Practices & Pitfalls
✅ Do
- Run
git fetchandgit statusbefore you push, so surprises never reach the remote - Always pull before you push on a shared branch
- Commit or stash your work before pulling, so you never merge on top of a dirty tree
- Do new work on a feature branch and merge via a Pull Request — it isolates conflicts
- Commit small and push often; tiny changes conflict far less than huge ones
❌ Don't
- Reach for
--forceas a first response to a rejected push — pull instead - Force push to
mainor any branch teammates share - Blindly accept "yours" or "theirs" in a conflict without reading both sides
- Delete and re-clone the repo to "fix" sync problems — you'll lose unpushed local commits
- Ignore the status message; Git usually tells you the exact command to run next
⚠️ "Detached HEAD" is not a sync error
If you ever see You are in 'detached HEAD' state, you've checked out a specific commit rather than a branch. It's unrelated to remote sync. Get back on track with git switch main (or git checkout main), then continue as normal.
Summary
🎉 Key Takeaways
- Your code lives in three places — working directory, local repo, and remote — and Git only moves it on command
- Drift is always one of ahead, behind, or diverged;
git fetchthengit statustells you which - The universal fix is the loop commit → pull → resolve → push
- Merge conflicts are Git asking you to choose; edit the marked region, remove the markers, add, commit
- Reserve force pushing for rewritten history on your own branch, and prefer
--force-with-lease
📚 Additional Resources
- git-scm.com — git pull documentation
- Pro Git — Basic Branching and Merging
- GitHub Docs — Dealing with non-fast-forward errors
- GitHub Docs — Resolving a merge conflict on the command line
🚀 What's Next?
Now that you can keep a repo in sync, browse the rest of the reference library. Up next is the HTML Files Index — a complete, categorized map of every extra tutorial and cheat sheet in this collection.
🎉 The red text no longer scares you
Sync problems are just Git protecting your team's work. Read the status, pull before you push, and resolve with intent — that's the whole game.