Search code examples
gitgit-post-receivegit-bare

Bare git repo post-receive hook no longer able to checkout


I have the common setup of a bare git repo on a web server using a post-receive hook to automatically checkout changes to a web site. This has been working great.

However, a coworker recently cloned the bare repo (git clone <ssh url>), made some changes on their local repo, committed them, and then pushed the changes back (git push origin master). I'm not sure if this process is what caused the issue, but since the push, the post-receive hook no longer works. It fails with the output:

fatal: You are on a branch yet to be born

Here is the hook:

#!/bin/bash
GIT_WORK_TREE=/home/marweldc/app git checkout -f

I am able to pull the changes my coworker made from the remote to my local instance. Running git branch inside the bare repo dir on the server shows * master, and git log shows all the changes including my coworker's, so the remote repo is obviously tracking things fine.

However, if I do, say, a GIT_WORK_TREE=/home/marweldc/app git status I get the following output:

# Not currently on any branch.
#
# Initial commit
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#       ../CONTRIBUTING.md
#       ../README.inno.txt
#       <all the other files and folders that should be in the repo>
nothing added to commit but untracked files present (use "git add" to track)

I've also tried running GIT_WORK_TREE=/home/marweldc/app git checkout -f master, which yields:

error: pathspec '.git/master' did not match any file(s) known to git.

We're stumped. What has changed that is causing our hook to fail in this way when it was working fine before?

Edit:

Some things I've tried re comments below:

  • cat HEAD from inside the bare repo yields ref: refs/heads/master
  • GIT_TRACE=1 git rev-parse master from within the bare repo yields

    trace: built-in: git 'rev-parse' 'master' 
    f6462b06f75d126ab932e3cfccef7385da2805ba 
    

    but the same command with the --work-tree option set yields

    trace: built-in: git 'rev-parse' 'master'
    master
    fatal: ambiguous argument 'master': unknown revision or path not in the working tree.
    Use '--' to separate paths from revisions
    
  • Running GIT_WORK_TREE=/home/marweldc/app-new git checkout -f (a fresh directory to checkout to) works fine

Solution

  • Edit: there's some very specific (and perhaps CentOS-specific) problem with separate Git and work-tree directories in the very old Git version 1.7.1 in use here, such that for this one specific case, --work-tree=/home/marweldc/app causes Git to be unable to travel back and forth between the Git directory and the work-tree. (Other work-tree paths do not cause the problem.)

    Git fails to notice its own failure to get back to the bare repository, and then is unable to do anything with branch names (since it could not get back to the repository it is likely to fail at everything at this point).

    A more modern Git probably does not have the bug in the first place, or would notice the failure to switch back and forth between work-tree and bare repository.

    Meanwhile, specifying --git-dir=<path> seems to work around the problem.

    Original (general) answer is below.


    This:

    fatal: You are on a branch yet to be born

    means just what it says, although what it says may be confusing. :-) You really are "on" some branch that does not yet exist.

    (It is not clear which branch you are on—you may have a "detached HEAD", as Git calls it—but it does seem as though you no longer have a master branch, if you ever did.)

    Short version

    Use git branch to see what branch you are on now, and what branches you have available. Use git symbolic-ref HEAD refs/heads/master to forcibly set the bare repository's HEAD back to the master branch. For some other branch named B, use refs/heads/B.

    Long-form answer, i.e., what's going on, follows.


    Whether or not a repository is bare, it still has a current branch

    In any ordinary, non-bare repository, you can see what branch you are on by running git status. But git status looks at the work-tree, so in a bare repository, git status refuses to run: there is no work-tree.

    Another way to see what branch you are on is to run git branch:

    $ git branch
      diff-merge-base
    * master
      stash-exp
    

    This works in both bare and non-bare repositories. The * goes by the name of the current branch.

    How, in a regular repository, do you change which branch you're on?

    The answer1 is: git checkout. For instance, git checkout master puts you (or me) on master, and git checkout stash-exp puts you (or me) on stash-exp.


    1There is one other way, using a plumbing command, but you don't normally want to use that as it can mess with the difference between current commit, index, and work-tree. But since a bare repository has no work-tree, this becomes ... well, "safe" is too strong, but we can say "much less dangerous".


    How, in a bare repository, do you change which branch you're on?

    The answer is still git checkout.

    When you check out a branch in a bare repository, this uses, and changes, your current branch in exactly the same way as when you check out a branch in a non-bare repository.

    Of course, you can't git checkout a branch in a bare repository, because that updates the work-tree. Except, you can, when you use git --work-tree=... checkout .... You supply the name of the branch to check out, and that writes the new branch name into HEAD as usual. If you check out a commit by hash ID, that detaches HEAD as usual.

    Unborn (orphan) branches

    But there's one more special case: if you set HEAD to the name of a branch that does not yet exist (as you would with git checkout --orphan for instance), that puts the name into HEAD without creating the branch. That's normal enough in a regular (non-bare) repository, except that you only see it in two cases:

    • When you first create a new, empty, repository. You're on master, but master does not yet exist. You resolve this situation by putting a commit in the repository, which goes onto master, which creates master. (The new commit is a root commit, i.e., has no parent commit.)

    • When you use git checkout --orphan: this sets you up in the same way as when you're in a new, empty, repository, by writing the name of the branch into HEAD as usual, but without creating the branch. You resolve this situation by writing a new commit, which goes onto the unborn branch, which creates the branch, which is now born (exists, pointing to the new root commit you just made.)

    Doing the same sort of thing in a bare repository

    Obviously, you can't do this in the usual way in a bare repository, because the usual way is to work with the work-tree, git add things into the index, and git commit. A bare repository has no work-tree. (It does still have the index, and git checkout, which you can do when you supply a work tree with --work-tree, still writes through the index. This is kind of tricky and messes with many people's post-receive deployment hooks, since they don't understand the index.)

    What you can, and do all the time, do in a bare repository is receive git pushes. When you receive a push, you accept requests (or commands) from another Git of the form:

    • Please set your master branch to commit 1234567
    • Delete branch testing

    (It's up to you to decide whether and how to check these requests, in a pre-receive or update hook. If you do nothing special, you get Git's default built-in checks, which allow anyone to create or delete any branch or tag, but only allow fast-forward pushes for branches.)

    Suppose, for instance, that are, in your bare repository, on branch master, and branch master does not yet exist. Then someone out there on the Interwebs connects to your server and says: "hey, you, bare Git repository, have some commits here, and now create branch master pointing to commit 1234567!" In this case, assuming you accept this request / command, once your Git is done serving them, you do have a master now. This takes you from:

    fatal: You are on a branch yet to be born
    

    to:

    On branch master
    

    because your current branch master, which did not exist before, does exist now.

    Using the plumbing commands

    The git update-ref and git symbolic-ref plumbing commands are meant to be used in scripts. They assume you know exactly what you are doing. (If you use them without knowing what you are doing, you can make a mess: they have no built in safety checks.) The second of these, in particular, is how Git internally sets HEAD to select which branch you are "on".

    If you run:

    git symbolic-ref HEAD refs/heads/branch

    this writes ref: refs/heads/branch into HEAD, and you are now On branch branch. It does not update the index and it does not update the work-tree—but in a bare repository, there is no work-tree, and receiving a push does not update the index, so this is pretty similar to what happens when you receive a push.

    Thus, this is a reasonably safe thing to do. It lets you change which branch you are on, without having to use git checkout. This is the normal and correct, albeit fiddly—make sure you spell the branch name correctly, and include the refs/heads/ prefix—way to set HEAD in a bare repository.