Git Push Rejected: Fix 'Failed to Push Some Refs'
Run git pull --rebase, resolve any conflicts, then push again.
20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.
- ✓Git installed with a configured name, email, and credential helper
- ✓A cloned repo with push access to at least one branch
- ✓Basic comfort with git status, git log, and resolving a text conflict
- 'Failed to push some refs' means the remote branch holds commits your local branch doesn't have, so Git blocks a push that would discard them
- Fix it with git fetch plus git status, then git pull --rebase to replay your commits on top of the remote's newest work
- If your branches diverged, resolve each conflict, run git rebase --continue, then push normally without any force flag
- Never fix this with git push --force on a shared branch; if you must overwrite, use git push --force-with-lease so you can't wipe a teammate's fresh commits
Think of the shared branch as a shared grocery list on the fridge. You copied it in the morning, added your items at home, and tried to tape your copy over the fridge. But your roommate already added their items to the fridge copy while you were out. Git refuses to let your old copy wipe out their additions. That's the rejection. The fix is simple: copy the newest fridge list first, merge your items into it, then pin that combined list back up.
You're done coding. You run git push, lean back, and instead of a success message you get a wall of text ending in 'failed to push some refs to ...' plus a hint about fetching first. Your commits are fine. Your code is fine. Nothing is lost. Git simply noticed that the remote branch moved while you weren't looking, and it refuses to let your push silently bury someone else's work.
This rejection is Git's non-fast-forward protection doing its job. Every push asks the remote: does your branch contain everything my branch has, plus my new commits? If the remote holds even one commit you lack, the answer is no. The usual trigger is a teammate pushing to the same branch, but it also happens after history rewrites, pushes from two machines, or a wrong upstream.
The fix follows one repeatable loop: fetch what's new, combine it with your work, then push the combined result. Most of the time that means git pull --rebase followed by a normal push. When branches truly diverged you'll resolve a conflict or two first. The one thing you must not do is reach for --force on a shared branch, since that turns a two-minute sync into a deleted-commit incident for your team.
This guide walks you through reading the rejection, syncing with fetch, choosing rebase versus merge, untangling diverged branches, and using --force-with-lease for rare cases that need an overwrite.
What 'Failed to Push Some Refs' Actually Means
A ref is just a named pointer to a commit: main points at one commit, origin/main points at the commit your remote last had. When you push, Git asks the remote to move its pointer forward along your history. That move is a fast-forward only if the remote's current commit is an ancestor of your tip — meaning your branch contains everything the remote has, plus your new work on top. If the remote points at a commit you don't have, moving the pointer to your tip would orphan those commits, so the remote rejects the push with 'failed to push some refs'.
The error text always includes a hint to fetch first, and the per-branch line reads '[rejected] main -> main (non-fast-forward)'. Non-fast-forward is the technical name for the condition: your history and the remote's history can't be combined by simply sliding the pointer forward. Something has to give — either you absorb their commits into your branch, or you deliberately overwrite them.
This check happens entirely on the receiving side before any objects move, which is why a rejected push changes nothing. Your commits stay local, the remote stays untouched, and you can retry as often as you like. Treat the rejection as information, not damage: it names the exact branch pair that conflicts and tells you which side is missing commits.
Fetch First: See Exactly What You're Missing
Fetching downloads the remote's newest objects without touching your working tree, your branch pointer, or any file you have open. That's what makes it safe to run at any moment, even with uncommitted changes and a running dev server. After git fetch origin, your remote-tracking branches like origin/main reflect the server's true state, and git status can compare your branch against it honestly.
Read the status line carefully because it prescribes the fix. 'Your branch is behind origin/main by 3 commits' means pure catch-up: rebase or merge, then push. 'Have diverged, and have 2 and 3 different commits each' means both sides moved and you'll resolve the overlap. Pair this with git log --oneline HEAD..origin/main to list the exact commits you're missing, oldest first, so you can spot a migration, a config change, or a revert before you combine.
Make fetch-then-status a reflex before every push on shared branches. It takes two seconds and answers the only question that matters: did the world move since I last looked? Engineers who push blind discover the answer from a rejection; engineers who fetch first discover it from a status line and never see the error at all. When the log shows unfamiliar commits, read their messages before combining — a revert or migration in the gap changes how carefully you merge.
Pull --rebase vs Plain Pull: Pick the Right Combine
git pull is fetch plus combine in one step, and the flag you pass decides how the histories join. Plain git pull merges: it creates a merge commit with two parents tying your work and their work together. git pull --rebase replays: it temporarily shelves your commits, fast-forwards your branch to the remote tip, then reapplies your commits one by one on top. Both end with a branch the remote will accept, but they tell very different stories in the log.
Rebase keeps history linear, which is why most teams prefer it for everyday syncing on feature branches. Reviewers see your commits in a straight line on top of the newest main, bisect stays clean, and reverting one change doesn't drag a merge bubble along. Merge preserves the true chronology — both lines of work visibly joined at a point — which suits long-lived branches where the fact of parallel work matters, like release branches absorbing hotfixes.
Configure your default once so you stop deciding under pressure: git config pull.rebase true makes every pull rebase unless you override with --no-rebase. One caution: never rebase commits you've already pushed to a shared branch, since replaying rewrites hashes your teammates already based work on. Rebase is for catching up your unpublished work, not for rewriting published history.
Diverged Branches: Rebase, Resolve, and Push Cleanly
Divergence means both sides moved: they pushed 3 commits you lack and you wrote 2 commits they lack. Git can't fast-forward either direction, so you must produce a third history containing both. Start by listing each side with git log --left-right so you know exactly what must be reconciled — often the overlap is smaller than the scary 'diverged' label suggests, like a version bump on both sides or two edits to neighboring lines.
Rebase handles most divergences: your commits replay onto their tip, and each conflict pauses the replay for you to resolve. Open the conflicted file, choose the correct combined content (not just yours, not just theirs), stage it, and continue. If a replayed commit no longer makes sense on the new base, you can edit or drop it mid-rebase rather than carrying dead work forward. For big parallel efforts where both histories deserve preservation, merge instead and write a merge message that explains what joined and why.
After combining, verify before pushing: git log --oneline --graph shows one unified line (rebase) or a clean join (merge), and running the test suite catches semantic conflicts Git can't see, like two branches renaming the same function differently. Only then push normally. A successful push after divergence should need no force flags at all — if you're reaching for one, the combine isn't finished.
Force-With-Lease: the Only Safe Way to Overwrite
Sometimes overwrite is legitimate: you amended a commit on your personal branch, rebased your own pull request, or need to remove a pushed secret. Plain --force performs the overwrite blindly — it moves the remote pointer to your tip no matter what landed since your last fetch, silently orphaning anyone's fresh commits. --force-with-lease adds a condition: move the pointer only if the remote still points where I last saw it. If a teammate pushed meanwhile, the lease fails and your commits stay safe.
The lease compares your cached remote-tracking ref against the server's actual tip. That means its safety depends on a fresh fetch — a stale cache makes the lease check against old information. So the correct sequence is always fetch, rebase your overwrite onto the newest tip if possible, then force-with-lease. If the lease still fails, someone is actively pushing to that branch, and the right move is a conversation, not a bigger flag.
Scope this tool ruthlessly. It's acceptable on personal feature branches and your own pull requests. It's never acceptable on main, develop, or release branches — those should carry server-side protection that rejects all force-pushes. If you find yourself force-pushing a shared branch regularly, the workflow is broken: switch to pull requests so overwrites become reviews instead of races.
When the Rejection Isn't About Sync at All
Not every rejection is a missing-commit problem, and fetching won't fix those. A 'pre-receive hook declined' message means the server ran a policy script that vetoed your push: missing required reviews, unsigned commits, a forbidden file like a .env with secrets, or a branch name that violates convention. The hook's output names the rule — read it fully instead of retrying, because no sync operation overrides server policy.
Protected-branch rules produce similar-looking rejections on GitHub, GitLab, and Bitbucket: direct pushes to main blocked, required status checks not yet green, or force-pushes disabled. The fix is workflow, not Git plumbing — open a pull request, wait for CI, get the review. Likewise, pushing to the wrong upstream (your fork's main instead of upstream, or a stale branch name after a rename) rejects with confusing refspec errors; verify with git remote -v and the @{u} upstream check before assuming anything about history.
Large pushes can also be rejected mid-stream: oversized files trip the server's size limit, and slow connections time out during object transfer. git count-objects -v shows your bloat, and moving big assets to Git LFS before pushing solves it permanently. If the rejection mentions pack limits or RPC failures, shrink the push — split it into smaller commit batches or push fewer branches at once.
A --force Push at 4:52 PM Wiped a Migration and Stalled Deploys for 38 Minutes
- git push --force on a shared branch doesn't just publish your work, it rewrites everyone else's. A push rejected for non-fast-forward reasons is Git telling you someone else's commits are at risk. Sync with pull --rebase instead, and treat --force as banned on any branch two people use.
- Branch protection is a cheap guardrail that would have turned this incident into a second rejected push. Protect main and release branches so the server rejects force-pushes even when a tired engineer types the flag. The 5 minutes of setup pays for itself the first time it fires.
- Quiet pushes to shared branches are a process smell. The migration went up with no announcement and the hotfix went up with no fetch, and the two collided. A team norm of announcing shared-branch pushes, or routing them through pull requests, removes the collision window entirely.
| File | Command / Code | Purpose |
|---|---|---|
| inspect-rejection.sh | git fetch origin | Fetch First |
| sync-with-rebase.sh | git pull --rebase origin main | Pull --rebase vs Plain Pull |
| resolve-divergence.sh | git fetch origin | Diverged Branches |
| safe-overwrite.sh | git fetch origin | Force-With-Lease |
| diagnose-non-sync-rejection.sh | git push 2>&1 | tee /tmp/push-output.txt | When the Rejection Isn't About Sync at All |
Key takeaways
Common mistakes to avoid
6 patternsRunning git push --force on a shared branch to clear the error
Retrying the same push without fetching first
Accepting every default merge commit when syncing feature branches
Resolving rebase conflicts by always keeping 'your' side
Rebasing commits teammates already pulled
Ignoring the hook message and fighting policy with plumbing
Interview Questions on This Topic
What does 'failed to push some refs' mean, and is anything lost when it happens?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.
That's Git. Mark it forged?
6 min read · try the examples if you haven't