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.
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:
chown 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\n' "$*"
}
e_err() {
printf '%s\n' "$*" >&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 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