In Git, why is a push rejected as non-fast-forward, and what should you do about it?
answer
- The remote checks a relationship between two commits
- Ancestry is the condition
- Otherwise published commits stop being reachable
- The receiving side enforces it, not your client
- Fetch and integrate before pushing again
basics
~20 sThe remote refuses because the branch's current commit is not an ancestor of the commit you are pushing, so accepting it would drop commits. Fetch, integrate the remote work with a merge or rebase, then push again.
solid answer
~40 sA push asks the receiving repository to move a ref, say `refs/heads/main`, from its current commit to yours. By default the receiving side only allows a **fast-forward**: the old commit must be an ancestor of the new one, so nothing already published becomes unreachable. If someone else pushed meanwhile — or you amended or rebased commits you had already published — that ancestry no longer holds and the update is rejected, with output like `! [rejected] main -> main (fetch first)` or `(non-fast-forward)`. Nothing on the remote changes; the ref simply is not moved. The correct response on a shared branch is `git fetch` followed by a merge or rebase of the remote commits into yours, then push. Overwriting the remote history instead is a deliberate, separate decision that discards whatever those commits were.
code
console · 5 lines$ git push origin main
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'origin'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally.go deeper
Recall that the message means the remote has commits you do not, and that the fix is to fetch and integrate before pushing again rather than to force anything.
Explain the ancestry rule the receiver applies and why it exists, and identify both causes: a colleague's push, and your own amend or rebase of already-published commits.
Show the diagnostic habit — fetch, read HEAD..origin/main, then choose merge or rebase deliberately — and know that other receiver-side rules and hooks produce rejections that look similar but mean something else.
Own the convention: decide as a team whether shared branches integrate by merge or rebase, and whether the receiving side should refuse non-fast-forward updates outright so the rule is not a matter of individual discipline.
## What a push is asking for A push is a ref update request: "set `refs/heads/main` on the remote from old value X to new value Y", plus the objects needed to make Y complete. The receiving side (`git receive-pack`) validates each requested update before applying it. The default rule for branches is the **fast-forward rule**: the update is allowed only if the ref's current value is an ancestor of the proposed value. ## Why that rule exists If X is an ancestor of Y, every commit reachable before the update is still reachable afterwards — the branch only gained history. If X is *not* an ancestor of Y, the commits between the merge base and X stop being reachable from that branch. On a shared branch those are other people's published commits, and losing them silently is exactly the failure Git refuses to enable by default. The rule is enforced by the receiver, not by your client, which is why it protects the team rather than only you. ## The two common causes 1. **Someone else pushed first.** Your remote-tracking ref is stale, so you built on an older tip. Your branch and the remote branch have diverged. 2. **You rewrote your own already-published commits.** `git commit --amend`, an interactive rebase, or a reset plus recommit produce *new* commit objects with new IDs. The remote's tip is a commit you no longer have in your branch's ancestry, so the update is non-fast-forward even though no one else touched anything. The printed reason hints at which you are in: Git tends to say `(fetch first)` when your remote-tracking ref is out of date — you have not even seen the remote's commit — and `(non-fast-forward)` when you have it locally but it is not an ancestor of what you are pushing. Either way the accompanying hint says the remote contains work you do not have locally. ## What happens to the data The ref is not moved and the branch on the remote is unchanged. Objects may have been transferred as part of the attempt, but a rejected update leaves them unreferenced on the receiving side; a modern receiver keeps incoming objects in a quarantine area precisely so a refused push does not litter the repository. From the branch's point of view, the push was a no-op. ## Responding correctly The reflex an interviewer is testing for is *fetch and look*, not *force*. 1. `git fetch origin` — refresh the remote-tracking ref so you can see what is actually there. 2. `git log --oneline HEAD..origin/main` — read the commits you are missing. This step is what distinguishes a considered response from a mechanical one. 3. Integrate: `git merge origin/main` (preserves both histories, creates a merge commit) or `git rebase origin/main` (replays your commits on top, keeping history linear). Which one you pick is a team convention. 4. Resolve any conflicts, then `git push` again — now a fast-forward. If the rejection came from your own rewrite of a branch that only you use, overwriting the remote is legitimate; but that is a distinct, explicit decision with its own safety mechanics, not the default answer to a rejection. ## Related receiver-side rules The fast-forward rule is one of several checks the receiving repository applies. Deletions can be refused with `receive.denyDeletes`, non-fast-forward updates can be refused outright regardless of client flags with `receive.denyNonFastForwards`, and a `pre-receive` or `update` hook can reject any proposed update for arbitrary reasons. All of them produce a rejected line and an unchanged remote ref, so read the message rather than assuming every rejection means "diverged". ## Tag updates The same protection applies to updating an existing tag: pushing a different commit under an existing tag name is refused unless forced, because tag consumers assume tags are immutable. Creating a new tag name is unaffected. ## Summing up for the interviewer Name the rule (ancestry), name the reason (published commits would become unreachable from the branch), identify both causes (a colleague's push, or your own rewrite), and describe the fetch-inspect-integrate-push loop. Mentioning that the rejection is enforced by the receiver and that nothing was modified on the remote rounds it out.
- You amended your last commit and now the push is rejected, though nobody else pushed. Why?Amending does not edit a commit; it creates a new commit object with a new ID and moves your branch to it. The remote's tip is the original commit, which is no longer in your branch's ancestry, so the update is non-fast-forward. The rejection is correct: accepting it would make the published commit unreachable from that branch.
- Does a rejected push mean the objects never left your machine?Not necessarily — objects can be transferred before the ref update is evaluated. What matters is that the ref is not moved, so the remote branch is unchanged; a modern receiver holds incoming objects in a quarantine area and discards them when the update is refused. From every reader's perspective the push simply did not happen.
- Which command tells you exactly what you are missing after such a rejection?`git fetch origin` first, then `git log --oneline HEAD..origin/main` lists the commits the remote has that you lack, and `git log --oneline origin/main..HEAD` lists yours. Reading both before choosing merge or rebase is the difference between an informed integration and a blind one.
- Can a push be rejected for reasons other than fast-forward?Yes. The receiving repository may refuse deletions via `receive.denyDeletes`, refuse all non-fast-forward updates regardless of client flags via `receive.denyNonFastForwards`, or reject any update from a `pre-receive` or `update` hook. Each produces a rejected line with its own reason, so read the message instead of assuming divergence.
saying these in an interview costs you the question
- Reaches for force-push as the first response
- Thinks the rejection means a permissions or network problem
- Believes their commits were lost by the rejection
- Says pulling with rebase is always required rather than a team convention
- Assumes only a colleague's push can cause it