1

I cloned a repo from github, made a new branch, pushed back up to github, then deleted the repo locally, and finally cloned again. Now I'd like to restore my branches back to how they were before I deleted the the repo locally, also along the way I'd like to learn more about what's going on with my situation and how it applies to the notions of HEAD, detached head state, remotes, and refs/heads.

I know the two branches of interest (mbigras and mcb-fix-type) are still there but they have been renamed:

atom-pane-manager git:master ❯ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
  remotes/origin/mbigras
  remotes/origin/mcb-fix-typo

So initially I thought I could just rename the remotes/origin/mbigras branch to mbigras and be done but I got the following error:

atom-pane-manager git:master ❯ git branch -a | grep mbigras | pbcopy
atom-pane-manager git:master ❯ git branch -m remotes/origin/mbigras mbigras
error: refname refs/heads/remotes/origin/mbigras not found
fatal: Branch rename failed

There's several things I'm curious about:

  1. Why can't I rename the branch?
  2. Why is refs/heads/ included on the branch name in the error message?

Also looking at the branches I'm also wondering:

  1. What do the remotes/origin/HEAD and remotes/origin/master branches mean?

I was poking around in .git trying to make heads or tails of the situation

atom-pane-manager git:master ❯ tree .git/refs
.git/refs
├── heads
│   └── master
├── remotes
│   └── origin
│       └── HEAD
└── tags

4 directories, 2 files

I was expecting to at least find remotes/origin/mbigras and remotes/origin/mcb-fix-typo in remotes but they're not, which is also confusing.

I also read another post that talks about "detached head state" I'm not sure if that's aplicable to my situation but the post did go over the git rev-parse command which might help shed some light:

atom-pane-manager git:master ❯ git rev-parse master
73b3c240ff9b35d760d8e4302308d3c0e725fc76
atom-pane-manager git:master ❯ git rev-parse HEAD
73b3c240ff9b35d760d8e4302308d3c0e725fc76
atom-pane-manager git:master ❯ git rev-parse remotes/origin/HEAD
73b3c240ff9b35d760d8e4302308d3c0e725fc76
atom-pane-manager git:master ❯ git rev-parse remotes/origin/master
73b3c240ff9b35d760d8e4302308d3c0e725fc76
atom-pane-manager git:master ❯ git rev-parse remotes/origin/mbigras
9a25755b0175ba2a3548b439ffdb12b8010e74a6
atom-pane-manager git:master ❯ git rev-parse remotes/origin/mcb-fix-typo
6d235be40c7223fda7a8a6dbdf8b2fd7336e3d00

From these results it looks like HEAD, master, remotes/origin/HEAD, and remotes/origin/master all point to the same place, which makes sense because they're pointing to the last commit:

atom-pane-manager git:master ❯ git log --pretty=oneline -n 1 | pbcopy
# => 73b3c240ff9b35d760d8e4302308d3c0e725fc76 Prepare 1.0.1 release

And the two mystery branches do exist ...somewhere.

So I'm confused about what the heck is going on, and specifically the three questions mentioned above. I'd especially love an answer to include a visual representation of what is happening. Thanks

Community
  • 1
  • 1
mbigras
  • 7,664
  • 11
  • 50
  • 111

1 Answers1

2

Why can't I rename the branch?

As we'll see below, you shouldn't rename remote branches and git-branch won't let you. But that's not what happened. As git said, it couldn't find it.

atom-pane-manager git:master ❯ git branch -m remotes/origin/mbigras mbigras
error: refname refs/heads/remotes/origin/mbigras not found
fatal: Branch rename failed
                                                 ^^^^^^^^^

git branch -a said remotes/origin/mbigras but the branch is really named origin/mbigras. As we'll see later, that's even an abbreviation.

The format of git branch -a is a bit confusing. git branch and git branch -r both show you the branch name. git branch -a shows you the names of the local branches, but to differentiate remotes it prepends remotes/ to them. For example...

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

What do the remotes/origin/HEAD and remotes/origin/master branches mean?

HEAD is the currently checked out commit. origin/HEAD is the currently checked out commit on the remote... or it was the last time Git looked. Git only talks to remotes when you fetch, push, pull and clone. The rest of the time it's using "remote tracking branches" which are stored in .git/remotes/<remote name>/<branch>. These record the state of the branches in the remote the last time it was looked at. This is part of what makes Git so blazingly fast, it's almost never talking to a network.

The arrow is letting you know that origin/HEAD is on the same commit as origin/master. This means the currently checked out branch on the remote is master. It usually indicates what the default branch is. A project using a different naming scheme might use trunk or development instead of the Git convention of master.

Why is refs/heads/ included on the branch name in the error message?

Local branches are stored in .git/refs/heads/ and remotes are stored in .git/refs/remotes/. They're just a file with a commit ID in them, git calls this a "symbolic reference".

$ cat .git/refs/heads/master 
177e49fff4348251ec30e3641d116accc0734c9d
$ cat .git/refs/remotes/origin/master 
177e49fff4348251ec30e3641d116accc0734c9d

That tells me master and origin/master point to the same commit.

master is actually the abbreviated name for a branch. The full name is refs/heads/master which is just the filename in .git/. Git will use them interchangeably. master is ambiguous, it could be a tag, it could be a branch, it could be a bunch of other things. So in an error message, Git will use the full name to be unambiguous.

When you tell Git you want to work with master it will do a search sort of like the PATH lookup your shell does when you try to run a command.

1. If $GIT_DIR/<refname> exists (HEAD, FETCH_HEAD, ORIG_HEAD, ...);

2. otherwise, refs/<refname> if it exists; (???)

3. otherwise, refs/tags/<refname> if it exists; (it's a tag)

4. otherwise, refs/heads/<refname> if it exists; (it's a local branch)

5. otherwise, refs/remotes/<refname> if it exists; (it's a remote branch)

6. otherwise, refs/remotes/<refname>/HEAD if it exists. (it's a remote name)

The gitrevisions docs explains all this in gory detail.


Knowing this, we can figure out another reason why you can't rename remotes/origin/mbigras: git branch isn't doing the full search, it's assuming that's a local branch.

error: refname refs/heads/remotes/origin/mbigras not found
               ^^^^^^^^^^^

Remote branches are in .git/refs/remotes/. git branch -m will never rename a remote because they're in a different directory. This is good! The remote tracking branches are supposed to be kept in sync with the remote repository, if you start renaming them things will get weird.

You could go into .git/refs/remotes and start renaming files and that would rename the remote. Don't do that because you don't actually want to rename the remote, you want to make a new local branch from the remote.

git branch origin/mbigras mbigras

That's it (you'll probably want to check mbigras out). This is roughly equivalent to:

cp .git/refs/remotes/origin/mbigras .git/refs/heads/mbigras

Plus some edits to .git/config to remember that mbigras is tracking origin/mbigras so git pull and git push know what remote and branch to talk to if you don't spell out git pull origin mbigras. That's origin mbigras, not origin/mbigras, because it's git pull <remote name> <remote branch name>.

(And then there's the exception for packed refs which we'll see below.)

Now you can work on your local branch mbigras and push and pull from the remote version of mbigras via it's tracking branch origin/mbigras which will be kept in sync for you, but only when you run push, pull, and fetch.

I was expecting to at least find remotes/origin/mbigras and remotes/origin/mcb-fix-typo in remotes but they're not, which is also confusing.

This is because of optimizations. What I described above is way Git works when things are unpacked. Git will use packfiles and packed references for optimization purposes I'm not qualified to explain but having to do with making cloning even more efficient.

Long story short, if a ref isn't in .git/refs/ it will be in .git/packed-refs.

$ cat .git/packed-refs 
# pack-refs with: peeled fully-peeled 
5f73dc320dbf320b6a6b497048dade6626d0c74b refs/remotes/origin/80_method_methods
260c0405871b7f92ed301041fef3f6c7ed90a5a5 refs/remotes/origin/appveyor
df99a98b7ba26bf5c4eb2a7fe3ec35bfe8090652 refs/remotes/origin/gh-pages
...

Similarly, sometimes an object won't be in .git/objects/, instead it will be in .git/pack/. The format of packfiles is well beyond my understanding.

With Git there's always something new! I've been using it for eight years and this is the first time I've encountered packed refs.

This might all seem a bit overwhelming, but having the guts of your version control system on display means you can fully understand it.

Schwern
  • 153,029
  • 25
  • 195
  • 336
  • Very informative! Only thing, when you mention that I need to copy my branch from remote (or should I say from origin? or is saying remote or from origin mean the same thing?) instead of renaming it, I think the command for that is: `git checkout -b mbigras origin/mbigras` and not `git branch origin/mbigras mbigras`. Am I understanding you correctly? – mbigras Jul 02 '16 at 20:48
  • 1
    @mbigras origin is the name of a specific remote, the default name for the remote you cloned from, just like master is the default branch. And you don't copy branches in git, that's an svn thing. A git branches is just a name pointing at a commit. A new branch is just a new name pointing at the same commit the original one is. From there they can diverge like a branch in a tree. So you'd say "make a branch from *the* remote" (most of the time there's only one remote) or "make a branch from origin". `git checkout -b bar foo` is shorthand for `git branch foo bar` plus `git checkout bar`. – Schwern Jul 02 '16 at 21:29