Skip to content
ohsudo, Home
CI/CD

Git "! [rejected] main -> main (fetch first)": why the push failed and how to fix it safely

Fix git push "rejected (fetch first)" and "non-fast-forward": pull with rebase or merge, use --force-with-lease instead of --force, and handle protected branches.

Beginner4 min readApplies to Git 2.30+ · GitHub · GitLab · Bitbucket

by OhSudo ··

Part of a learning path:Git for everyday work

You push, and Git refuses:

$ git push
To github.com:example/app.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'github.com:example/app.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.

The remote branch has commits you don’t have — a colleague pushed, you committed from another computer, or someone merged a pull request in the web interface. A push may only add commits on top of what’s there; it would have to throw those commits away, so Git stops. Nothing is broken, and nothing is lost yet. Don’t make it worse with --force.

  1. Your mainA → B → your commit
  2. Remote mainA → B → a colleague's commit(breaks here)
  3. git pull, then pushyours on top of theirs(works)
The remote branch has commits you don't have. A push may only add commits on top; taking yours would throw theirs away, so Git stops. Get them first, then push.

TL;DR

Terminal window
git pull --rebase # put your commits on top of the remote ones
git push

If the rebase stops with a conflict: fix the files, git add them, git rebase --continue. To give up and go back: git rebase --abort.

1. See what’s different

You can try this in the terminal, where your push has just been rejected:

Terminal window
git fetch
git log --oneline main..origin/main # commits on the remote that you don't have
git log --oneline origin/main..main # your commits that the remote doesn't have

git fetch only downloads; it doesn’t touch your files. Now you know what you’re about to combine.

The same rejection can also look like this:

! [rejected] main -> main (non-fast-forward)
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart.

Same cause, same fix — Git just already knew about the remote commits.

2. Combine: rebase or merge

Rebase replays your commits on top of the remote ones. History stays a straight line — what most teams prefer for small local work:

Terminal window
git pull --rebase
git push

Merge keeps both lines and adds a merge commit. Use it when your branch is shared with others, or when the team prefers it:

Terminal window
git pull --no-rebase
git push

If a plain git pull answers with “You have divergent branches and need to specify how to reconcile them”, Git wants you to choose once. Make rebase the default:

Terminal window
git config --global pull.rebase true

3. Conflicts

If you and the remote changed the same lines, Git stops and marks the files:

CONFLICT (content): Merge conflict in config/app.yml

git status lists them. Open each file, keep the right version between the <<<<<<<, ======= and >>>>>>> markers, remove the markers, then:

Terminal window
git add config/app.yml
git rebase --continue # or: git commit, if you were merging

Changed your mind? git rebase --abort (or git merge --abort) puts everything back as it was before the pull.

4. When you really do need to force

After you rewrite commits you already pushed (git commit --amend, an interactive rebase, a squash), the remote has the old versions and you have the new ones. A normal push is rejected, and here overwriting is the point — but do it carefully:

Terminal window
git push --force-with-lease

--force-with-lease overwrites the remote only if it’s still where you last saw it. If a colleague pushed in the meantime, it refuses instead of silently deleting their work — plain --force would delete it.

One catch: an IDE that runs git fetch in the background updates “where you last saw it”, and the protection is gone. Git 2.30+ adds a check for that: git push --force-with-lease --force-if-includes.

Warning

Never force-push a branch other people work on (main, develop) unless the whole team agreed. Their next pull will be a mess, and their commits can disappear from the remote.

5. Protected branches

If the branch is protected on the server, the push fails even when it’s a normal one:

remote: error: GH006: Protected branch update failed for refs/heads/main.

On GitLab: You are not allowed to push code to protected branches on this project. The fix isn’t in Git: push to a new branch and open a pull request (merge request):

Terminal window
git switch -c fix/login-timeout
git push -u origin fix/login-timeout

Shell, we have a problem!

Similar, but different: fatal: refusing to merge unrelated histories — the local repository and the remote one started separately (typically: you ran git init locally and created the repository on GitHub with a README). Combine them once with git pull origin main --allow-unrelated-histories. Permission denied (publickey) on push is an SSH key problem — see SSH “Permission denied (publickey)”. If the two sides share no history at all, see refusing to merge unrelated histories. If git pull stops because your local changes would be overwritten, see that article. If Git says your branch doesn’t exist, see src refspec main does not match any. All the everyday commands are in the Git Cheat Sheet. RPC failed / the remote end hung up unexpectedly is a push that broke off in transit.

$ ./practice.sh

Practice this in the terminal

Open the Terminal Playground with this article’s scenario loaded. It is safe and runs in your browser.

Open the terminal →

Found this useful? Share it.

Found a mistake? Let us know at contact@ohsudo.com.

Tools

Try it yourself

Articles

Related articles