Search code examples
gitsaasgit-post-receivegit-worktree

git post-receive for multiple worktrees on the same server


I am following this tutorial and it has been working really well. We now want to run several accounts on the server with the same package and I am trying to modify post-receive but I am not doing something right and cannot find the answer. The goal is to have separate cpanel type accounts running the same code from our github repo, with updates happening to all accounts whenever we push changes.

The original code in post-receive is this:

#!/bin/bash 
git --work-tree=/path/to/firstInstance --git-dir=/var/control/project.git checkout -f

and it works as expected, whenever we push from our local repo and update github our remote server is also updated to /var/control/project.git which then gets pushed to the first account.

When I add another line for a different work-tree, so there are 3 lines which read:

#!/bin/bash
git --work-tree=/path/to/firstInstance --git-dir=/var/control/project.git checkout -f
git --work-tree=/path/to/secondInstance --git-dir=/var/control/project.git checkout -f

I can add new files to both instances but deleting only happens on the second instance. Clearly I am not doing it right but I cannot find what I am looking for here or elsewhere online. Any help would be greatly appreciated.


Solution

  • You are (presumably1) using a bare repository, which is the right thing to do, but you're hitting a snag that is only obvious after you have become a Git Guru. 😀 The trick is that you need one index per work-tree.2 We'll get to defining all these terms in a moment, but the TL;DR part is to use this somewhat magic sequence:

    GIT_INDEX_FILE=index.firstInstance git --work-tree=/path/to/firstInstance --git-dir=/var/control/project.git checkout -f
    GIT_INDEX_FILE=index.secondInstance git --work-tree=/path/to/secondInstance --git-dir=/var/control/project.git checkout -f
    

    This can definitely be simplified; to see how, read on.


    1The instructions in your link say to use git init --bare, so I assume you are doing that.

    2There are other tricks that will work, but I'm going with this one.


    What's going on here

    A normal Git clone consists of three parts, as it were:

    • the repository proper, which itself consists of two main databases and many auxiliary databases; and
    • an index and a work-tree, which are paired together.

    Using one of these normal repositories, the git worktree add command can add more work-trees. Each one has an index-and-work-tree pair. They are bound together (somewhat loosely, but enough to always treat them as a pair).

    A bare clone omits the work-tree without omitting the index. This means that your bare repository has a free index: the work-tree that it would be bound to doesn't exist. You can therefore run git --work-tree=path command argument1 argument2 .... This assigns, temporarily, a work-tree to this bare repository. The (single) index and this work-tree are now bound together. The one command you supply is run, using the standard index and that work-tree.

    The problem is that that one standard index now describes that work-tree, and not any other work-tree. If you run git --work-tree=otherpath command ..., you invoke Git with its standard index and that other path bound together. Git assumes that the index correctly describes this other work-tree. When it doesn't, things sometimes fail. More precisely, the index keeps track of what's in the work-tree, and Git partly just assumes that it's correct.3


    3Git does some checking. The index has in it cached data about that work-tree, and Git will verify that at least some of that data remain correct. Exactly how much data it trusts and how much it doesn't trust depend on what it sees as it does these checks. This means the effect, when the cache data don't describe the work-tree correctly, is very difficult to predict. Sometimes everything works! But sometimes it doesn't. When it fails, it fails unpredictably and is very hard to debug.


    What we do about it

    We make use of the fact that Git is capable of using more than one index. As we noted above, if you add extra work-trees to a standard (non-bare) repository, each added work-tree gets its own index.4 But when we use --work-tree or GIT_WORK_TREE to override the standard work-tree, or supply one for an otherwise bare repository, we have not overridden the standard index for the standard work-tree. Using the GIT_INDEX_FILE environment variable allows us to override the standard index (whose name is just index).

    We need to do this for all but one "extra" work-tree. One of the work-trees we use with the bare repository can use the standard index, so we really only need one GIT_INDEX_FILE environment variable assignment on one of the two git commands here. But for symmetry, or the ability to add a third checkout, we can just override the standard index each time.

    Note that the --git-dir option (or GIT_DIR environment variable) is not required if the current working directory of the bash script is already the bare repository directory, so in many cases you could omit the --git-dir=. To simplify the script, you could run:

    cd /var/control/project.git
    

    at the front of the script and omit each of the --git-dir= options.

    Note also that these git checkout -f commands do not supply a commit hash ID or branch name, so they always check out whatever commit is identified by the special name HEAD.


    4These added work-trees get their own HEAD reference and others as well. We aren't taking care of this issue here. Whether and when that is a problem is another topic.