1

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
ralbatross
  • 2,448
  • 4
  • 25
  • 45

1 Answers1

3

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.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Thanks for the detailed reply, but I think I'm still missing something. The bare repo is on the master branch (`git branch` yields `* master`), and that branch DID exist previously (I have been pushing to the bare repo, and the post-receive hook has been working fine for months, until now; furthermore, `cat HEAD` on the bare repo yields `ref: refs/heads/master`). However, the repo seems to think it's not on a branch when doing something with a work tree. So, it's still not clear to me what git incantation I need to make in order to fix our situation. – ralbatross Apr 25 '17 at 17:41
  • Interesting. If you're on the server and you run `git rev-parse master` in the bare repository, does it come up with a hash ID? If that works, try the same with `git --work-tree=... rev-parse master` (or the same using the env variable—these work exactly the same internally); it should still work. In which case, why is the `git checkout` failing? – torek Apr 25 '17 at 18:38
  • `git rev-parse master` yields a hash ID, but running with the --work-tree flag yeilds `fatal: ambiguous argument 'master': unknown revision or path not in the working tree.` (And yes, the working tree path exists.) – ralbatross Apr 25 '17 at 19:20
  • Wow. That is *not* supposed to happen. As an experiment, try `GIT_TRACE=1 git --work-tree=... rev-parse master` vs the same without the `--work-tree`. They should do exactly the same thing, e.g., print `13:31:40.410988 git.c:371 trace: built-in: git 'rev-parse' 'master'` (and then the hash; the time stamps will change of course). – torek Apr 25 '17 at 20:32
  • Similar result: without setting the work-tree, I get the expected result; when I set the work-tree I get `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` – ralbatross Apr 26 '17 at 11:10
  • Another interesting tidbit: running `git --work-tree=... checkout -f` on an empty temp directory that I set up seems to work fine... – ralbatross Apr 26 '17 at 12:19
  • Wow (again). One more possible experiment, what happens if you run `git --work-tree=... --git-dir= rev-parse master` (and the same with `git branch`, etc)? In any case, it looks like your Git is veering off into never-never land when used with that particular `--work-tree`, and that's downright mysterious. I tried pointing my own Git at a work-tree that had a Git repo in it and that did not bother it, it still used the bare repo. – torek Apr 26 '17 at 16:21
  • The non-work-tree command works, but when I try to include the `--work-tree` option I get `fatal: Could not jump back into original cwd: No such file or directory`. (And I can cd into the work-tree path no problem, and see files there...so weird) – ralbatross Apr 26 '17 at 19:13
  • Hm, that "could not jump back into original cwd" is clearly tied into the problem. I don't know why Git would not be able to "jump back" (some sort of bizarre permissions issue, probably) but it's failing to get back to the bare repo, which then makes it fail to be able to parse `master`. You've exposed some other bug (Git should have noticed the failure earlier, rather than just saying: duuurrr can't find master...), but, see if you can figure out why Git can't get back to point A once it moves to point B. – torek Apr 26 '17 at 19:38
  • BTW which version of Git is this? I don't see `Could not jump back` in any of the source files for Git 2.12+. – torek Apr 26 '17 at 19:44
  • Ah, found it: the string changed slightly way back in Git 1.7.10. You must be using an even older Git. You are probably on CentOS (as far as I know only CentOS users are stuck with ancient 1.7.1 Git). – torek Apr 26 '17 at 19:52
  • You're right, I'm running git 1.7.1, and I do believe the server is running CentOS – ralbatross Apr 26 '17 at 20:26
  • Oh gosh, I've been running these commands from inside the bare repo's root directory. When I run from outside, and specify --git-dir it works :P I've already marked your answer as correct, but probably worth adding a note to your answer to reflect this finding. – ralbatross Apr 26 '17 at 20:37
  • I added a front section to the answer, describing all this. – torek Apr 26 '17 at 20:49
  • I use in my `hook` `git --git-dir=/foo/tik.git --work-tree=/home/foo/tik checkout master -f` - does git create the work tree if it does not exist? I will try of course, but wonder if generally `git` creates dirs along its commands or not. – Timo May 15 '21 at 10:31
  • 1
    @Timo: when I tested this (probably about Git 1.8 or so), Git wouldn't `mkdir` a work-tree from nothing, but would `mkdir` subdirectories inside an empty work-tree. I expect that particular bit of code might not be stable from release to release though. It's not clear whether Git should create a work-tree just because you named it, but for internal purposes it might be convenient, so Git might go back and forth between different routines for different reasons, from version to version. – torek May 15 '21 at 18:03