⊕ a tessier-ashpool subsidiary

stagit on OpenBSD

⊕ 2020-05-10

I have a lot of personal projects that I work on in a semi-distributed fashion and I like to keep development of all my projects on a locally hosted git server over SSH. Unfortunately this is not the best for browsing through projects or quickly visually showing updates. So I started hosting an internal service on my git server using stagit(1) which will turn a git repo into a set of static HTML resources.

In the end it looks something like this:

And actually viewing a repository:

I’ll be using nginx for serving the content and git hooks for generating the static HTML. Theoretically the only part of this that will need a user interaction apart from git interaction is the initial creation of a new repository. This is done using some doas(1) trickery and helps keep everything done by the git user done in a safer non-shell spawning fashion.

Setting up

The first steps are to install git and nginx from the OpenBSD packages and then create a new _git user that will be our local user for managing the git and stagit functionality. (This name can be changed to something more like the traditional git user, but I wanted to keep with the underscore for service user tradition on OpenBSD):

pkg_add stagit nginx git
groupadd -g 998 _git
mkdir -p -m 744 /var/git/repos /var/git/template
useradd -u 998 -g 998 -L daemon -c "git backend user" -d /var/git/repos -s /usr/local/bin/git-shell _git
chown _git:_git /var/git/repos

Next set up a pseudo-config file that will be imported by all the automation scripts on this deployment, in this example these are just shell script variables. Change these to match your needs. This file will hold the default git home directory, web server directory, clone URI, default owner, and default description. Place the following in /var/git/config.rc:

GIT_HOME="/var/git/repos"
WWW_HOME="/var/www/htdocs/git"
CLONE_URI="_git@git.hosakacorp.net"
DEFAULT_OWNER="poptart"
DEFAULT_DESCRIPTION="default description"
GIT_USER="_git"

Restrict access to this to prevent the git user from being the owner of the configuration:

chmod root:wheel /var/git/config.rc

Now we will set up the post-receive hook for git, this server will run the stagit generators after the server receives a push. Simply this pulls our configuration file, changes directories to the web server directory for the current repository, and then generates and links the relevant created files. This should be placed in /var/git/repos/template/post-receive:

#!/bin/sh
# Author: Cale "poptart" Black
# License: MIT

set -euf

. /var/git/config.rc

export LC_CTYPE='en_US.UTF-8'
src="$(pwd)"
name=$(basename "$src")
dst="$WWW_HOME/$(basename "$name" '.git')"
mkdir -p "$dst"
cd "$dst" || exit 1

echo "[stagit] building $dst"
/usr/local/bin/stagit "$src"

echo "[stagit] linking $dst"
ln -sf log.html index.html
ln -sf ../style.css style.css
ln -sf ../logo.png logo.png

The hook is just a shell script so make sure it is marked executable:

chmod +x /var/git/template/post-recieve

Next I decided to split up the logic for the main index generating file into a separate invocable executable that can also be used in the other scripts /usr/local/bin/stagit-gen-index:

#!/bin/sh 
# Author: Cale "poptart" Black
# License: MIT

set -eu

. /var/git/config.rc
stagit-index "$GIT_HOME"/*.git > "$WWW_HOME/index.html"

The last shell script we need to write is for automating the creating on a new git repository on the system. This reads in a title and description for the new repo and then initializes the new repository in an empty and bares state, sets up the git hooks, and applies our defined defaults. The following was created in /usr/local/bin/stagit-newrepo:

#!/bin/sh
# Author: Cale "poptart" Black
# License: MIT

set -eu

. /var/git/config.rc

e_log() {
    printf '%s
' "$*"
}

e_err() {
    printf '%s
' "$*" >&2
}

e_exit() {
    e_err "$*"
    exit 1
}

DESC=""
REPO=""

if [ $# -gt 1 ]; then
        DESC="$2"
else
        DESC="$DEFAULT_DESCRIPTION"
fi

if [ $# -eq 0 ]; then
        e_exit "not enough args"
else
        REPO="$(basename "$1")"
fi

git init --bare "$GIT_HOME/$REPO.git"
cp "$GIT_HOME/template/post-receive" "$GIT_HOME/$REPO.git/hooks/post-receive"
echo "$CLONE_URI/$REPO.git" > "$GIT_HOME/$REPO.git/url"
echo "$DEFAULT_OWNER" > "$GIT_HOME/$REPO.git/owner"
if [ -n "$DESC" ]; then
        echo "$DESC" > "$GIT_HOME/$REPO.git/description"
else
        echo "this is a placeholder" > "$GIT_HOME/$REPO.git/description"
 fi
chmod u+x "$GIT_HOME/$REPO.git/hooks/post-receive"
mkdir "$WWW_HOME/$REPO"
/usr/local/bin/stagit-gen-index

Then clean up and mark all of our files with the appropriate permissions and restrictions:

chmod +x /usr/local/bin/stagit-newrepo
chmod +x $GIT_HOME/template
chown _git:_git /var/www/htdocs/git

Now in order to keep everything tidy I allow anyone in the gitadmin group or my personal account to invoke the scripts that we wrote as the _git user:

/etc/doas.conf

permit nopass poptart as _git cmd /usr/local/bin/stagit-newrepo
permit nopass poptart as _git cmd /usr/local/bin/stagit-gen-index
permit nopass :gitadmins as _git cmd /usr/local/bin/stagit-newrepo
permit nopass :gitadmins as _git cmd /usr/local/bin/stagit-gen-index

The nginx(1) configuration for this is incredibly barebones and essentially is just changing the root to where I had it configured in the config.rc file or wherever you have it configured. I added additional TLS/SSL config blocks for good practice reasons:

/etc/nginx/nginx.conf

server {
    listen       443;
    server_name  git.hosakacorp.net;
    root         /var/www/htdocs/git;

    ssl                  on;
    ssl_certificate      /etc/ssl/git.hosakacorp.net-fullchain.pem;
    ssl_certificate_key  /etc/ssl/private/git.hosakacorp.net-key.pem;
    ssl_password_file    /etc/ssl/private/git-key-password

    ssl_session_timeout  5m;
    ssl_session_cache    shared:SSL:1m;

    ssl_ciphers  HIGH:!aNULL:!MD5:!RC4;
    ssl_prefer_server_ciphers   on;
}

...snip...

Give the nginx server a simple rcctl enable nginx and nginx start nginx to get things started and we can get back to the stagit configuration.

Now in order to see an example we can generate an empty index file and then create a new test repo with the description test description:

doas -u _git /usr/local/bin/stagit-gen-index
doas -u _git /usr/local/bin/stagit-newrepo test "test description"

NOTE: The repository will not have the test git repo until the bare repository has at least one commit to generate off of.

Now we can clone the repo and start using it like normal:

git clone _git@git.hosakacorp.net:test.git

When you push commits your commits will look like similar to the following and be accessible from the web server:

$ git push -f
Enumerating objects: 12, done.
Counting objects: 100% (12/12), done.
Delta compression using up to 4 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (12/12), 1.57 KiB | 61.00 KiB/s, done.
Total 12 (delta 0), reused 0 (delta 0)
remote: [stagit] building /var/www/htdocs/git/test
remote: [stagit] linking /var/www/htdocs/git/test
To git.hosakacorp.net:test.git
 * [new branch]      master -> master