Ch. 16 · Git & CI/CD

Git: Your branch and 'origin/main' have diverged (Fix)

Fix 'Your branch and origin/main have diverged' and fatal: Need to specify how to reconcile divergent branches: merge, rebase or ff-only.

~7 min readintermediateupdated Oct 4, 2026

Two messages, one situation. git status reports it after a fetch, and git pull refuses to continue (Git 2.54.0 output from a local reproduction):

$ git status
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 2 different commits each, respectively.
  (use "git pull" if you want to integrate the remote branch with yours)

$ git pull
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.
Text

Your local main has 1 commit that origin/main does not, and origin/main has 2 that you do not. Git can no longer move one pointer forward to match the other, so it wants you to pick how to combine them. Nothing is broken and nothing is lost.

Quick fix checklist

  • See both sides first: git log --oneline --graph main origin/main.
  • Local commits on a shared branch: git pull --rebase (linear history).
  • Want to keep both lines visible: git pull --no-rebase (merge commit).
  • You never commit on this branch directly: git pull --ff-only, and if that fails, find out why.
  • You rebased or amended a branch you already pushed: don’t pull; git push --force-with-lease.
  • Set a default once: git config --global pull.rebase true (or false, or pull.ff only).

Before you start

Commit or stash local edits so the working tree is clean. Know that origin/main is your last fetched snapshot of the server’s branch; git status compares against it and says nothing about pushes made since your last fetch, so run git fetch first. You should know what a merge commit and a rebase are; the merge versus rebase note covers that in depth.

Why it happens

Picture the commit graph. You and the remote started from the same commit, then each added work:

$ git log --oneline --graph --all
* beb7d58 Add cache config
| * 4d9b7f9 Add README
| * 5219616 Set log level
|/
* 4293b40 Add config
Text

beb7d58 is yours; the two on the right are a teammate’s, now on origin/main. The “1 and 2” in the status message are exactly those counts, which you can compute yourself:

$ git rev-list --left-right --count main...origin/main
1	2
Text

Three common routes lead here: a teammate pushed while you committed locally; you committed on two machines; or you rewrote commits you had already pushed (rebase, amend, reset), so your copies and the server’s copies of the same work now have different IDs.

Before Git 2.27, git pull silently merged in this situation. Git 2.27 started warning when no pull.rebase preference was set, and since Git 2.33.1 a pull that cannot fast-forward stops with fatal: Need to specify how to reconcile divergent branches. until you choose. If the branches have not diverged (you are only behind), a plain git pull still fast-forwards without asking.

Step-by-step walkthrough

Step 1: Look at what each side has

git fetch
git log --oneline main..origin/main    # commits only on the remote
git log --oneline origin/main..main    # commits only on your branch
Terminal

This is the decision point. If the remote-only commits are your own work under different IDs (same messages, different hashes), you rewrote history: skip to the worked scenario. If they are genuinely new work, you need to combine.

Step 2: Option A, rebase your commits on top

$ git pull --rebase
Successfully rebased and updated refs/heads/main.
$ git log --oneline --graph
* 6a050ef Add cache config
* 4d9b7f9 Add README
* 5219616 Set log level
* 4293b40 Add config
$ git status -sb
## main...origin/main [ahead 1]
Text

Your commit got a new ID (beb7d58 became 6a050ef) because its parent changed. History is a straight line and git push is a fast-forward. If a commit conflicts, Git pauses; fix, git add, git rebase --continue.

Choose rebase when the local commits are yours alone and unpushed, which is the normal case on a shared main or develop.

Step 3: Option B, merge both lines

$ git pull --no-rebase
Merge made by the 'ort' strategy.
 README.md  | 1 +
 config.yml | 1 +
 2 files changed, 2 insertions(+)
 create mode 100644 README.md
$ git status -sb
## main...origin/main [ahead 2]
Text

You are now ahead by 2: your commit plus the merge commit. Nothing was rewritten, so this is safe even when your local commits were already shared elsewhere. The cost is a merge commit like “Merge branch ‘main’ of github.com:acme/app”, which adds noise when it happens on every pull.

Step 4: Option C, fast-forward only

--ff-only refuses to create anything. On divergent branches it stops:

$ git pull --ff-only
hint: Diverging branches can't be fast-forwarded, you need to either:
hint:
hint: 	git merge --no-ff
hint:
hint: or:
hint:
hint: 	git rebase
hint:
hint: Disable this message with "git config set advice.diverging false"
fatal: Not possible to fast-forward, aborting.
Text

(Older releases suggest git config advice.diverging false; the git config set form is newer.) That failure is useful: set pull.ff only on branches you only consume, such as main when all changes arrive through pull requests. An unexpected divergence then shows up as an error instead of an accidental merge.

Step 5: Make your choice permanent

git config --global pull.rebase true   # rebase on every pull
# or per repository, without --global
git config --show-origin --get pull.rebase
Terminal

--show-origin tells you which file the value came from, which helps when a repository-level setting overrides your global one. git pull --rebase or --no-rebase on the command line always wins over config.

Worked scenario

Sam pushed feature/search with two commits, then rebased it locally onto the latest main to pick up a hotfix:

$ git rebase main
Successfully rebased and updated refs/heads/feature/search.
$ git status
On branch feature/search
Your branch and 'origin/feature/search' have diverged,
and have 3 and 2 different commits each, respectively.
Text

Following the hint and running git pull --no-rebase produces this mess, each commit present twice:

*   9423912 Merge branch 'feature/search' of github.com:acme/app into feature/search
|\
| * 0551c39 Add search tests
| * 43e2cf7 Add search endpoint
* | 90cac20 Add search tests
* | 00f6927 Add search endpoint
* | d63bb61 Hotfix: null check
|/
* f94754e Initial
Text

Diagnosis: the “2” on the remote side are Sam’s own old commits. git log --cherry-mark confirms it by marking patch-identical pairs with =:

$ git log --oneline --cherry-mark --left-right feature/search...origin/feature/search
= 90cac20 Add search tests
= 0551c39 Add search tests
= 00f6927 Add search endpoint
= 43e2cf7 Add search endpoint
< d63bb61 Hotfix: null check
Text

Every remote-only commit has an equal on the local side, so nothing on the server needs keeping. The fix is to replace the remote branch, not to pull:

$ git push --force-with-lease
 + 0551c39...90cac20 feature/search -> feature/search (forced update)
$ git status -sb
## feature/search...origin/feature/search
Text

If any remote commit had lacked an = partner, it would be someone else’s work, and Sam would cherry-pick it before pushing.

Common mistake

Pulling after rewriting your own branch. As shown above, merging old and new copies of the same commits duplicates them, and a later cleanup is harder than the lease push would have been.

Setting pull.rebase true and rebasing published merges. Rebase flattens merge commits by default. If your local branch contains a merge you want to keep, use git pull --rebase=merges, or merge instead.

Copying the hint’s git config lines blindly. Run only one of them. Setting both pull.rebase true and pull.ff only makes rebase pulls refuse whenever a fast-forward is impossible, which is exactly when you wanted the rebase.

Verify the behavior

git fetch
git status -sb | head -1
git rev-list --left-right --count main...origin/main
Terminal

Before pushing you should see [ahead N] and N 0: nothing remains on the remote that you lack. After git push, the status line is ## main...origin/main with no counts and the rev-list prints 0 0.

Interview exercise

“git status says your branch and origin/main have diverged with 1 and 2 different commits. What exactly do those numbers mean, and how do you decide between merge, rebase and fast-forward-only?”

Answer and reasoning

The first number is commits reachable from my branch but not from origin/main; the second is the reverse. Both being non-zero means neither tip is an ancestor of the other, so neither side can fast-forward. It is computed against my last fetch, so I fetch first.

Then I ask what the commits are. If my local commit is unpublished work, I rebase: it becomes one linear history and the push is a fast-forward. If my commits were already shared (another branch was built on them, or they were pushed elsewhere), I merge, because rebasing changes their IDs and breaks anyone who built on them. On branches I only consume, I configure pull.ff only so divergence becomes an error that prompts investigation.

The special case is when the remote-only commits are older versions of my own commits after a rebase or amend. Merging duplicates them; the right move is git push --force-with-lease, after checking with --cherry-mark that nothing on the remote is someone else’s.

Continue learning

More in Git & CI/CD

esc