Git Internals

Refspecs and Ref Updates

Explain how refspecs determine which refs are mapped and updated during fetch and push.

Who This Is For
  • Readers building a durable Git mental model
  • Developers who keep running into history, ref, or recovery confusion
Prerequisites
  • Comfort reading basic Git output
  • A rough idea of commits, branches, and HEAD
Common Risks
  • Learning low-level terms without connecting them to commands
  • Collapsing objects, refs, and working state into one concept

If you think about sync as only "download objects" or "upload commits," you are seeing only half of the operation. Git also has to answer: which refs should those updates change?

That is what a refspec describes.

Start with the real problem

How Refspecs Map Remote to Local RefsFetch refspecs map remote refs/heads/* to local refs/remotes/origin/*. Push refspecs map local refs/heads/* to remote refs/heads/*.
Fetch mapping
HEAD -> refs/heads/feature/login
refs/heads/main → refs/remotes/origin/main: feature/login -> F
Remote refs: origin/main -> D
refs/tags/* → refs/tags/*: v2.0.0 -> D
Push mapping
HEAD -> F
DFG

During both fetch and push, Git is not only transferring objects. It also has to decide:

  • which source ref to read from
  • which destination ref to update
  • whether that update is allowed
  • how multiple refs should be mapped

A refspec is the rule that connects the source side to the destination side.

What a refspec is

You can think of a refspec as a mapping declaration:

  • left side: source ref
  • right side: destination ref

A common example looks like this:

refs/heads/*:refs/remotes/origin/*

That does not mean "copy everything everywhere." It means:

  • remote branch refs under refs/heads/*
  • map into local remote-tracking refs under refs/remotes/origin/*

So a fetch does not usually write directly into your working branches. It writes into remote-tracking refs according to the refspec.

Why fetch depends on it

When you run git fetch origin, Git broadly does this:

  1. learn which refs exist on the remote
  2. transfer the needed objects
  3. apply fetch refspec rules to decide which local refs to update

That is why:

  • the remote has main
  • your local origin/main gets updated
  • your local main does not move automatically

That separation is intentional. It lets Git distinguish between "record the remote state" and "update my current branch."

Why push depends on it too

Push has the same mapping idea in the other direction.

Git needs to know:

  • which local ref you want to publish
  • which remote ref should receive it

For example:

git push origin feature:main

Conceptually means:

  • local source ref: feature
  • remote destination ref: main

Once you understand refspecs, that command stops looking magical and starts looking like a precise ref mapping.

Use case 1: why git push origin main works

Many people write:

git push origin main

It looks like only one name was given, but Git can infer the source-to-destination mapping from defaults and context. In practice it means "push my local main to remote main."

So the branch name itself is not inherently magical. Git is filling in a refspec-like mapping for you.

Use case 2: why fetch gives you origin/main

A common beginner question is:

  • the remote updated main
  • why did fetch update origin/main instead of my main?

The answer is the default fetch refspec. It typically maps:

  • remote heads
  • into local remote-tracking refs

So fetch updates your local record of the remote, not your working branch.

Use case 3: why deleting a remote branch is still a ref operation

More advanced push forms, including deleting a remote branch, make more sense once you realize push is fundamentally a remote ref update.

At a conceptual level, you are not "deleting some files on the server." You are asking the remote to remove a ref from its namespace.

Special case: wildcard mappings

A refspec can describe one fixed branch, but it can also use patterns like * to map groups of refs.

That is especially useful for:

  • default fetch behavior across many branches
  • mirrored repositories
  • namespaced ref layouts

Special case: allowed vs rejected updates

Ref updates are also where safety rules show up.

For example, push may be rejected because the remote checks whether:

  • the update is fast-forward
  • the destination already contains commits you would overwrite

So a rejected push is often a ref update rule issue, not just a content conflict.

Common misconceptions

"Fetch and push only move commit objects"

Incomplete. They move objects and update refs. Refspecs explain where those updates land.

"origin/main is the remote branch itself"

Not exactly. origin/main is your local record of the remote branch.

"Push failures are only about file conflicts"

No. Many push failures are about whether the ref update is allowed.

Why this helps you understand commands

Once refspecs make sense, it becomes easier to understand:

  • why fetch updates remote-tracking refs
  • how push can send one local branch to a differently named remote branch
  • why default fetch and push behavior seems automatic
  • why some updates are rejected
  • why sync is really object transfer plus ref movement

Suggested follow-up

It pairs especially well with:

  • git fetch
  • git push
  • git remote
  • git branch -r
  • git ls-remote