What is the difference between git checkout branch vs git checkout origin/branch?

It is important to understand how Git manages refs in order to understand how some of the Git commands operate.

The Git glossary defines a ref as:

ref: a name that begins with refs/ (e.g. refs/heads/master) that points to an object name or another ref (the latter is called a symbolic ref). There are a few special-purpose refs that do not begin with refs/. The most notable example is HEAD.

A branch, a remote branch, even a stash are all refs. Git uses their names to reference specific commits in the commit graph.

However, these refs are not necessarily equivalent. This is particularly true for local branches, also known as heads, which are named references to the commit at the tip of the branch.

In a normal situation, HEAD is a reference to the head of your current branch.

If HEAD is instead a reference to an arbitrary commit, then you are in a detached HEAD state.

Now, let’s see how this manifests in practice.

use case 1: wrong git checkout

Let’s consider two developers, Alina and Blake, working on the same repository.

They both start from the same commit graph.

Alina:

alina

ebc4912mainHEAD

Blake:

blake

ebc4912mainHEAD

The git server also has the same commit tree:

arenas.git

ebc4912mainHEAD

Blake creates a branch b1 using the git switch -c command (which also sets b1 as the current branch for her).

blake

blake git:( main ) git switch -c b1

Switched to a new branch 'b1'

blake git:( b1 )

She creates a commit on that branch:

blake

blake git:( b1 ) echo "A" > fileA.txt

blake git:( b1 ) git add fileA.txt

blake git:( b1 ) git commit -m 'add fileA'

[b1 7d60b3c] add fileA

 1 file changed, 1 insertion(+)

 create mode 100644 fileA.txt

blake git:( b1 )

And then she pushes it.

blake

blake git:( b1 ) git push

fatal: The current branch b1 has no upstream branch.

To push the current branch and set the remote as upstream, use

    git push --set-upstream origin b1

To have this happen automatically for branches without a tracking

upstream, see 'push.autoSetupRemote' in 'git help config'.

blake git:( b1 )

Git indicates that a branch can only be pushed to a remote if it has an upstream branch. If not, Git doesn’t know how to keep a reference to that commit on the remote.

The error message gives a hint on how to fix that: Git assumes that the developer may want to use the same branch name on the remote, so it suggests the command to set that name on the origin.

Blake runs the git push --set-upstream command suggested by Git:

blake

blake git:( b1 ) git push --set-upstream origin b1

Enumerating objects: 4, done.

Counting objects: 100% (4/4), done.

Delta compression using up to 8 threads

Compressing objects: 100% (2/2), done.

Writing objects: 100% (3/3), 267 bytes | 267.00 KiB/s, done.

Total 3 (delta 0), reused 0 (delta 0), pack-reused 0

remote: 

remote: Create a new pull request for 'b1':

remote:   http://localhost:3030/gitpowerup/arenas/compare/main...b1

remote: 

remote: . Processing 1 references

remote: Processed 1 references in total

To ssh://remote.mygit.com/gitpowerup/arenas.git

 * [new branch]      b1 -> b1

branch 'b1' set up to track 'origin/b1'.

blake git:( b1 )

Git indicates that everything is good: the local branch b1 in Blake’s repository is set up to track the branch origin/b1 on the origin server.

At this stage, both commit graphs are the same between the origin and Blake’s repository:

On the origin server:

arenas.git

7d60b3cebc4912b1mainHEAD

On Blake’s machine:

blake

7d60b3cebc4912b1mainorigin/b1HEAD

Back on Alina’s machine, her Git repository still has the previous commit graph.

In order to access that new branch b1, a git fetch is required. (We’ll see later what happens if she forgets to do the git fetch beforehand).

alina

alina git:( main ) git fetch --all

remote: Enumerating objects: 4, done.

remote: Counting objects: 100% (4/4), done.

remote: Compressing objects: 100% (2/2), done.

remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0

Unpacking objects: 100% (3/3), 247 bytes | 123.00 KiB/s, done.

From ssh://remote.mygit.com/gitpowerup/arenas

 * [new branch]      b1         -> origin/b1

alina git:( main )

Once done, Alina’s repository is in sync with the remote:

alina

7d60b3cebc4912mainorigin/b1HEAD

Now, Alina can switch branches.

Let’s try the wrong way first: git checkout origin/b1:

alina

alina git:( main ) git checkout origin/b1

Note: switching to 'origin/b1'.

You are in 'detached HEAD' state. You can look around, make experimental

changes and commit them, and you can discard any commits you make in this

state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may

do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 7d60b3c add fileA

alina

Git indicates that we are now in a detached HEAD state.

Git provides an explanation of what a detached HEAD is and what we can do from there.

Basically, in that state, you can explore the code and even create new commits, but those changes would not be in sync with the remote branch b1.

The reason for being in a detached HEAD state is that the parameter you gave to git checkout is not a local branch name, but instead a reference to an arbitrary commit.

In our specific git checkout command, the commit specified in the command line (origin/b1) is a reference to the commit (refs/remotes/origin/b1) on the remote for the current tip of the branch b1 there.

When in that state, the HEAD reference points directly to the commit that is also referenced by origin/b1 (abbreviation for refs/remotes/origin/b1):

alina

7d60b3cebc4912HEADmainorigin/b1

proper git checkout

Let’s switch back to safer territory, aka main (not strictly required):

alina

alina git checkout main

Previous HEAD position was 7d60b3c add fileA

Switched to branch 'main'

Your branch is up to date with 'origin/main'.

alina git:( main )

And then we perform the proper command: git checkout b1:

alina

alina git:( main ) git checkout b1

branch 'b1' set up to track 'origin/b1'.

Switched to a new branch 'b1'

alina git:( b1 )

This time, Git tells us that the branch b1 is set up to track origin/b1, and the commit graph reflects that:

alina

7d60b3cebc4912b1mainorigin/b1HEAD

It is worth pausing for a moment to compare these two graphs.

Even though both cases technically had the current commit as 7d60b3c, the setup of the refs is very different:

  • HEAD is a symbolic reference to b1 (the full name is refs/heads/b1)
  • both our local branch b1 and origin/b1 point to the same commit, meaning that the local branch and the remote are in sync

It is important to note that Git made this assumption because origin/b1 was already present in the local repository, indicating to Git that Alina wanted to track that branch.

In this state, both Alina and Blake can work on the same branch b1.

another use case: git checkout before a git fetch

Now, let’s have Blake create a branch b2 using git checkout -b, which does the same thing as git switch -c.

blake

blake git:( main ) git checkout -b b2

Switched to a new branch 'b2'

blake git:( b2 )

And let’s have her create and push a commit on that branch b2.

This will result in the following commit graph on both Blake and the origin server:

blake

38dd78c7d60b3cebc4912b1b2mainorigin/b1origin/b2HEAD

Now, Alina decides to switch to that branch b2:

alina

alina git:( main ) git checkout b2

error: pathspec 'b2' did not match any file(s) known to git

alina git:( main )

Oops… that didn’t work.

The reason is that Alina’s local repository is not aware of the branch b2 yet.

For Git to be aware of that branch, a git fetch is required:

alina

alina git:( main ) git fetch --all

remote: Enumerating objects: 4, done.

remote: Counting objects: 100% (4/4), done.

remote: Compressing objects: 100% (2/2), done.

remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0

Unpacking objects: 100% (3/3), 246 bytes | 61.00 KiB/s, done.

From ssh://remote.mygit.com/gitpowerup/arenas

 * [new branch]      b2         -> origin/b2

alina git:( main )

This command updates Alina’s Git commit graph:

alina

38dd78c7d60b3cebc4912b1mainorigin/b1origin/b2HEAD

Still no b2 branch in sight, but we have the origin/b2 ref, and that’s what Git will be using in the following git checkout:

alina

alina git:( main ) git checkout b2

branch 'b2' set up to track 'origin/b2'.

Switched to a new branch 'b2'

alina git:( b2 )

And now we are all set:

alina

38dd78c7d60b3cebc4912b1b2mainorigin/b1origin/b2HEAD

use case #3: local branch created before git checkout

Let’s have Blake create and push a branch named b3:

blake

6f53ff738dd78c7d60b3cebc4912b1b2b3mainHEAD

Now let’s suppose that Alina is not aware of that branch b3 yet and creates a local branch also named b3, and then does a git fetch:

alina

alina git:( main ) git checkout -b b3

Switched to a new branch 'b3'

alina git:( b3 ) git fetch --all

remote: Enumerating objects: 4, done.

remote: Counting objects: 100% (4/4), done.

remote: Compressing objects: 100% (2/2), done.

remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0

Unpacking objects: 100% (3/3), 247 bytes | 123.00 KiB/s, done.

From ssh://remote.mygit.com/gitpowerup/arenas

 * [new branch]      b3         -> origin/b3

alina git:( b3 )

Now we have 2 branches named b3:

alina

6f53ff738dd78c7d60b3cebc4912b1b2b3mainorigin/b1origin/b2origin/b3HEAD

In order for that local branch b3 to track the remote branch origin/b3, Alina needs to set it up with the git branch -u command:

alina

alina git:( b3 ) git branch -u origin/b3origin/b3

branch 'b3' set up to track 'origin/b3'.

alina git:( b3 )

This leads to the following commit graph:

alina

6f53ff738dd78c7d60b3cebc4912b1b2b3mainorigin/b1origin/b2origin/b3HEAD

Hmmm… the commit tree did not change! But internally, Git now knows that those branches are related and that the local branch b3 should track the remote branch origin/b3:

For instance, the git status command will tell Alina that her local branch b3 is behind the remote branch origin/b3 by 1 commit:

alina

alina git:( b3 ) git status

On branch b3

Your branch is behind 'origin/b3' by 1 commit, and can be fast-forwarded.

  (use "git pull" to update your local branch)

nothing to commit, working tree clean

alina git:( b3 )

And now Alina and Blake can share their work on the same branch b3.