Bare Git repositories

Posted on 1 July 2010 in Programming

We started a new project at Resolver today -- I'm pretty excited about it, and will be blogging about it soon. However, in the meantime, here's something that's half a note-to-self and half something to help people googling for help with Git problems.

We've previously been using Subversion as our main source code control system, but for more recent projects we've moved to Mercurial. When we started the new one today, we decided to try out Git for a change; I use GitHub for my personal stuff, but hadn't used it for anything involving multiple developers -- and various people had been telling us that it wasn't subject to some of the problems we'd had with Mercurial.

So we created a new Git repo on a shared directory, by creating a directory and then running git init in it. We then cloned it into a working directory on my machine, and started work. After a while, we had our first checkin ready, so we a dded the files, committed them, and then decided to push to the central repo to make sure everything worked OK. We got this error message:

remote: error: refusing to update checked out branch: refs/heads/master
remote: error: By default, updating the current branch in a non-bare repository
remote: error: is denied, because it will make the index and work tree inconsistent
remote: error: with what you pushed, and will require 'git reset --hard' to match
remote: error: the work tree to HEAD.
remote: error:
remote: error: You can set 'receive.denyCurrentBranch' configuration variable to
remote: error: 'ignore' or 'warn' in the remote repository to allow pushing into
remote: error: its current branch; however, this is not recommended unless you
remote: error: arranged to update its work tree to match what you pushed in some
remote: error: other way.
remote: error:
remote: error: To squelch this message and still keep the default behaviour, set
remote: error: 'receive.denyCurrentBranch' configuration variable to 'refuse'.

It took us a while to work out precisely what this meant, because we'd never heard of "bare" repositories before. It turns out that there are two kinds of repository in Git: bare and non-bare. A non-bare repository is the same as the ones we were used to in Mercurial; it has a bunch of working files, and a directory containing the version control information. A bare repository, by contrast, just contains the version control information -- no working files.

Now, you can (in theory) push and pull between repositories regardless of whether they are bare or not. But if you were to push to a non-bare repository, it would cause problems. Part of the SCC data that Git keeps is an index, which basically tells it what the head of the current branch looks like. Now, if you push to a non-bare repository, Git will look at the working files, compare them to the index, and see that they differ -- so it will think that the working files have changed! For example, if your push added a new file, it would notice that the working directory didn't include that file, and would conclude that it had been deleted. There's a step-by-step example here.

You can see how that could be confusing. So bare repositories exist as a way of having central repositories that a number of people can push to. If you want to transfer changes from a non-bare repository to another, the correct way is to pull from the destination rather than push from the target -- which makes some kind of sense when you think about it. In general, any repository that someone is working on is not something that should be receiving changes without their approval... on the other hand, we've not encountered problems with pushing to regular repositories with Mercurial.

Anyway, this was our first checkin, so we had no history to lose, we fixed the problem by creating a new central repository using git --bare init in a new directory on the shared drive, cloning it to a new working repo, copying our files over from the old working repo to the new one, committing, and pushing back to the bare repository. It worked just fine. If we'd done multiple checkins before we tried our first push, we could have saved things by hand-editing the central repository; it had no working files (because we'd only just created it) so we could have moved the contents of the .git directory up to the repository's root, and deleted .git -- this would have "bared" it so that we could have pushed from our working repo. That would have been a bit scary, though.