Home > database >  git checkout branch shows wrong state
git checkout branch shows wrong state

Time:10-16

git branch -a shows me

* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/feature
  remotes/origin/master
  remotes/origin/origin/feature

I want to checkout the last branch remotes/origin/origin/feature.

I don't really know why there is remotes/origin/feature and remotes/origin/origin/feature they were created by someone else.

The structure looks like this

         --> F --> G        origin/feature
       /
A --> B --> C --> D --> E   master

Now I want to merge remotes/origin/origin/feature into master.

Strangley, If I want to checkout the feature branch using

git checkout 'origin/feature'

all files are at the state from point B and not at point G. I triggered the merge via the gitlab GUI and it told me I have to use

git checkout -b 'origin/feature' 'origin/feature'

This worked, now all files are at the stage of G. Why is this so? Why do I have to use the -b option even if the branch already exists and why do I have to enter it twice?

CodePudding user response:

First, do not use git checkout, which is obsolete and replaced with git switch. Or at least use git checkout origin/feature -- to force git checkout to consider origin/feature as a branch name, not a file, as I mentioned here.
(Hence git switch, which only deals with branches, not branches or files, like git checkout does)

Second:

Why do I have to use the -b option even if the branch already exists and why do I have to enter it twice

Because the branch exists only as a remote tracking branch (a memento of what was last fetched from the remote named origin)

It is best to not include origin/xxx in the name of the branch, because it is confusing since the default remote repository is itself named origin.

A simple git switch origin/feature is enough, because it has a guess mode:

If <branch> is not found but there does exist a tracking branch in exactly one remote (call it <remote>) with a matching name, treat as equivalent to

$ git switch -c <branch> --track <remote>/<branch>

CodePudding user response:

Besides VonC's advice (which is good: use git switch to avoid some of the confusion), it's worth noting that remotes/origin/feature and remotes/origin/origin/feature are not actually branch names. Git's terminology here is awful: it is confusing and not always consistent.

To make sense of this, you need to know several things about Git:

  • It's actually the commits that matter. A name—whether it is a branch name, or a tag name, or some other kind of name entirely—exists so that you and Git can find the hash ID of the commit. The hash ID, that big ugly string of letters and digits like d420dda0576340909c3faff364cfbd1485f70376, is the true name of the commit, but humans are bad at hash IDs. We like simple names like main or next or v2.37.0 or whatever.

  • So, Git provides names. These come in a long list of flavors. Branch names are just one of those flavors.

  • You can have a branch xyzzy and a tag xyzzy. You probably shouldn't—it's very confusing when you do this—but you can do it. So Git needs to know how to tell a branch name from a tag name, for instance.

  • The method Git uses to know this is to give everything a "full name" (like a person's full name: "J Robert Oppenheimer"), and to use a shortened name for more familiar usage (e.g., "Bob").

To categorize the names, Git sticks them in a namespace: all branch names start with refs/heads/, and then the rest of the string is the branch name, so master is actually short for refs/heads/master. All tag names start with refs/tags/, so if you were to create a tag named master, it would be refs/tags/master. Don't do this: Git can keep them straight internally by sticking the prefixes back on, but you probably won't.

Now, one particular, and slightly weird, name-flavor is the remote-tracking name.1 A remote-tracking name is a name (of course) in your own Git repository, and it actually starts with refs/remotes/. Git leaves out the refs/, and sometimes also the remotes/ part as well:

$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/feature
  remotes/origin/master
  remotes/origin/origin/feature

Had you run git branch -r, Git would not have listed * master at all, and would have taken remotes/ off the remaining names, leaving:

origin/HEAD -> origin/master
origin/feature
origin/master
origin/origin/feature

Again, these are all remote-tracking names. They really are spelled "refs/remotes/origin/whatever". Git is just shortening them by leaving off "refs/" or "refs/remotes/", and you can do that too.


1Git has, over the years, not been entirely consistent in what Git calls these, but modern Git tries to use the phrase "remote-tracking branch name" pretty consistently now. I don't like the insertion of "branch" here: I think it's actually less confusing to leave it out. So that's what I do, but know that Git sticks it in. Pay particular attention to the "remote-tracking" part, as that's the real clue.


Remote-tracking names and where they come from

Every Git repository is independent of all other Git repositories, even if they're linked and yours is a clone of some other existing repository. So every Git repository has its own branch names. You have your master, Joe has Joe's master, Sally has Sally's master, Ramesh has Ramesh's master, and so on.

When you cloned the repository you're using, they had a branch named master too. Your Git refers to that repository as origin. So your Git took their name master—technically, refs/heads/master—and changed it to origin/master, or technically refs/remotes/origin/master. So their master, a branch name, became your origin/master, a remote-tracking name.

Your Git software did that for every branch name that their Git software said that they have in their repository. So that tells us that, given the list of origin/* names that git branch -a showed, they have HEAD, feature, master, and—here's the weird one—origin/feature. That's a branch name in their Git repository, whose full name is refs/heads/origin/master. Their Git software and your Git software always use the full name internally so that they don't get confused about this. But your Git software shows you your abbreviated names. Your Git created a refs/remotes/origin/origin/feature to match their branch named origin/feature, and your Git now shows that as either origin/origin/feature (after taking off refs/remotes/) or remotes/origin/origin/feature (after taking off only refs/).

Checkout vs switch

The git checkout command lets you use both branch names and remote-tracking names. If you put something in that's ambiguous, git checkout has rules by which it figures out what you mean. Confusingly, both git checkout and git switch have modes that will create a new branch name, and this interacts with the "figuring out" rules in weird and wonderful (or terrible) ways. Because git switch has simpler rules, the interactions are not as wacky. Unfortunately for you, someone else goofed up and set up a situation where you get to experience at least a little bit of wackiness no matter what you do.

The rules for git checkout go roughly like this (although they're actually considerably more complicated):

  1. Try something as a branch name: do we have a branch with that name? If so, use that as our existing branch name.
  2. Try something as, in order, a tag name, a remote-tracking name, or a general-purpose name. Do we have that name as that kind of name? If so, use that as a non-branch name.
  3. If none of those worked and --guess mode is in effect, proceed into the "guess and create new branch if possible" code. (This used to be called "DWIM mode", and still is in older Git documentation.)

The middle rule here, step 2, allows you to check out any specific commit as a detached HEAD. The git switch command won't do that unless you add --detach to the arguments.

Because origin/feature is a remote-tracking name in your repository, your git checkout takes it up if and when it hits step 2 (but not if it finds origin/feature as a branch name in your repository in step 1). But origin/origin/feature is also a remote-tracking name in your repository, so you could git checkout origin/origin/feature and perhaps get that in step 2.

Once you create a branch named origin/feature, git checkout takes that up in step 1.

Because this kind of thing does happen, it's a bad idea for someone to create a branch named origin/whatever. It's also a bad idea to have a remote-tracking name refs/remotes/origin/origin/feature, but you don't control this directly: you can only ask or command the Git repository over at origin to use a different name, such as refs/heads/feature2.

Fixing the blame and/or the problem

Whoever created the name origin/feature as a branch name over on origin is the person at fault. To fix the problem, though, you just need to rename the branch over on origin. This may be easy—perhaps there's a web interface to do that, for instance—or it may be a bit tricky.

Once the branch on origin has been renamed (to feature2 or some more meaningful name), run:

git fetch --prune

to have your Git software call up their Git software, see their new list of branch names, and delete your refs/remotes/origin/origin/feature name while also creating the new remote-tracking name based on the new branch name.

  •  Tags:  
  • git
  • Related