├── .gitignore ├── .travis.yml ├── Makefile ├── README.md ├── gitreceive ├── package.json └── tests ├── Dockerfile └── gitreceive.bats /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | branches: 5 | only: 6 | - master 7 | before_install: 8 | - sudo apt-get install -y rsync 9 | - echo -n $travisci_{1..30} >> ~/.ssh/id_rsa_base64 10 | - base64 --decode --ignore-garbage ~/.ssh/id_rsa_base64 > ~/.ssh/id_rsa 11 | - chmod 600 ~/.ssh/id_rsa 12 | - echo -e "Host ci.progrium.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config 13 | script: 14 | - rsync -avz --exclude ".git" -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" . gitreceive@ci.progrium.com:. 15 | - ssh gitreceive@ci.progrium.com make test 16 | 17 | #notifications: 18 | # irc: "chat.freenode.net#dokku" 19 | env: 20 | global: 21 | - secure: "CnUscGcLAp70E0Ok8mYjvRwbixG7SXA0wSqWCUH4L7bWr+anqtARFyIrPJXX\nTA0IfjqwhrJ72ToD3srmBKt+cp1vWaOnVbyUoGviMg2dLRayJxF/24/F5ROZ\ntuUinUlRTo3h0zu03lmcJVN1yl9jVi/7aukBUSQQNEcW52dB3a4=" 22 | - secure: "Dx2tppnkY4wDiOU+vsQF860/t8du67YzhDm3Yb8e90R7bD7bnJ444/I0Ezel\noCO0u/RzQsaS2Ms6QO1xDcle2Cl6p+Il78nAdPdpePWlMhVzKXIYvAbDJCP3\nXl04CVY4svyCLIk3Gga9T9/21O7NoDLZXwg4sg4bOaAB+G0nNkY=" 23 | - secure: "GcBV4OoIMUgE4kAOEdI+EPuGdGjsloRuDL0EjI1Lc7PoT3icf3xLr2FB+l+g\nvJTG4CHEY4aLfOkCOA44vJZ0cvHUisqv/mC08yCg39jtw6KUQSg18HcfTX/u\nX0HXKPXWMcIEmjhvUgUONmBonV2gwcPMYkENhQy3l0Uec3jwomc=" 24 | - secure: "F932CiYdF76bzMDBwPFPHDJhfNkAEIJe98YNPQxgyj59SHlVHvgs2ySmdZZy\n/lIqxD1CgFeewMmYrTnUM5BKHjoLOKJqYI3KkSoRy1aUpKb5o1jtNS/049mj\ntG01VMT/Lk0r37+a7tYCXhbJ81z/mmWVucIKjASO5Pyb8NieLRI=" 25 | - secure: "CfR2n51kb5KPdqYCUtXA/sM2BPF6XcWltX1MPQiQmU690RaYGThNM+y1Uhub\nMBjc7s85ejmX23b7TgY6biVheu/EWSBlnkUt88aubQZTZ4LZDCKyHpaJB4hU\nWEVKUUVG1M2VI6atV1VMzI/VOe6KhZ9OhWMvQBkXTSCijTtYDO0=" 26 | - secure: "IZBTzMB/hV/d8cspbu+Lm+J/70slbJP7VFXh007zJZtBabQvQAJMyu9RGy1A\nAW425UKC/3ogQ/xNPGaBsgeoDpwpIR8iZXAOrlzAKc3vKnv3YYAfZ7o9J1Y2\nTO1RLEmosO4PFPKkVFzsJJjbtiTz508Ak5fHokXY9nT8ouVOs48=" 27 | - secure: "amC03pSIqrIUx92Yap39FggdXxgA9QOtslXeIeZJMWerCR8/J8FYA78KZzvV\ngftT6yM8z6sq3Z4Rpu13Ci9OGUsFEz2FYUITze8rVAZXTBOy9ht4XzDlV+kA\n/NJ5OICev3o+Ra5WnSJw1rGoWzVWn4sJBPWss1s3mYas+6zdaE8=" 28 | - secure: "cs2+mY/ITTGHOp4W4FDbDRbtlyFkHGiyoMSWG4qLj3++ks//HTg7We39TkgR\nGZbgityJdeltN9t8D6i6ScxrFb6Rgcehq1ep735VCXlvq+0zYk2jUegdWLSp\n7cHuvSmICpT9UBLygdgrO4gaAaEXmsLMjqtkWwPZcv6kj0rMNvg=" 29 | - secure: "Y5V/H1IKTJUtELAZrAqVi6eF+lQJ6poDUAZm9uq9dO2bnqSg0XncH4EXOWeZ\nljqxyZxJKrUTMWpG+wDT6YZkoCvq3w9Zxo9Fo1GSpSwjpUxGv0skJHE+sJw9\n2huXswmDXAdV7kRCLOBC/rvfzy9P/r4xOYG4meqIo/wb01LJ284=" 30 | - secure: "QXvf5yWs6dUP0JOpK1Revpbho5/oEhJa0lmYxZYP+b+YU66Pf6SoXSX2Dunj\n4kr0LqtUhq0JOkkJnNvop1dWiUf65RNkNKEr423GSu6Ic1d9xxBhuRnZaocQ\niCEJbPQSlX/nSwRDxkvDdNyQxKwXDB2Vj/cIbVyAa+cuiXooGbM=" 31 | - secure: "YP6RtpAjkFHih66jPRyyosLGXITPrvnubkpAyVYghn+cs9jA8/nWujrERqtL\nkYwIe5Evzhyc1GvMyDRWAWh7mY6F9gp5Q1TEwfFGRZlN2n7iTb6dQNcE8Kbk\nJhFbI1AzyyYAe+Iwokj1jjHwfVfsTHC6lwyPoEPXeOjIC4B2lwk=" 32 | - secure: "WUQL8me3Eyp8dpix7ERgJeUTUOZPbsqWY82uJ6PtiCROF1axriEfw+VtnNrF\nq3QvHBXFWj568afPaJ8a3Lv8mdnZYgr3RWWbT04zyF3fW2XHbTRGNDJfqIG6\n/qzMRBgFJHbwBjzyFyYQlYx6HbsphUQRVtAbmhzKdC0oZsDigHw=" 33 | - secure: "YPScmVVUr7qEQZMd1/J5/lwETAgOvpgb77nAJIWStwsZ/Iq73Jh4grAFxAV+\nJttuw4AZOTqz5l5ggNtvBxKfSrYOYWCY7x+91IZb8Fo6MbzAETi2j7ho3Eh2\n1XbCWUTkv5RtLkO2gT8Fahtfc9x4zTnJuk02HC5f/0R+zMAZ01U=" 34 | - secure: "ggRiVqKFZ5bu/5tBMvJ7AECeDSN1BYU/hObC3MjZCGVn6uQwtLK081CHRWXz\nnQCNm0F1yksGUOiVbsqqOVJ/nn3TtS3SVbETeiCW7QrIHfYK6+P4a6Z4MhWX\nI4SnlxYSRW5IQHkmo40n8Xg/d8167wjlsoeIoMkZAb4RFP3xRIg=" -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION = 0.2.0 2 | 3 | install: 4 | cp gitreceive /usr/local/bin/gitreceive 5 | chmod +x /usr/local/bin/gitreceive 6 | 7 | check-docker: 8 | which docker || exit 1 9 | 10 | test: check-docker 11 | cp gitreceive tests 12 | docker build -t gitreceive-test tests 13 | rm tests/gitreceive 14 | docker run --rm gitreceive-test 15 | 16 | clean: 17 | docker rmi gitreceive-test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gitreceive 2 | ========== 3 | [![Build Status](https://travis-ci.org/progrium/gitreceive.png?branch=master)](https://travis-ci.org/progrium/gitreceive) 4 | 5 | Creates an ssh+git user that accepts on the fly repository pushes and triggers a hook script. 6 | 7 | Push code anywhere. Extend your Git workflow. 8 | 9 | gitreceive dynamically creates bare repositories with a special `pre-receive` hook that triggers your own general gitreceive hook giving you easy access to the code that was pushed while still being able to send output back to the git user. 10 | 11 | ## Requirements 12 | 13 | You need a Linux server with `git` and `sshd` installed. 14 | 15 | ## Installing 16 | 17 | On your server, download https://raw.github.com/progrium/gitreceive/master/gitreceive to a location on your $PATH and make it executable. 18 | 19 | ## Using gitreceive 20 | 21 | #### Set up a git user on the server 22 | 23 | This automatically makes a user and home directory if it doesn't exist. 24 | 25 | $ sudo gitreceive init 26 | Created receiver script in /home/git for user 'git'. 27 | 28 | You use a different user by setting `GITUSER=somethingelse` in the 29 | environment before using `gitreceive`. 30 | 31 | #### Modify the receiver script 32 | 33 | As an example receiver script, it will POST all the data to a RequestBin: 34 | 35 | $ cat /home/git/receiver 36 | #!/bin/bash 37 | URL=http://requestb.in/rlh4znrl 38 | echo "----> Posting to $URL ..." 39 | curl \ 40 | -X 'POST' \ 41 | -F "repository=$1" \ 42 | -F "revision=$2" \ 43 | -F "username=$3" \ 44 | -F "fingerprint=$4" \ 45 | -F contents=@- \ 46 | --silent $URL 47 | 48 | The username is just a name associated with a public key. The 49 | fingerprint of the key is sent so you can authenticate against the 50 | public key that you may have for that user. 51 | 52 | Commands do not have access to environment variables from the `/etc/profile` directory, so if you need access to them, you will need to maually `source /etc/profile` - or any other configuration file - within your receiver script. 53 | 54 | The repo contents are streamed into `STDIN` as an uncompressed archive (tar file). You can extract them into a directory on the server with a line like this in your receiver script: 55 | 56 | mkdir -p /some/path && cat | tar -x -C /some/path 57 | 58 | 59 | #### Create a user by uploading a public key from your laptop 60 | 61 | We just pipe our local SSH key into the `gitreceive upload-key` command via SSH: 62 | 63 | $ cat ~/.ssh/id_rsa.pub | ssh you@yourserver.com "sudo gitreceive upload-key " 64 | 65 | The `username` argument is just an arbitrary name associated with the key, mostly 66 | for use in your system for auth, etc. 67 | 68 | `gitreceive upload-key` will authorize this key for use on the `$GITUSER` 69 | account on the server, and use the SSH "forced commands" syntax in the remote 70 | `.ssh/authorized_keys` file, causing the internal `gitreceive run` command to 71 | be called when this key is used with the remote git account. This allows us to 72 | intercept the `git` requests and set up a `pre-receive` hook to run on the 73 | repo, which triggers the custom receiver script. 74 | 75 | #### Add a remote to a local repository 76 | 77 | $ git remote add demo git@yourserver.com:example 78 | 79 | The repository `example` will be created on the fly when you push. 80 | 81 | #### Push!! 82 | 83 | $ git push demo master 84 | Counting objects: 5, done. 85 | Delta compression using up to 4 threads. 86 | Compressing objects: 100% (3/3), done. 87 | Writing objects: 100% (3/3), 332 bytes, done. 88 | Total 3 (delta 1), reused 0 (delta 0) 89 | ----> Receiving progrium/gitreceive.git ... 90 | ----> Posting to http://requestb.in/rlh4znrl ... 91 | ok 92 | To git@gittest:progrium/gitreceive.git 93 | 59aa541..6eafb55 master -> master 94 | 95 | The receiver script did not attempt to silence the output of curl, so 96 | the respones of "ok" from RequestBin is shown. Use this to your 97 | advantage! You can even use chunked-transfer encoding to stream back 98 | progress in realtime if you wanted to keep using HTTP. Alternatively, you can have the 99 | receiver script run any other script on the server. 100 | 101 | #### Handling submodules 102 | Submodules are not included when you do a `git push`, if you want them to be part of your workflow, have a look at [Handling Submodules](https://github.com/progrium/gitreceive/wiki/TipsAndTricks#handling-submodules). 103 | 104 | ## So what? 105 | 106 | You can use `gitreceive` not only to trigger code on `git push`, but to provide 107 | feedback to the user and affect workflow. Use `gitreceive` to: 108 | 109 | * Put a `git push` deploy interface in front of App Engine 110 | * Run your company build/test system as a separate remote 111 | * Integrate custom systems into your workflow 112 | * Build your own Heroku 113 | * Push code anywhere 114 | 115 | I used to work at Twilio. Imagine pushing a repo with a TwiML file to a 116 | gitreceive repo with a phone number for a name. And then it runs that 117 | TwiML on Twilio and shows you the result, all from the `git push`. 118 | 119 | 120 | ## Big Thanks 121 | 122 | DotCloud, DigitalOcean 123 | 124 | ## License 125 | 126 | MIT 127 | -------------------------------------------------------------------------------- /gitreceive: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | readonly GITUSER="${GITUSER:-git}" 4 | readonly GITHOME="/home/$GITUSER" 5 | 6 | # Given a relative path, calculate the absolute path 7 | absolute_path() { 8 | pushd "$(dirname $1)" > /dev/null 9 | local abspath="$(pwd -P)" 10 | popd > /dev/null 11 | echo "$abspath/$(basename $1)" 12 | } 13 | 14 | # Create a Git user on the system, with home directory and an `.authorized_keys' file that contains the public keys 15 | # for all users that are allowed to push their repos here. User defaults to $GITUSER, which defaults to 'git'. 16 | setup_git_user() { 17 | declare home_dir="$1" git_user="$2" 18 | useradd -d "$home_dir" "$git_user" || true 19 | mkdir -p "$home_dir/.ssh" 20 | touch "$home_dir/.ssh/authorized_keys" 21 | chown -R "$git_user" "$home_dir" 22 | } 23 | 24 | # Creates a sample receiver script. This is the script that is triggered after a successful push. 25 | setup_receiver_script() { 26 | declare home_dir="$1" git_user="$2" 27 | local receiver_path="$home_dir/receiver" 28 | cat > "$receiver_path" < Posting to \$URL ..." 32 | #curl \\ 33 | # -X 'POST' \\ 34 | # -F "repository=\$1" \\ 35 | # -F "revision=\$2" \\ 36 | # -F "username=\$3" \\ 37 | # -F "fingerprint=\$4" \\ 38 | # -F contents=@- \\ 39 | # --silent \$URL 40 | EOF 41 | chmod +x "$receiver_path" 42 | chown "$git_user" "$receiver_path" 43 | } 44 | 45 | # Generate a shorter, but still unique, version of the public key associated with the user doing `git push' 46 | generate_fingerprint() { 47 | awk '{print $2}' | base64 -d | md5sum | awk '{print $1}' | sed -e 's/../:&/2g' 48 | } 49 | 50 | # Given a public key, add it to the .authorized_keys file with a 'forced command'. The 'forced command' is a syntax 51 | # specific to SSH's `.authorized_keys' file that allows you to specify a command that is run as soon as a user logs in. 52 | # Note that even though `git push' does not explicitly mention SSH, it is nevertheless using the SSH protocol under the 53 | # hood. 54 | # See: http://man.finalrewind.org/1/ssh-forcecommand/ 55 | install_authorized_key() { 56 | declare key="$1" name="$2" home_dir="$3" git_user="$4" self="$5" 57 | local fingerprint="$(echo "$key" | generate_fingerprint)" 58 | local forced_command="GITUSER=$git_user $self run $name $fingerprint" 59 | local key_options="command=\"$forced_command\",no-agent-forwarding,no-pty,no-user-rc,no-X11-forwarding,no-port-forwarding" 60 | echo "$key_options $key" >> "$home_dir/.ssh/authorized_keys" 61 | } 62 | 63 | # Remove the slash from the beginning of a path. Eg; '/twbs/bootstrap' becomes 'twbs/bootstrap' 64 | strip_root_slash() { 65 | local str="$(cat)" 66 | if [ "${str:0:1}" == "/" ]; then 67 | echo "$str" | cut -c 2- 68 | else 69 | echo "$str" 70 | fi 71 | } 72 | 73 | # Get the repo from the incoming SSH command. This is needed as the original intended response to `git push' is 74 | # overridden by the use of a 'forced command' (see install_authorized_key()). The forced command needs to know what repo 75 | # to act on. 76 | parse_repo_from_ssh_command() { 77 | awk '{print $2}' | sed -e "s/'\(.*\)'/\1/" | sed 's/\\'\''/'\''/g' | strip_root_slash 78 | } 79 | 80 | # Create a git-enabled folder ready to receive git activity, like `git push' 81 | ensure_bare_repo() { 82 | declare repo_path="$1" 83 | if [ ! -d "$repo_path" ]; then 84 | mkdir -p "$repo_path" 85 | cd "$repo_path" 86 | git init --bare > /dev/null 87 | cd - > /dev/null 88 | fi 89 | } 90 | 91 | # Create a Git pre-receive hook in a git repo that runs `gitreceive hook' when the repo receives a new git push 92 | ensure_prereceive_hook() { 93 | declare repo_path="$1" home_dir="$2" self="$3" 94 | local hook_path="$repo_path/hooks/pre-receive" 95 | cd "$home_dir" 96 | cat > "$hook_path" < /dev/null 102 | } 103 | 104 | # When a repo receives a push, its pre-receive hook is triggered. This in turn executes `gitreceive hook', which is a 105 | # wrapper around this function. The repo is updated and its working tree tarred so that it can be piped to 106 | # `$home_dir/receiver'. The receiver script is setup by `setup_receiver_script()'. 107 | trigger_receiver() { 108 | declare repo="$1" user="$2" fingerprint="$3" home_dir="$4" 109 | # oldrev, newrev, refname are a feature of the way in which Git executes the pre-receive hook. 110 | # See https://www.kernel.org/pub/software/scm/git/docs/githooks.html 111 | while read oldrev newrev refname; do 112 | # Only run this script for the master branch. You can remove this 113 | # if block if you wish to run it for others as well. 114 | [[ "$refname" == "refs/heads/master" ]] && \ 115 | git archive "$newrev" | "$home_dir/receiver" "$repo" "$newrev" "$user" "$fingerprint" 116 | done 117 | } 118 | 119 | # Places cursor at start of line, so that subsequent text replaces existing text. For example; 120 | # "remote: Updated branch 'master' of 'repo'. Deploying to dev." becomes 121 | # "------> Updated branch 'master' of 'repo'. Deploying to dev." 122 | strip_remote_prefix() { 123 | sed -u "s/^/"$'\e[1G'"/" 124 | } 125 | 126 | main() { 127 | # Be unforgiving about errors 128 | set -euo pipefail 129 | 130 | readonly SELF="$(absolute_path $0)" 131 | 132 | case "$1" in 133 | # Public commands 134 | 135 | init) # gitreceive init 136 | setup_git_user "$GITHOME" "$GITUSER" 137 | setup_receiver_script "$GITHOME" "$GITUSER" 138 | echo "Created receiver script in $GITHOME for user '$GITUSER'." 139 | ;; 140 | 141 | upload-key) # sudo gitreceive upload-key 142 | declare name="$2" 143 | local key="$(cat)" 144 | install_authorized_key "$key" "$name" "$GITHOME" "$GITUSER" "$SELF" 145 | echo "$key" | generate_fingerprint 146 | ;; 147 | 148 | # Internal commands 149 | 150 | # Called by the 'forced command' when the git user first authenticates against the server 151 | run) 152 | declare user="$2" fingerprint="$3" 153 | export RECEIVE_USER="$user" 154 | export RECEIVE_FINGERPRINT="$fingerprint" 155 | export RECEIVE_REPO="$(echo "$SSH_ORIGINAL_COMMAND" | parse_repo_from_ssh_command)" 156 | if [ ! $RECEIVE_REPO ]; then 157 | echo "ERROR: Arbitrary ssh prohibited!" 158 | exit 1 159 | fi 160 | local repo_path="$GITHOME/$RECEIVE_REPO" 161 | ensure_bare_repo "$repo_path" 162 | ensure_prereceive_hook "$repo_path" "$GITHOME" "$SELF" 163 | cd "$GITHOME" 164 | # $SSH_ORIGINAL_COMMAND is set by `sshd'. It stores the originally intended command to be run by `git push'. In 165 | # our case it is overridden by the 'forced command', so we need to reinstate it now that the 'forced command' has 166 | # run. 167 | git-shell -c "$(echo "$SSH_ORIGINAL_COMMAND" | awk '{print $1}') '$RECEIVE_REPO'" 168 | ;; 169 | 170 | # Called by the pre-receive hook 171 | hook) 172 | trigger_receiver "$RECEIVE_REPO" "$RECEIVE_USER" "$RECEIVE_FINGERPRINT" "$GITHOME" | strip_remote_prefix 173 | ;; 174 | 175 | *) 176 | echo "Usage: gitreceive [options]" 177 | ;; 178 | esac 179 | } 180 | 181 | [[ "$0" == "$BASH_SOURCE" ]] && main $@ 182 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitreceive", 3 | "version": "0.2.0", 4 | "description": "Easily accept and handle arbitrary git pushes", 5 | "global": "true", 6 | "install": "make install", 7 | "scripts": [ "gitreceive" ] 8 | } 9 | 10 | -------------------------------------------------------------------------------- /tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:precise 2 | 3 | RUN apt-get -y update && apt-get -y install git ssh 4 | RUN ssh-keygen -t rsa -N "" -f /root/.ssh/id_rsa 5 | RUN cp /root/.ssh/id_rsa.pub /root/.ssh/authorized_keys 6 | RUN mkdir -p /var/run/sshd 7 | RUN echo "Host localhost\n UserKnownHostsFile=/dev/null\n StrictHostKeyChecking=no" >> /root/.ssh/config 8 | RUN git clone https://github.com/sstephenson/bats.git && cd bats && ./install.sh /usr/local 9 | RUN git config --global user.email "robot@example.com" 10 | RUN git config --global user.name "Robot" 11 | 12 | ADD . /tmp 13 | RUN mv /tmp/gitreceive /usr/local/bin/gitreceive 14 | CMD /usr/sbin/sshd && /usr/local/bin/bats /tmp/gitreceive.bats -------------------------------------------------------------------------------- /tests/gitreceive.bats: -------------------------------------------------------------------------------- 1 | 2 | teardown() { 3 | rm -rf /home/git 4 | userdel git 5 | } 6 | 7 | @test "gitreceive init creates git user ready for pushes" { 8 | gitreceive init 9 | [[ -d /home/git ]] 10 | [[ -f /home/git/.ssh/authorized_keys ]] 11 | [[ -f /home/git/receiver ]] 12 | [[ "git" == "$(ls -l /home/git/receiver | awk '{print $3}')" ]] 13 | } 14 | 15 | @test "gitreceive receiver script gets tar of pushed repo" { 16 | gitreceive init 17 | cat /root/.ssh/id_rsa.pub | gitreceive upload-key test 18 | local output_dir="$BATS_TMPDIR/$BATS_TEST_NAME-push" 19 | mkdir -p "$output_dir" 20 | chown git "$output_dir" 21 | cat < /home/git/receiver 22 | #!/bin/bash 23 | set -x 24 | tar -C $output_dir -xvf - 25 | EOF 26 | local input_repo="$BATS_TMPDIR/$BATS_TEST_NAME-repo" 27 | mkdir -p "$input_repo" 28 | cd "$input_repo" 29 | git init 30 | echo "foobar" > contents 31 | git add . 32 | git commit -m 'only commit' 33 | git remote add test git@localhost:test-$BATS_TEST_NUMBER 34 | git push test master 35 | [[ -f "$output_dir/contents" ]] 36 | [[ "foobar" == $(cat "$output_dir/contents") ]] 37 | } 38 | --------------------------------------------------------------------------------