Understanding `git fetch` and `git pull`

Illustrated git pull and git fetch commands.

A basic understanding of Git may be helpful for reading this note. For an introduction to core concepts such as commits, the working tree, HEAD, and branches, see my earlier note.

Understanding git fetch and git pull

Overview

This note explains what git fetch and git pull actually do, and how they relate to several core Git references:

  • local branches
  • remote-tracking branches
  • upstream branches
  • fetch refspecs

At a high level:

  • git fetch updates the local repository’s record of the remote state
  • git pull first performs a fetch, and then integrates the upstream branch into the current local branch

Thus, a plain

1
git pull

may be understood conceptually as:

1
2
git fetch
git merge

or, if rebase is configured:

1
2
git fetch
git rebase

To understand these commands precisely, one must first distinguish between ordinary local branches and remote-tracking branches.


Local branches and remote-tracking branches

Assume that the repository has two local branches and one remote named origin:

1
2
3
4
5
6
7
local branches:
main
dev

remote-tracking branches:
origin/main
origin/dev

These names refer to different kinds of references:

  • main, dev: ordinary local branches
  • origin/main, origin/dev: remote-tracking branches corresponding to branches on the remote origin

Thus:

  • main is a local editable branch
  • dev is a local editable branch
  • origin/main is Git’s local record of where remote main was the last time the repository communicated with origin
  • origin/dev is Git’s local record of where remote dev was the last time the repository communicated with origin

A remote-tracking branch such as origin/main is still a local reference (pointer). It is not the remote branch itself; rather, it is Git’s local record of that remote branch.

Therefore, updating origin/main means updating the local record of remote main. It does not mean that the local branch main has moved.

Remote-tracking branches

A remote-tracking branch is Git’s local record of a branch on a remote repository. Such references are stored under refs/remotes/<remote>/....

Examples:

  • origin/main
  • origin/dev

A remote-tracking branch:

  • is stored locally in the repository
  • is updated by commands such as git fetch and git pull
  • is distinct from an ordinary local working branch

Its purpose is to record where Git last observed the corresponding remote branch. It is therefore mainly used for inspection and integration, rather than direct development.


Upstream branches

A local branch may have an upstream branch. The term upstream branch does not denote a distinct kind of branch; it denotes a tracking relation between a local branch and its default counterpart.

In common practice, that counterpart is a remote-tracking branch.

This relation is used by commands such as git pull and git status.

Typical examples include:

  • local main tracks origin/main
  • local dev tracks origin/dev

In these cases, origin/main and origin/dev are remote-tracking branches, and they simultaneously serve as the upstream branches of local main and local dev, respectively.

This relation may be inspected with:

1
git branch -vv

For example:

1
2
* dev  8f2c1ab [origin/dev]  message...
main a19d443 [origin/main] message...

This indicates that:

  • local dev has upstream origin/dev
  • local main has upstream origin/main

Therefore, when the current branch is local dev, a plain

1
git pull

usually means:

  1. fetch from the remote
  2. integrate origin/dev into local dev

The upstream branch determines what is integrated into the current local branch. It does not, by itself, determine the complete set of references updated during fetch.


Fetch refspecs

The references updated by git fetch are governed by the remote’s fetch refspec.

It may be inspected with:

1
git config --get-all remote.origin.fetch

A common default is:

1
+refs/heads/*:refs/remotes/origin/*

This mapping specifies that every branch under the remote namespace refs/heads/* is fetched into the corresponding local remote-tracking namespace refs/remotes/origin/*.

Under this default configuration, git fetch origin usually updates:

  • origin/main
  • origin/dev
  • other origin/* remote-tracking branches

This explains why running git pull on local dev may still update origin/main during the fetch step.

However, updating origin/main still does not imply that local main has moved.


Semantics of git fetch

git fetch contacts a remote, downloads objects (commits, etc.) not yet present in the local repository, and updates the corresponding remote-tracking branches (such as origin/main and origin/dev).

1
git fetch origin

It does not:

  • modify the current local branch
  • modify the working tree
  • stage changes for commit
  • merge remote changes into the current branch

A typical result is the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
Before fetch:
main -> C3
dev -> D2
origin/main -> C3
origin/dev -> D2
remote main is actually at C5
remote dev is actually at D4

After fetch:
main -> C3 (unchanged)
dev -> D2 (unchanged)
origin/main -> C5 (updated)
origin/dev -> D4 (updated)

Thus, git fetch updates the local repository’s knowledge of the remote state, but does not modify the current local branch.


Semantics of git pull

git pull first executes git fetch, and then integrates the upstream branch into the current local branch. The integration step is performed by merge or rebase, depending on configuration.

1
2
git pull
git pull --rebase

In the common case where local dev tracks origin/dev, invoking git pull on branch dev is conceptually equivalent to:

1
2
git fetch origin
git merge origin/dev

or, if rebase is configured:

1
2
git fetch origin
git rebase origin/dev

Two mechanisms should be distinguished carefully:

  • the fetch step updates remote-tracking branches according to the fetch refspec
  • the integration step applies only to the current branch and its upstream

Therefore, if both remote main and remote dev have advanced, then running git pull on local dev may yield:

1
2
3
4
origin/main  updated
origin/dev updated
dev updated by merge or rebase
main unchanged

In particular, the movement of origin/main does not imply the movement of local main.


Useful inspection commands

The following commands expose complementary aspects of branch state:

1
2
3
4
git branch -a
git branch -vv
git config --get-all remote.origin.fetch
git log --oneline --decorate --graph --all

git branch -a

Lists local branches and remote-tracking branches.

Example:

1
2
3
4
5
* dev
main
remotes/origin/HEAD -> origin/main
remotes/origin/dev
remotes/origin/main

This indicates that:

  • dev and main are local branches
  • the current branch is dev
  • origin/dev and origin/main are remote-tracking branches
  • the default branch of remote origin is main

git branch -vv

Shows each local branch together with its upstream and divergence status.

Example:

1
2
* dev  9476dca [origin/dev] fix: vibing ...
main 342dfbf [origin/main: behind 41] feat: changed ...

This indicates that:

  • local dev tracks origin/dev
  • local main tracks origin/main
  • local main is 41 commits behind origin/main

git config --get-all remote.origin.fetch

Shows the fetch refspec used for origin.

Example:

1
+refs/heads/*:refs/remotes/origin/*

This indicates that fetch updates the local origin/* remote-tracking references for all branches on the remote.

git log --oneline --decorate --graph --all

Displays the commit graph together with branch and reference labels.

Example:

1
2
3
4
5
* 9476dca (HEAD -> dev, origin/dev) fix: vibing ...
| * a81c9ef (origin/main) merge pull request ...
| * 73bc210 update docs
|/
* 342dfbf (main) feat: changed ...

This shows directly that:

  • HEAD is attached to local dev
  • local dev and origin/dev currently point to the same commit
  • origin/main is ahead of local main

Taken together, these commands reveal the existence, tracking relations, and relative positions of local branches, remote-tracking branches, and HEAD.