The Complete Fix for a Rejected git push: "remote contains work" and non-fast-forward
If you're frozen in front of that red "fetch first" message
You finished your work, hit git push with confidence, and the terminal answered in red:
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to 'origin'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. ... (fetch first)First, don't panic. Not a single line of your commits is gone. This error is a rejection, not a deletion. The remote is simply ahead of your local branch, so Git is politely asking you to integrate those changes before you push again.
The most dangerous thing you can do right now is blindly copy-paste git push --force from a search result. That can wipe a teammate's commits. Spend 30 seconds diagnosing the cause first.
Diagnose the cause from a single line of the error
Even when the word is the same—"rejected"—the details tell you why. Find your situation in the table below.
| Situation | Actual output | Cause in one line | Recommended action |
|---|---|---|---|
| Remote is ahead | ! [rejected] main -> main (fetch first) / Updates were rejected because the remote contains work | Someone pushed to the same branch while you were working | git pull --rebase, then push |
| Local and remote have diverged | ! [rejected] main -> main (non-fast-forward) / failed to push some refs to 'origin' | Local and remote split onto different commits | fetch → resolve conflicts → push |
| Someone force-pushed | (non-fast-forward), but history is still tangled after pull | A teammate rewrote history with --force | Confirm with the team, then --force-with-lease |
| Tag collision | ! [rejected] v1.2.0 -> v1.2.0 (would clobber existing tag) | A tag with the same name already exists on the remote | Delete and recreate the tag, or force-push the tag |
Everyday collaboration is almost always case 1 or 2. Treat case 3 with extra care, and handle case 4 as a tag-specific problem.
A safe resolution path: rebase vs merge
There are two ways to bring the remote into your local branch. The resulting history looks different.
git pull (merge) — creates a merge commit.
* a1b2c3 Merge branch 'origin/main' ← unnecessary merge commit
|\
| * 9f8e7d teammate's commit (remote)
* | 4d5c6b my commit (local)
|/
* 0a1b2c common ancestorgit pull --rebase — replays your commits on top of the remote, keeping history linear.
* 4d5c6b' my commit (rebased) ← a clean straight line
* 9f8e7d teammate's commit
* 0a1b2c common ancestor| merge | rebase | |
|---|---|---|
| History | Merge commit is created | Stays linear |
| Best when | You want to keep the collaboration trail as-is | Clean history; trunk-based development with easy PR reviews |
| Watch out | Merge commits pile up | Never rebase commits already pushed to a shared branch |
Through 2025–2026, trunk-based development and squash/rebase merges on PRs have become the default, so more teams set pull --rebase as the default on personal branches. You can lock that in with git config --global pull.rebase true.
Copy-paste command sets
The common case (this solves ~90% of incidents)
git fetch origin
git pull --rebase origin main
# after resolving conflicts in the files
git add <충돌_해결한_파일>
git rebase --continue
git push origin mainWhen rebase is going nowhere (safe undo)
git rebase --abort # safely return to the state before rebase startedSafe force-push (only when you truly need it)
git push --force-with-lease origin mainWhen it looks like work vanished — reflog
git reflog # inspect every HEAD movement
# e.g. 4d5c6b HEAD@{2}: commit: the work I want to restore
git reset --hard HEAD@{2} # reset to that pointreflog records every HEAD movement for about 90 days. Even if a rebase or reset made a commit feel "gone," you can usually restore it from here. That's the evidence behind "your data is not gone yet."
--force vs --force-with-lease: what's the difference
The difference decides whether a teammate's commits live or die.
--force overwrites unconditionally. Here's the scenario.
- You and a teammate start from the same point
- The teammate pushes a commit → the remote is now ahead
- Unaware of the remote change, you run
git push --force - The teammate's commit disappears from the remote entirely 😱
--force-with-lease checks one more time. It compares the remote ref as of your last fetch with the remote's current state, and rejects the push if anyone published a new commit in between.
$ git push --force-with-lease
! [rejected] main -> main (stale info) ← remote changed, so the push is rejectedIn other words, --force-with-lease is a safety latch: "overwrite only if the remote is still exactly as I last saw it." If a force-push is unavoidable, always use this option.
⚠️ Warning
- Using
--forceon a shared branch (main/develop) can permanently delete teammates' commits.- Before any force-push, always
git fetchfirst so you know the latest state.--force-with-leaseonly helps if you run it right after a fetch.
A note from the field
Early in my career I was so afraid of conflicts that I shoved a --force through and wiped half a day of a teammate's work. We recovered it from their local reflog, but after that I locked in two team rules. First, GitHub/GitLab protected branch rules that block force-push on main entirely. Second, every force-push must use --with-lease. Those two changes ended the "my commits disappeared" incidents.
Recurrence-prevention checklist
Actions you can apply today.
- Set pull's default to rebase with
git config --global pull.rebase true - Enable branch protection rules on
main/develop(block force-push and direct pushes) - Make
git fetcha habit before every push - Use only
--force-with-leasefor force-pushes - Work on a personal branch → PR (squash/rebase merge) flow
A red error is not an accident—it's Git protecting you. Read the message, find the cause in the table, integrate with rebase, and use --force-with-lease only when you truly must. Follow that order and a rejected push stops being scary.
FAQ
Q. I'm getting too many conflicts during git pull --rebase and just want to go back to the start.
A. Run git rebase --abort. It returns you safely to the state before the rebase started. No data is lost, so use it without hesitation.
Q. I pushed with --force-with-lease and it was rejected again with stale info.
A. That means new commits landed on the remote after your last fetch. That's the safety latch doing its job. git fetch, review the changes, decide whether overwriting is still correct, then try again.
Q. I ran reset --hard and a commit I still need is gone. Can I recover it?
A. Yes. Run git reflog, find the point you want (HEAD@{n}) in the HEAD movement history, and restore it with git reset --hard HEAD@{n}. reflog is kept for about 90 days by default.
Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
Comments
Be the first to comment.