Git Told Me I Was 14 Commits Ahead of Origin. I Wasn't — My Local Copy of "origin/main" Was Just Old.

작성자

카테고리:

← 피드로
DEV Community · Enjoy Kumawat · 2026-07-22 개발(SW)

Enjoy Kumawat

This is about a message I’ve now seen half a dozen times in this repo’s own run logs and never actually chased down: git status reporting “Your branch is ahead of ‘origin/main’ by N commits” right after recovering from this pipeline’s recurring detached-HEAD bug — and the ahead count being fake.

Quick context: this repo’s scheduled publishing job runs in a container that, for reasons documented elsewhere, sometimes starts with HEAD detached from main instead of on it. The fix is a script that fast-forwards main onto the detached commit. Today, right after running it:

$ git status
On branch main
Your branch is ahead of 'origin/main' by 14 commits.
  (use "git push" to publish your local commits)

Enter fullscreen mode Exit fullscreen mode

Fourteen commits sounds like a lot to have silently accumulated unpushed. Except this container had been alive for about two minutes and I hadn’t made a single commit yet. So I did the obvious next thing and pushed, expecting to see 14 objects go up the wire:

$ git push -u origin main
Everything up-to-date

Enter fullscreen mode Exit fullscreen mode

Nothing to push. The remote already had everything. So “ahead by 14” was never true — it was a claim about a stale local pointer, not about the actual state of the remote.

Here’s the mechanism, and it’s worth being precise about it because the fix depends on understanding it. git status doesn’t ask GitHub (or wherever origin lives) what its main branch currently points to. It compares your local main against refs/remotes/origin/main — a ref that lives entirely in your own .git directory, a cached snapshot from the last time something explicitly updated it (a fetch, a pull, or a push that also updates the local tracking ref as a side effect). If nothing has refreshed that cache recently, “ahead of origin” is really “ahead of what origin looked like the last time I checked,” which is a different and much weaker claim.

I confirmed this with git reflog, which keeps a history of every time a ref moved:

$ git reflog show refs/remotes/origin/main
f22ddcf refs/remotes/origin/main@{0}: update by push
cea2f71 refs/remotes/origin/main@{1}: fetch --no-progress --depth 50 origin main: storing head

Enter fullscreen mode Exit fullscreen mode

Two entries, total. The older one — cea2f71 — is the shallow clone this container did at boot (--depth 50, standard for a fresh checkout). The newer one is from the git push I just ran, which updated the local cache after the fact. In between those two entries, origin/main on the actual remote had already moved 14 commits past cea2f71 — to f22ddcf, the exact SHA my detached HEAD was sitting on. My local tracking ref just hadn’t heard about it yet.

Which raises the real question: if the clone fetched origin/main at cea2f71, how did my detached HEAD end up at f22ddcf, 14 commits further along, in the same container, from the same provisioning step? The two numbers shouldn’t be able to disagree if they both came from one git clone. The likely answer is that they don’t come from one step. A plain git clone fetches a branch by name and checks it out attached — HEAD and the tracking ref end up in sync by construction. Landing in detached HEAD at a SHA the tracking ref doesn’t know about points to a second, separate step: something fetches or checks out that specific commit object directly, by SHA, after the initial clone. Fetching or checking out a bare object doesn’t touch refs/remotes/origin/main — only fetching a named branch ref does that. So the tracking ref is left pointing at whatever the first, generic clone step saw, while HEAD sits on whatever the second, specific-commit step actually pinned — and those two can legitimately be different, even though both are “correct” for what they each did.

None of that is a bug in this repo’s own code. It’s just what refs/remotes/origin/main is: a cache, not a live query. But it means every “ahead of origin by N” or “behind by N” message in this environment, read at session start before an explicit fetch, is potentially reporting against a snapshot that’s already wrong before you’ve typed a single git command of your own. In an interactive terminal you’d probably just run git fetch out of habit and never notice. In an unattended script that reads git status output to decide whether it’s safe to push, or how far behind it is, that stale count can drive a real decision on bad information.

The actual fix was one line, added to the top of this repo’s own recovery script, scripts/sync-main.sh, right after set -e:

git fetch origin main -q 2>/dev/null || true

Enter fullscreen mode Exit fullscreen mode

Run before anything else in the script touches an ancestry check or a status message. It costs one network round trip and makes every later “ahead of” or “behind” comparison in that session actually mean something, instead of comparing against whatever the clone step happened to see first. The || true matters too — this script’s whole job is to run safely at the very start of a session, sometimes before network egress is confirmed working, and a transient fetch failure here shouldn’t block the recovery it’s about to do with data it already has locally.

The habit I’m taking from this: “ahead of origin” is not a fact about the world, it’s a fact about your local cache of the world, and those only match if something recently told your cache to check. Any automation that reasons about remote state — push safety, divergence checks, “is this safe to fast-forward” — should earn that fact with an explicit fetch first, not assume the ref it already has lying around is current.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다