Saturday, May 02, 2015

A Better Way to Git Push to Deploy (updateInstead & push-to-checkout)

Wow, nearly a year since my last post. I was sort of thinking it would be something more profound, but here goes: Git recently (with version 2.3) introduced a way of easily pushing changes into a remote non-bare repository, a.k.a. push-to-deploy. The old way would be to have a post-receive hook run some update logic which would do some procedure to update a non-bare repository. There now is a simple way of configuring the target repository to update its work tree instead upon being pushed to.

Now, pushing to the target repo may fail in cases where it has been modified, so soon after, a new push-to-checkout hook was introduced to deal with this, but it will only take effect in Git 2.4. I'll show how to set up both below.

Surprisingly, when searching for "push-to-checkout", I found very few articles about this, even though it was loudly mentioned on the GitHub blog (twice, and on StackOverflow, of course). So here's another one for the googles. Besides, I just wrote this whole example on the Git Users Mailing list and figured I'd repeat it here.

Note that as it happens, I've never found myself in the sorry position of having to deploy this way in real life (real developers package their distributables), so don't take my word this being the way to do it on production systems.

Note that Git version must be >= 2.3 for this to work at all, and must be 2.4 in order for the push-to-checkout hook to take effect.
[master][~/temp/foo-web]$ git --version 
git version 2.3.6 

Take some random repo and clone it to a "remote" location:
[~/temp]$ git clone foo foo-web 
Cloning into 'foo-web'...done.

So, let's pretend that this non-bare repository is on our web-host:
[~/temp]$ cd foo-web

Configure to update on incoming pushes:
[master][~/temp/foo-web]$ git config receive.denyCurrentBranch updateInstead

Create the hook:
[master][~/temp/foo-web]$ vim .git/hooks/push-to-checkout

Contents of the push-to-checkout hook, as exemplified in the test here:

/bin/sh
echo >&2 updating from $(git rev-parse HEAD)
echo >&2 updating to "$1"

git update-index -q --refresh && git read-tree -u -m HEAD "$1" || {
    status=$?
    echo >&2 read-tree failed
    exit $status
}

Make the hook executable
[master][~/temp/foo-web]$ chmod +x .git/hooks/push-to-checkout

Now let's take it for a spin. Go back to the original repo:
[master][~/temp/foo-web]$ cd .. 
[~/temp]$ cd foo 
[master][~/temp/foo]$ git remote add web ../foo-web 
[master][~/temp/foo]$ git push web master 
Everything up-to-date

OK, that wasn't very impressive. Make some changes first, a new file heya.txt for example:
[master][~/temp/foo]$ echo `random_word` > heya.txt; git add .;git commit -m `random_word` 
[master b6765e2] overpaint 1 file changed, 1 insertion(+) create mode 100644 heya.txt

Away we go:
[master][~/temp/foo]$ git push web master 
Counting objects: 3, done. 
Delta compression using up to 4 threads. 
Compressing objects: 100% (2/2), done. 
Writing objects: 100% (3/3), 322 bytes | 0 bytes/s, done. 
Total 3 (delta 0), reused 0 (delta 0) 
To ../foo-web 
   eb2711a..b6765e2  master -> master 
Push succeeded! Now, let's see if our new file arrived OK:
[master][~/temp/foo]$ cd .. 
[~/temp]$ cd foo-web

Tada!
[master][~/temp/foo-web]$ ls 
README   foo      heya.txt