Managing multiple working copies with git

07/22/2010

We have a CMS at Buffalo, which we have deployed to several servers for a few of our clients. Until recently, it was only two separate servers and was relatively easy to manage with Subversion (though slightly cumbersome - svn ci -m "blah"; ssh server; cd /site; svn up; exit; ssh server2; cd /site; svn up;) but now that we're deploying the same CMS to 8 servers, simple changes and bug fixes are becoming a pain in the ass to deploy.

I've been tentatively looking into git for quite some time now. I'm incredibly cautious of the bleeding edge when it comes to how I make money because I prefer to have things that I can rely on and will serve me well than keep up with the latest fads. That's not to say that I'm not interested in all the cool new jazz, and I keep up as much as family life permits, but I don't dive in and adopt without learning and understanding everything I need first. Sensible? Yes. I'm surprised at how many people flaunt this ethos.

That being said, git has proved itself invaluable to me in the last 4 months. The way it encourages you to work is great. I do all changes on branches then merge them into master and remove branches to keep everything clean. I also have $deploy-staging and $deploy-live (where $deploy is a working copy) so that I can manage configuration for each working copy. This probably isn't git best practice, but I've found it to be incredibly convenient. I work on up to 20 different sites in any week, so being able to merge changes and conflicts for live stuff locally saves me headaches galore. No-brainer. My git workflow goes something like this:

git checkout -b hotfix-phperror
# do some work
git commit -am "Hotfix for PHP Error fixed"
git checkout master
git merge hotfix-phperror
# resolve any conflicts
git checkout staging
git merge master
# test on staging - everything OK
git checkout live
git merge master

That might seem like quite a bit of work, but it keeps everything tidy. Unfortunately, it does mean that if things fail on staging, I still have the history of it in master. I suppose it would make more sense to merge the hotfix into staging, then merge that into live, then merge everything into master and tag once it's verified working to keep it clean, but either way, this tip will work.

I'm under the impression that git frowns upon what I'm about to recommend, but it works so well that I can hardly ignore it! One proviso for this is that all the working copies you push to have to be the same. Lucky for us, the configuration for our CMS is held by the live site (separate repository) rather than the CMS itself, so each working copy is the same.

With the above in mind, I'm going to assume the following:

• You've got a central, bare repository to push to • Your live branch (identical for all working copies) is tracked from said central repository

First off, you have to make sure that your local working copy has all remotes available. Obviously your bare repository will be available because that's how you're doing things anyway, but we also need working copies. You can check that everything's there by running a quick git remote. If you're all good, then we can start pushing code around.

As I said before, git doesn't seem to like this sort of thing. If you were to try and push to one of your working copies where the checked out branch is the one you're pushing to, git will whine at you and preserve the changes you push so that you can stash any local differences. You can force this by changing to the directory and running git reset --hard or git stash, but that defeats the purpose of this so we need a workaround.

Luckily, git gives you all the hooks you need and more for this. We'll be creating a post-update hook that will force-update your push. With that in mind, cd to the root of your working copy on one of your remotes and do something like:

#!/bin/sh
cd ..
env -i git reset --hard

In .git/hooks/post-update and chmod it executable.

Now, when you push to the repository git will moan at you, but this hook will run and force all your changes. Awesome.

In the warning message git spits out, it threatens that new versions of git will auto-reject pushes to checked out branches, so a quick run of:

git config --set receive.denyCurrentBranch "ignore"

Will shut git up and have it doing what you tell it to. I suppose the reason it does this is it assumes that someone is working in that working copy and they don't want you frivolously overwriting their code. Luckily, we know what we're doing is safe so there's no harm in doing this. Do remember, though, that all changes made in the working copy will be overwritten when you push using this method, so don't work on live working copies. Don't do that anyway, but definitely don't do it here!

Now, you can do the following:

git remote add remote-alias ssh://root@blah/path/to/working/copy
git fetch
git push remote-alias live

In all the feedback, you'll see something like "HEAD is now at 0d5431b HELLO!!!" (the hash and first line of your last commit message) which lets you know that things have worked.

Now you're able to push to remotes without complaints, from the comfort of your local working copy, with a bit of scripting you can deploy your local live branch to all its remote locations with a little bit of scripting:

for remote in `git remote`; do git push $remote branchname; done;

For future reference, we'll assume that this isn't the only time you'll do this, so save the following as a script in your path somewhere (I call it git-rpush):

for remote in `git remote`; do git push $remote $@; done;

Which you can call with git rpush live. Assuming that all your working copies have the config set and the hook installed, you can now push a change to all of your local repositories at once without having to remember which ones you actually need to push to, then spending half an hour SSH-ing all over the place to do it.

I don't know about you, but that's just saved me a crap-load of time. Hope it helps someone!

Coincidentally, if you do need this to be locally configurable, you can easily check out your branch and create a patch for configuration, then add the patch removal and application into hooks. I would do something like the following (assuming you already have your patch):

#!/bin/sh
cd ..
env -i git apply -R config.patch

In .git/hooks/pre-update and chmod executable

This will remove your config so that no conflicts occur during the update. All you need is to re-apply the patch in the post-update, after you've done the reset. Insert the following:

git apply config.patch

Your config will be preserved. You can even keep these patches in version control, just change the name for all your remotes and update your hooks accordingly. Then enter my competition on how easy you can make your life by scripting it!

Any and all questions and improvements are welcome and appreciated.

I would like to thank the following post for hand-holding through the intricacies of what I was trying to achieve with hooks. So thanks!