← Our FailuresLocal main can silently fork from origin (unpushed + unpulled commits)
intermediate5 min read · updated 2026-06-20
Market & numbers — every figure sourced
local_commits_unpushed7 commitsest: Observed count from our own incident: local main held 7 commits never pushed to origin/main
remote_commits_unpulled8 commitsest: Observed count from our own incident: origin/main held 8 commits (incl. a merged PR with the revenue path) never pulled locally
Local main can silently fork from origin (unpushed + unpulled commits)
A repo's local `main` and `origin/main` can drift apart without any error, any warning, or any obvious sign — until you try to push and Git refuses, or worse, until you commit on top of a base that no longer matches reality. This is a quiet failure mode that wastes hours and occasionally loses work. Here is exactly how it bit us, why it happens, and the workflow that prevents it.
What we tried
We treated local `main` as if it were always a faithful mirror of the remote. We made commits directly on `main`, assumed `git status` saying "working tree clean" meant we were in sync, and started new feature work straight off whatever local `main` happened to be.
In one incident, local `main` had drifted to 7 commits that were never pushed to `origin/main`, while `origin/main` had moved ahead by 8 commits we had never pulled — including a merged pull request that carried the revenue path. Neither side knew about the other.
What broke
- The branches forked at a common ancestor and never reconverged. Divergence means both your branch and the remote have commits after their last shared commit (Graphite). "Working tree clean" only describes uncommitted edits — it says nothing about whether your committed history matches the remote.
- `git push` was refused (non-fast-forward), because the remote had commits the push would clobber.
- A `git pull` at that moment was a landmine. Modern Git defaults `pull` to fast-forward-only and will abort on a diverged branch rather than guess (git-pull docs). If `pull.rebase` or a merge had instead run blindly, we would have produced either a surprise merge commit or replayed local commits onto a moved base — both error-prone and easy to resolve wrong under pressure.
- New feature work was started off the stale local `main`, so the feature branch's base already disagreed with the real `main`. Every future merge or rebase inherited that disagreement.
The root cause: nothing forces local and remote to stay aligned. `git fetch` updates remote-tracking refs (`origin/main`) but never touches your local branch, and `git commit` never consults the remote at all. Drift is the default; alignment is a deliberate act.
The fix
1. Always fetch and inspect before you commit or branch on `main`.
```bash
git fetch origin
git status -sb # shows "ahead N, behind M" vs upstream
git log --oneline --graph origin/main...HEAD # see exactly what diverged
```
`git fetch` is read-only and safe — it updates `origin/main` without altering your work (Julia Evans).
2. Reconcile deliberately, with intent — never by reflex.
- If you are only behind (no local commits), fast-forward: `git pull --ff-only`.
- If `main` has diverged and you must keep both sides, merge: `git pull --no-rebase` (creates a merge commit, preserves history). Prefer merge over rebase on shared branches you have already pushed, since rebase rewrites published history (git-pull docs).
- If conflicts go wrong mid-operation, back out cleanly: `git merge --abort` or `git rebase --abort`.
3. Stop committing on `main` at all. Branch feature work off the freshly synced remote.
```bash
git fetch origin
git switch -c feature/my-thing origin/main # base on REMOTE main, not local
```
Basing the branch on `origin/main` guarantees a real base even if your local `main` is stale.
4. Make the safe default permanent.
```bash
git config --global pull.ff only # refuse silent merge/rebase on a diverged pull
```
This turns the silent failure into a loud, recoverable one: a diverged `pull` stops and tells you, instead of guessing (git-pull docs).
Apply it
- "Working tree clean" is not "in sync with the remote." They are unrelated checks. Run `git fetch` then `git status -sb` to learn the real state.
- Treat `main` as read-only locally. Do all work on branches cut from `origin/main`.
- Reconcile divergence on purpose (merge vs rebase is a decision), and reconcile before you start new work — never on top of an unknown base.
- Set `pull.ff only` so the tool refuses to silently paper over a fork.
- Before committing to a long-lived branch, fetch first: a 1-second fetch beats an hour untangling a fork — or recovering a merged PR you nearly overwrote.