/개발/How to Fix a Rejected git push: non-fast-forward and 'remote contains work'
Developmentgit push 오류 해결깃 푸시 거부

How to Fix a Rejected git push: non-fast-forward and 'remote contains work'

Diagnose git push non-fast-forward and 'remote contains work' rejection errors with a cause-by-cause table, then fix them without data loss using pull --rebase and force-with-lease—plus reflog recovery.

How to Fix a Rejected git push: non-fast-forward and 'remote contains work'

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:

CODE
! [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.

SituationActual outputCause in one lineRecommended action
Remote is ahead! [rejected] main -> main (fetch first) / Updates were rejected because the remote contains workSomeone pushed to the same branch while you were workinggit 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 commitsfetch → resolve conflicts → push
Someone force-pushed(non-fast-forward), but history is still tangled after pullA teammate rewrote history with --forceConfirm 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 remoteDelete 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.

CODE
*   a1b2c3 Merge branch 'origin/main'   ← unnecessary merge commit
|\
| * 9f8e7d teammate's commit (remote)
* | 4d5c6b my commit (local)
|/
* 0a1b2c common ancestor

git pull --rebase — replays your commits on top of the remote, keeping history linear.

CODE
* 4d5c6b' my commit (rebased)   ← a clean straight line
* 9f8e7d teammate's commit
* 0a1b2c common ancestor
mergerebase
HistoryMerge commit is createdStays linear
Best whenYou want to keep the collaboration trail as-isClean history; trunk-based development with easy PR reviews
Watch outMerge commits pile upNever 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)

Bash
git fetch origin
git pull --rebase origin main
# after resolving conflicts in the files
git add <충돌_해결한_파일>
git rebase --continue
git push origin main

When rebase is going nowhere (safe undo)

Bash
git rebase --abort   # safely return to the state before rebase started

Safe force-push (only when you truly need it)

Bash
git push --force-with-lease origin main

When it looks like work vanished — reflog

Bash
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 point

reflog 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.

  1. You and a teammate start from the same point
  2. The teammate pushes a commit → the remote is now ahead
  3. Unaware of the remote change, you run git push --force
  4. 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.

CODE
$ git push --force-with-lease
! [rejected] main -> main (stale info)   ← remote changed, so the push is rejected

In 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 --force on a shared branch (main/develop) can permanently delete teammates' commits.
  • Before any force-push, always git fetch first so you know the latest state. --force-with-lease only 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 fetch a habit before every push
  • Use only --force-with-lease for 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.

확인 정보
✦ ✦ ✦
편집 검토 · Editorial Review

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 · ·업데이트 ·
관련 공식 문서Git 공식 문서

Comments

Be the first to comment.