Reputation: 5974
As the title says, why does this happen?
As explained in documentation and tutorials origin/master is actually a local copy of branch on remote. So why does checking out to it leads to detached HEAD if it is "Just a local copy of the branch"
Upvotes: 3
Views: 555
Reputation: 490118
This is just the way branches are defined, in Git.
Specifically, a branch—that is, a regular, ordinary, local branch—is one kind of reference. So is a tag, and so is a remote-tracking branch. What distinguishes one from another is its full name, in the same way that if you are at a party with several people named Bob, they probably at least have different full names.
A branch name starts with refs/heads/
, while a remote-tracking branch starts with refs/remotes/
, followed by the name of the remote. This means refs/heads/master
is a branch name, while refs/remotes/origin/master
is a remote-tracking branch name.
These "reference" things simply store the ID of a (one, single) Git object, usually a commit.
While we call master
a branch, it's actually a branch name. Hence, it's a reference, and it stores the ID of a single commit. Which commit? Well, it's the tip commit of the branch. But then what the <adjective> is a branch?
A Git branch is also a set of commits. The way Git finds the set is to start with a branch name, which gives it the ID of the tip commit on the branch. From that commit, Git can find one or more earlier commits, called the parents of that commit. From those commits, Git can find their parents, and so on, back through time.
When people talk about "a branch" in Git, you have to determine from context whether they mean "the branch name", "the tip commit on the branch", or "some set of commits on that branch, maybe excluding commits that are on that branch and some other branches at the same time."
For (much) more on this, see What exactly do we mean by "branch"?
git checkout
a branch name, you get put on that branchUsing git checkout master
or git checkout develop
, you tell Git that you would like to "get onto" the given branch. How this actually works under the hood is that Git turns the branch name into the full reference name—refs/heads/branch
—and writes that name into the file .git/HEAD
. That is, your HEAD just records the current branch's name.
In this (attached-head) mode, the HEAD
file acts as an indirect reference. The tip of the branch is still recorded in the "real" reference, as stored under the full name. And for reasons good or bad (we'll see a few in a moment), Git restricts the strings allowed to be stored in HEAD
: they can only be ones that start with refs/heads/
.
Since refs/remotes/origin/master
does not start with refs/heads/
, you're simply not allowed to be "on" refs/remotes/origin/master
. Likewise, you cannot be on a tag like refs/tags/v2.1
: that does not start with refs/heads/
either. So if you try to check out one of these, Git puts you in "detached HEAD" mode.
In "detached HEAD" mode, Git records the raw hash ID of a commit in the .git/HEAD
file, instead of recording the name of the current branch. This means you're not "on" any branch. However, Git still lets you make new commits, or move around in history: when you do any of these things it just stores a new ID into .git/HEAD
as needed.
Mostly, detached HEAD mode is good for looking at history. It's also used under the hood by commands like git rebase
that copy whole chains of commits and then adjust branch-names.
So, if HEAD
has a hash ID in it, it's detached and that's your current commit. Otherwise, HEAD
has a branch-name in it (starting with refs/heads/
), and that branch-name stores the ID of ("points to") the tip commit of the branch.
To find out which branch you're on, Git reads HEAD
. If it has a raw hash ID, you're not "on" any branch (though in effect you're on a special anonymous branch). Otherwise Git strips off the refs/heads/
part and prints that as the name of the branch.
That's Reason 1 for disallowing remote-tracking branch names in HEAD
: there's no refs/heads/
to strip off.
Branches grow by making new commits. To make a new commit, Git:
HEAD
(following it to a branch reference if it's indirect).HEAD
(following it to the branch reference if it's indirect).This new commit now points back to the previous branch-tip, and because the new ID is now stored in the branch name, the new commit is now the tip of the branch you're on!
But that immediately leads to Reason 2 for disallowing remote-tracking branches: making new commits would result in you not tracking the remote correctly. The remote-tracking reference is supposed to point to a commit you obtained from the remote, not one you made locally.
Obviously both of these reasons could have answers (forbid new commits when on a remote-tracking branch or tag, for instance, and spell out the full reference whenever necessary). But the Git designers apparently don't like that, and prefer the "detached HEAD" mode thing instead.
Upvotes: 4