KISS

Keep It Simple Stupid

Howto: Serve a web application from different git branches

| comments

Update on 2013-06-09: I’ve updated the web_server_deploy.sh script, so it now prints the output of git fetch, to ease debugging some repo access problems.

One of the projects I’m working on is a cross-platform (iOS/Android/Windows) application with the GUI written in HTML/JS/CSS, so essentially it’s displayed in a web view, and I’m responsible for the iOS version. However, to help the web developers with debugging the code on the mobile platforms, I suggested using a web server that would serve the web code that can be displayed in a mobile browser. Of course, initiative is punishable, so I had to do it.

I am given a virtual machine with Windows (ouch!) to implement the idea. Since we have a number of different branches in the git repository and the developers want to test a feature without merging to develop yet, it is important that the server can share the files from different branches. The best concept I’ve come up with is to create a dedicated directory in the web root for each branch. We’ll need portable msysgit to work with git and have the decent command-line tools. Let’s do that!

Web server

First, I’ve chosen a lightweight, single-executable, yet full of features Mongoose webserver. I put it in the c:\dev\webserver\ directory. Here’s my config:

(mongoose.conf) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Mongoose web server configuration file.
# Lines starting with '#' and empty lines are ignored.
# For detailed description of every option, visit
# http://code.google.com/p/mongoose/wiki/MongooseManual

# cgi_pattern **.cgi$|**.pl$|**.php$
cgi_pattern **.bat$
# cgi_environment <value>
# put_delete_passwords_file <value>
# cgi_interpreter <value>
# protect_uri <value>
# authentication_domain mydomain.com
# ssi_pattern **.shtml$|**.shtm$
access_log_file c:\dev\webserver\access.log
# ssl_chain_file <value>
enable_directory_listing no
error_log_file c:\dev\webserver\error.log
# global_passwords_file <value>
# index_files index.html,index.htm,index.cgi
# enable_keep_alive no
# access_control_list <value>
# max_request_size 16384
# extra_mime_types <value>
listening_ports 8666
document_root c:\dev\app_docroot
# ssl_certificate <value>
# num_threads 10
# run_as_user <value>
url_rewrite_patterns /update.sh=c:\dev\webserver\git_update.bat,/testing=c:\dev\app_testing

Basically, I setup access and error log files, listening port to 8666, document root, and disable directory listing (security by obscurity, I know it doesn’t work very well). More interesting parameters are:

  • cgi_pattern **.bat$ says that the files with the bat extension are executable, and should be run when accessed.

  • url_rewrite_patterns /update.sh=c:\dev\webserver\git_update.bat,/testing=c:\dev\app_testing says that when a client asks for the /update.sh file, mongoose should run the git_update.bat script (see the previous point), and, for testing, the files in c:\dev\app_testing should be served when asked for /testing directory.

Update script wrapper

Now to the git_update.bat script which is actually a wrapper around a bash script that does all the work. I didn’t manage to have it run directly by the server, it’s windows, remember? OK, check it out:

(git_update.bat) download
1
2
3
4
5
6
7
@echo off
set PATH=c:\portable\portablegit\bin\;%PATH%
set TMP=C:\Users\user\AppData\Local\Temp
set TEMP=%TMP%
set GIT_DIR=c:\dev\app.git
set HOME=c:\Users\user
c:\portable\portablegit\bin\bash c:\dev\app.git\web_server_deploy.sh 2>&1

What it does is it sets up the required environment variables for the web_server_deploy.sh bash script:

Update script

The most yummy part here, the update script itself:

(web_server_deploy.sh) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/bin/bash

echo -ne "HTTP/1.1 200 OK\r\n\r\n"

DOCROOT_DIR=/c/dev/app_docroot/

if [[ "$1" == "--init" ]]; then
    ALL_REFS=$(git tag)$(git branch -r | grep -v 'origin/HEAD')
    for ref in ${ALL_REFS}; do
        echo "$ref init"
        mkdir -p "${DOCROOT_DIR}${ref}"
        git archive ${ref} web/src/ | tar -x --strip-components 2 -C "${DOCROOT_DIR}${ref}"
    done

    exit 0
fi

FETCH_OUTPUT=$(git fetch --dry-run --prune origin 2>&1)
echo ${FETCH_OUTPUT}

ALL_CHANGES=$(git fetch --prune origin 2>&1 | egrep -- '->')

NEW_AND_MODIFIED_REFS=$(echo "${ALL_CHANGES}" | awk '$2 ~ /\[new/ { print $6 }; $1 ~ /\.\./ { print $4 }' | sort -u)

for ref in ${NEW_AND_MODIFIED_REFS}; do
    echo "$ref updated"
    mkdir -p "${DOCROOT_DIR}${ref}"
    git archive ${ref} web/src/ | tar -x --strip-components 2 -C "${DOCROOT_DIR}${ref}"
done

DELETED_REFS=$(echo "${ALL_CHANGES}" | awk '$2 ~ /\[deleted\]/ { print $5 }')

for ref in ${DELETED_REFS}; do
    echo "$ref deleted"
    rm -rf "${DOCROOT_DIR}${ref}"
done

echo Done.

Because the script is intended to run by a webserver, thus printing HTTP headers and stuff, it first outputs that everything is 200 OK. The DOCROOT_DIR variable defines the base directory for all the files the script creates. NB it’s the same document root specified in mongoose.conf (see above), but written in unix style.

The script can run in normal mode (adding/updating/removing folders) and the first time mode (initial creation of the folders) triggered with the --init switch. You should run it the first time with this switch on. Then, in normal mode, the script fetches all the updates from origin, creates and rewrites the directories corresponding to the new and modified references (tags and branches), and removes the ones for the deleted refs.

The command to deploy the reference’s contents into a directory,

1
git archive ${ref} src/web/ | tar -x --strip-components 2 -C "${DOCROOT_DIR}${ref}"

, consists of two parts: first, git creates an archive from the current reference ${ref} of the only src/web/ directory, where the web sources are stored, and, second, tar immediately extracts the archive into a subdirectory in the document root (-C "${DOCROOT_DIR}${ref}"), skipping the src/web/ part (--strip-components 2).

Run the web server, and to update the contents from the repo, go to http://example.com:8666/update.sh (where example.com is your domain/IP address), which will show something like this:

1
2
3
4
origin/develop updated
tagBeta1 updated
origin/test/coolFeature deleted
Done.

To get the web app, go to http://example.com:8666/, and append the name of the branch/tag you want to get the full address, say http://example.com:8666/origin/develop.

Done! Thanks for reading!

Comments