Your push was refused. With Git 2.54 against a remote that someone else pushed to, the output is:
$ git push origin main
To github.com:acme/shop-api.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'github.com:acme/shop-api.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.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.If you have already fetched, the reason in brackets becomes (non-fast-forward) and the hint says “the tip of your current branch is behind its remote counterpart”. Both mean the same thing: the remote branch contains commits your local branch does not, and a normal push only ever adds commits on top of what the remote has. Git refuses rather than throw away someone’s work. (Output reproduced locally with Git 2.54.0; the remote path is replaced with a GitHub URL.)
Quick fix checklist
- Read the word in brackets:
fetch first/non-fast-forward(remote moved),stale info(your lease is out of date),remote rejected(server rule). - Normal case:
git pull --rebase origin main, fix any conflicts, thengit push. - Prefer merges?
git pull --no-rebase origin main, then push. - You amended or rebased a branch only you use:
git push --force-with-lease. [remote rejected] ... (protected branch hook declined)or aGH006message: push to a branch and open a pull request.error: src refspec main does not match any: the local branch does not exist (no commits yet, or it is calledmaster).
Before you start
You need a clean working tree (or a stash), because pulling may rewrite files. Know that origin/main is your local remote-tracking copy of the server’s branch, updated only by git fetch or git pull. Note whether anyone else commits to the branch you are pushing; that decides whether rewriting history is acceptable.
Why it happens
A push asks the server to move its branch pointer from commit X to commit Y. The server allows this by default only when Y is a descendant of X, a fast-forward: the old tip is still in the history, nothing is lost.
Two everyday situations break that rule:
- Someone else pushed first. Alice pushed to
main; you committed on the older tip, so the histories forked. Your client does not even have Alice’s commit, so it reportsfetch first. - You rewrote commits you already pushed.
git commit --amend,git rebaseorgit resetcreates commits with new IDs. The remote’s old ones are no longer in your history, so the push isnon-fast-forwardeven though nobody else touched the branch.
There is a third category where the server itself says no: protected branches, required pull requests, or a pre-receive hook. Those show ! [remote rejected] instead of ! [rejected], and fetching will not help.
Step-by-step walkthrough
Step 1: Reproduce the rejection
Two clones of one bare repository make the race visible:
git init -q --bare -b main origin.git
git clone -q origin.git alice && git clone -q origin.git bob
# (each clone: initial commit pushed by alice, then)
cd alice && echo alice >> app.txt && git commit -qam "Alice: tweak" && git push -q && cd ..
cd bob && echo bob > bob.txt && git add . && git commit -qm "Bob: add bob.txt"
git push origin main # ! [rejected] main -> main (fetch first)Step 2: Fetch and look before you act
$ git fetch
$ git status
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)Inspect what the other side added: git log --oneline main..origin/main. Commits from a teammate or your other machine must be integrated; commits you deliberately replaced (by amending or rebasing) get overwritten with a lease (Step 4). Pushing now gives the non-fast-forward variant:
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to 'github.com:acme/shop-api.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.Step 3: Integrate with rebase or merge
Rebase puts your commits on top of the remote tip, giving a straight line:
$ git pull --rebase origin main
* branch main -> FETCH_HEAD
Successfully rebased and updated refs/heads/main.
$ git push origin main
b77bff9..eebc7fa main -> mainA merge (git pull --no-rebase) keeps both lines and adds a merge commit. Both are correct. To set a default once:
git config --global pull.rebase true # or false for merge, or: pull.ff onlyIf the rebase stops on a conflict, edit the files, git add them, run git rebase --continue, then push. git rebase --abort returns you to where you started.
Step 4: Overwrite deliberately with –force-with-lease
When you rewrote your own branch, integrating would bring back the commits you replaced. Overwrite instead, but with a lease: --force-with-lease only succeeds if the remote branch still points where your origin/<branch> says it does. If a teammate pushed in the meantime, you get:
$ git push --force-with-lease origin feature
! [rejected] feature -> feature (stale info)
error: failed to push some refs to 'github.com:acme/shop-api.git'That rejection is the feature working: your view is stale, so fetch and look. The catch: if something fetched in the background (an IDE, say), the lease is satisfied and the push overwrites the teammate’s commit. Add --force-if-includes (Git 2.30+) to also require that the remote tip is in your branch’s reflog, meaning you actually integrated it:
$ git push --force-with-lease --force-if-includes origin feature
! [rejected] feature -> feature (remote ref updated since checkout)
hint: Updates were rejected because the tip of the remote-tracking branch has
hint: been updated since the last checkout.Plain --force checks nothing and should be reserved for repositories nobody else uses.
Step 5: Recognise server-side rejections
A protected branch on GitHub produces a remote: line with a GH006 code, for example:
remote: error: GH006: Protected branch update failed for refs/heads/main.
! [remote rejected] main -> main (protected branch hook declined)
error: failed to push some refs to 'github.com:acme/shop-api.git'Repository rulesets report GH013 instead. Self-hosted servers often reject through a hook, which a plain Git server shows as (pre-receive hook declined). No local command fixes these: push to a new branch (git push -u origin HEAD:fix/pricing) and open a pull request, or ask an admin.
Worked scenario
Bob amends the last commit on feature/search after review and pushes. Meanwhile Alice pushed a small fix to the same branch.
Broken attempt:
git commit --amend -m "Add search endpoint (reviewed)"
git push origin feature/search # rejected: fetch first
git push --force origin feature/search # "works", deletes Alice's fixDiagnosis: the plain push failed because the branch has a commit Bob never saw, and --force succeeded only because it checks nothing. Alice’s fix is now gone from the remote.
Fixed sequence:
$ git fetch origin
$ git log --oneline feature/search..origin/feature/search
531954b Alice: fix typo
9316fd4 Add search endpoint
$ git cherry-pick 531954b
$ git push --force-with-lease=feature/search:531954b origin feature/search
+ 531954b...9e0ff47 feature/search -> feature/search (forced update)The log shows Bob’s original commit (which he replaced on purpose) and Alice’s fix (which he must keep). He copies her commit onto his amended branch, then overwrites the remote with an explicit lease: “replace the branch only if it still points at 531954b, the commit I just reviewed”. Rebasing onto origin/feature/search would be wrong here, because it would replay the amended commit on top of the original one it replaced. Note that --force-if-includes rejects this push, since the cherry-pick created a copy of Alice’s commit rather than including the original; the explicit lease is the right tool.
Common mistake
Reaching for --force. The hint says pull, not force. Forcing a shared branch silently deletes the commits that caused the rejection, and recovery depends on someone still having them locally.
Pulling right after rewriting your own branch. If you rebased a pushed feature branch, git pull merges the old copies of your commits back in, so every commit appears twice. In that case the lease push is correct and the pull is wrong.
Confusing src refspec errors with rejections. error: src refspec main does not match any followed by failed to push some refs means your local branch named main does not exist: you have no commits yet, or the branch is master. Commit first, or push what you have with git push -u origin HEAD:main.
Verify the behavior
After a successful push the output shows a range, not a rejection:
To github.com:acme/shop-api.git
b77bff9..eebc7fa main -> mainA forced update shows + and (forced update) instead. Then confirm nothing is left on either side:
git fetch
git status -sb | head -1 # ## main...origin/main (no [ahead N] or [behind N])
git log --oneline -3 origin/mainThe remote commit you fetched in Step 2 should appear in git log as an ancestor of your tip.
Interview exercise
“Explain the difference between git push --force and git push --force-with-lease. Why can --force-with-lease still lose a teammate’s work, and how do you prevent that?”
Answer and reasoning
--force tells the server to set the branch to my commit whatever it currently points at. --force-with-lease adds a compare-and-swap: by default it expects the server’s branch to equal my local origin/<branch>, and the server refuses (stale info) if it moved.
The weakness is that the expectation comes from my remote-tracking ref, which any git fetch updates, including a background fetch by an editor. After that fetch the lease matches the new remote tip even though I never looked at or integrated the teammate’s commit, so the forced push still deletes it.
Two fixes: pass the expected value explicitly (--force-with-lease=feature:<sha I last saw>), or add --force-if-includes, which also checks that the remote tip appears in my local branch’s reflog, proving I had it in my history. At the process level, protect shared branches so force pushes are disabled entirely, and only rewrite branches you own.