Advanced

Handling git push

There are a number of patterns and recipes to cover elsewhere in this wiki, but I’m going to share this one here. You can use Cmd.io commands over SSH as Git remote endpoints to implement and react to git push from your repositories. Specifically, you can use one command for Git remotes: git-receive-pack.

You see, all that happens when you git push to an SSH remote like git@github.com:gliderlabs/cmd.git is it translates to the SSH command ssh git@github.com git-receive-pack 'gliderlabs/cmd.git'. This opens a session to github.com and runs the command git-receive-pack with the repo path argument. The actual git-receive-pack command then reads packed Git data over STDIN and applies it to the bare repository on the filesystem that was provided as an argument.

What Heroku and later gitreceive, Dokku, Flynn, etc all do in some form effectively is wrap git-receive-pack and install a pre-receive hook into the repository (perhaps created on the fly) that does some task. Git is designed to display the output of that hook back to the user during the push, so from that hook script you can git archive to get a tar of what was pushed and do whatever you want with it.

Currently, you can do this with Cmd.io by creating a command named git-receive-pack, which will handle all Git pushes to Cmd.io that authenticate with your username. Cmd.io is doing nothing special to make this work, but now you need a command that will properly handle the push.

Here’s an example to get you started. It involves three files: Dockerfile, git-receive, and pre-receive. Remember the last two need to have chmod +x run on them.

Dockerfile

FROM alpine:3.4
RUN apk add --update --no-cache git sed bash
COPY ./git-receive /bin/git-receive
COPY ./pre-receive /hooks/
ENTRYPOINT ["/bin/git-receive"]

Note that anything your pre-receive script is going to use also needs to be installed in this Dockerfile.

git-receive

#!/bin/bash
repo="$1"
if [[ "$repo" != /* ]]; then
  repo="/$repo"
fi
git init --quiet --bare "$repo"
cp /hooks/pre-receive $repo/hooks
git-shell -c "git-receive-pack '$repo'"

This normalizes the repository argument, creates a bare repo in that location, installs pre-receive as a hook for that repository, and performs the actual git-receive-pack. This will then trigger pre-receive.

pre-receive

#!/bin/bash

main() {
  # reads git push header data into variables
  read old new ref

  # use archive to tarpipe pushed branch files to a working directory
  git archive "$new" | (cd /tmp && tar -xpf -)

  # go to that directory
  cd /tmp

  # do something with the files!
  # exit non-zero and the push will fail.
}

delete-remote-prefix() {
  # this removes "remote: " that git prefixes hook output with client side
  sed -u "s/^/"$'\e[1G'"/"
}

main | delete-remote-prefix

This is just a template but from here you could deploy, do builds, run checks, or something more creative.

git remote

Once you’ve made a command from the above and installed it as git-receive-pack, you can add a remote to the repositories you want to push from like this:

$ git remote add cmd ssh://progrium@cmd.io/repo/path

The name of the remote can be anything you like, here it’s cmd. You can also drop the username if you were previously. The repo path can be anything as well, perhaps use it to determine what to do in your pre-receive script.

Unfortunately, git-receive-pack is not sharable. Perhaps in the near future there will be more integrated support in Cmd.io for this pattern.