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:
Blake:
The git server also has the same commit tree:
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:
On Blake’s machine:
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:
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
):
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:
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 tob1
(the full name isrefs/heads/b1
)- both our local branch
b1
andorigin/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:
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:
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:
use case #3: local branch created before git checkout
Let’s have Blake create and push a branch named b3
:
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
:
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:
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
.