Samir Parikh / Blog


Originally published on 13 October 2021

Last updated on 13 October 2021

Contents

Background

In my last post, I documented how I set up a simple FreeBSD server to host remote Git repositories that you could update via SSH. Unfortunately, the rest of the internet has no visibility to your work or repositories. This may not be an issue if all you need is a separate server to privately host remote repositories for yourself or a small team of contributors. But if you want to share what you’ve done or make your code visible in a limited manner, you’ll need some way to publish or visualize your repositories. One way to do this is using cgit, which I wrote about almost two years ago. It’s a Git front-end that exposes your remote repositories via Nginx or some other web server. It’s a much more lightweight front-end compared to other self-hosted options such as GitLab or Gitea.

Another option that I found recently, which is even more lightweight than (though not as feature full as) cgit is Stagit, which is a static Git page generator. It requires some additional work to get up and running (and to keep it updated as you push commits to your repos) but I’ve found the code easier to understand and find the static nature of the pages more appealing from a performance and security standpoint. For an example of Stagit in action, you can check out the developer’s own Git repository. In this post, I’ll document how I got it up and running.

Prerequisites

Before we install Stagit, I’m assuming you already have a FreeBSD machine or VPS that is storing your remote repositories as described in the previous post.

I’m also assuming that the same FreeBSD VPS is already serving static web pages (say for a blog) over HTTPS. This means, you already have a domain and an @ DNS A record configured so that you can serve your blog pages at https://example.com. Further, I’m assuming that you have installed the nginx and py38-certbot-nginx packages. If your remote machine is not currently serving any static content, you can go ahead and install those packages when we install Stagit.

We’ll be serving our Git repositories over on the git subdomain so please ensure that you’ve configured another DNS A record so that git.example.com points to your server’s IP address.

Finally, as we’ll be using git-daemon to allow visitors to our site to clone our repositories, we need to ensure that port 9418 is open to TCP traffic.

Prepare Remote Git Repositories for Stagit

Before we install Stagit, we need to ensure that our existing Git repositories contain some key files which Stagit uses to display the repo’s description, owner and clone URL. For each repository located in /home/git/repos/ (assuming this is the directory where your repos are located), update or create the following files:

Important Note Regarding Shells:

In my last post explaining how to configure a Git server and create a git user, I assigned git-shell to the git user. This was done to limit the ability of what a user with SSH access to the git user could do. Unfortunately, in my subsequent testing, I found that retaining git-shell as the user shell also prevents me from running the Git Daemon which will allow us to serve our repositories using the git protocol. Therefore, I had to change the shell of our git user to something like tcsh(1):

$ sudo chsh -s /bin/tcsh git

Update from 13 October 2021

I may have found a solution to the git-shell issue assigned to the git user. It appears that the shell that I was using for the git user was /usr/local/libexec/git-core/git-shell. (You can find which shell you are using by running grep git /etc/passwd.) What I should have been using is /usr/local/bin/git-shell which I found by running the which git-shell command.) Therefore, ensure your git user is running the correct shell by executing:

$ sudo chsh -s /usr/local/bin/git-shell git

Install Stagit

Install prerequisites:

$ sudo pkg install nginx py38-certbot-nginx # only if you don't have these packages already installed

$ sudo pkg install libgit2 # stagit dependency

Install Stagit:

$ git clone git://git.codemadness.org/stagit
Cloning into 'stagit'...
remote: Enumerating objects: 1134, done.
remote: Counting objects: 100% (1134/1134), done.
remote: Compressing objects: 100% (589/589), done.
remote: Total 1134 (delta 749), reused 827 (delta 544), pack-reused 0
Receiving objects: 100% (1134/1134), 187.97 KiB | 403.00 KiB/s, done.
Resolving deltas: 100% (749/749), done.

$ cd stagit/

$ make
cc -o stagit.o -c stagit.c -I/usr/local/include -O2 -pipe -D_XOPEN_SOURCE=700 -D_DEFAULT_SOURCE -D_BSD_SOURCE
cc -o reallocarray.o -c reallocarray.c -I/usr/local/include -O2 -pipe -D_XOPEN_SOURCE=700 -D_DEFAULT_SOURCE -D_BSD_SOURCE
cc -o strlcat.o -c strlcat.c -I/usr/local/include -O2 -pipe -D_XOPEN_SOURCE=700 -D_DEFAULT_SOURCE -D_BSD_SOURCE
cc -o strlcpy.o -c strlcpy.c -I/usr/local/include -O2 -pipe -D_XOPEN_SOURCE=700 -D_DEFAULT_SOURCE -D_BSD_SOURCE
cc -o stagit stagit.o reallocarray.o strlcat.o strlcpy.o -L/usr/local/lib -lgit2
cc -o stagit-index.o -c stagit-index.c -I/usr/local/include -O2 -pipe -D_XOPEN_SOURCE=700 -D_DEFAULT_SOURCE -D_BSD_SOURCE
cc -o stagit-index stagit-index.o reallocarray.o strlcat.o strlcpy.o -L/usr/local/lib -lgit2

$ sudo make install
mkdir -p /usr/local/bin
cp -f stagit stagit-index /usr/local/bin
for f in stagit stagit-index; do chmod 755 /usr/local/bin/$f; done
mkdir -p /usr/local/share/doc/stagit
cp -f style.css favicon.png logo.png example_create.sh example_post-receive.sh README /usr/local/share/doc/stagit
mkdir -p /usr/local/man/man1
cp -f stagit.1 stagit-index.1 /usr/local/man/man1
for m in stagit.1 stagit-index.1; do chmod 644 /usr/local/man/man1/$m; done

Test your installation:

$ stagit
stagit [-c cachefile | -l commits] [-u baseurl] repodir

$ stagit-index
stagit-index [repodir...]

Create Static Content

First, create the directory stagit from which the Git repository content will be served:

$ sudo mkdir -p /usr/local/www/stagit

This will be the root directory within which we’ll create additional directories for each of our repositories in /home/git/repos/. Therefore, if we have a repository called /home/git/repos/repository01, we need to create the corresponding directory under /usr/local/www/stagit:

$ sudo mkdir /usr/local/www/stagit/repository01

Repeat the last step for each repository you have.

Next, copy over the favicon.png, logo.png, and style.css files that came with Stagit over to our root directory:

$ sudo cp /usr/local/share/doc/stagit/favicon.png /usr/local/www/stagit/

$ sudo cp /usr/local/share/doc/stagit/logo.png /usr/local/www/stagit/

$ sudo cp /usr/local/share/doc/stagit/style.css /usr/local/www/stagit/

Now we can generate the static HTML files for our Git repositories. To do this, we have a couple of options:

  1. We can use the stagit command to generate the individual repo files and the stagit-index command to create the overall index file for all of the repositories. Stagit’s README file has further instructions on how to do this but this can be a bit tedious if you have a lot of repositories already in place.

  2. We can use the example_create.sh script that comes with Stagit to create these files for us automatically. That’s the approach I’ll document here.

Open up the example_create.sh file (it is in the stagit directory you cloned earlier) and update the lines in bold below:

#!/bin/sh
# - Makes index for repositories in a single directory.
# - Makes static pages for each repository directory.
#
# NOTE, things to do manually (once) before running this script:
# - copy style.css, logo.png and favicon.png manually, a style.css example
#   is included.
#
# - write clone URL, for example "git://git.codemadness.org/dir" to the "url"
#   file for each repo.
# - write owner of repo to the "owner" file.
# - write description in "description" file.
#
# Usage:
# - mkdir -p htmldir && cd htmldir
# - sh example_create.sh

# path must be absolute.
#reposdir="/var/www/domains/git.codemadness.nl/home/src"
reposdir="/home/git/repos"
curdir="$(pwd)"

# make index.
stagit-index "${reposdir}/"*/ > "${curdir}/index.html"

# make files per repo.
for dir in "${reposdir}/"*/; do
        # strip .git suffix.
        r=$(basename "${dir}")
        d=$(basename "${dir}" ".git")
        printf "%s... " "${d}"

        mkdir -p "${curdir}/${d}"
        cd "${curdir}/${d}" || continue
        stagit -c ".cache" -u "https://git.example.com/$d/" "${reposdir}/${r}"

        # symlinks
        ln -sf log.html index.html
        ln -sf ../style.css style.css
        ln -sf ../logo.png logo.png
        ln -sf ../favicon.png favicon.png

        echo "done"
done

Copy the script over to our root directory:

$ sudo cp example_create.sh /usr/local/www/stagit/

Now we can run the script:

$ sudo sh /usr/local/www/stagit/example_create.sh
repository01... done
repository02... done
repository03... done

Update /etc/rc.conf

There are a couple of services we need to enable in /etc.rc.conf. These are nginx to serve the static pages and git_daemon to allow users to clone our repositories. Add the following lines to your /etc/rc.conf file:

nginx_enable="YES"
git_daemon_enable="YES"
git_daemon_directory="/home/git/repos"
git_daemon_flags="--syslog --base-path=/home/git/repos --export-all --reuseaddr --detach"

Start the git_daemon service (we’ll start the nginx service later):

$ sudo service git_daemon start

Update Nginx Configuration

Now that we have our static files, we need to update our /usr/local/etc/nginx/nginx.conf file to serve them. Assuming that you already have Nginx running to serve another site, this requires us to add another server block within the overall http section. If you don’t already have Nginx running, you can just update the existing server block with the appropriate server_name and root directives:

worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    server {
    server_name  example.com;

        location / {
        root   /usr/local/www/blog;
            index  index.html index.htm;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   /usr/local/www/nginx-dist;
        }
    
    listen 443 ssl; # managed by Certbot
    ssl_certificate /usr/local/etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /usr/local/etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
    include /usr/local/etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

    server {
        server_name  git.example.com;

        location / {
            root  /usr/local/www/stagit;
            index  index.html index.htm;
        }
    }

    server {
        if ($host = example.com) {
            return 301 https://$host$request_uri;
    } # managed by Certbot

        listen       80;
    server_name  example.com;
    return 404; # managed by Certbot
    }
}

Double-check that your Nginx configuration is correct:

$ sudo nginx -t
nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok
nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful

Restart Nginx:

$ sudo service nginx restart
Performing sanity check on nginx configuration:
nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok
nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful
Stopping nginx.
Performing sanity check on nginx configuration:
nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok
nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful
Starting nginx.

At this point, you should be able to go to http://git.example.com to see your repositories.

Enable TLS Connectivity with Let’s Encrypt

As we’ve done before, we can quite easily encrypt our connections to git.example.com using TLS by obtaining a Let’s Encrypt certificate:

$ sudo certbot --nginx -d git.example.com
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for git.example.com

Successfully received certificate.
Certificate is saved at: /usr/local/etc/letsencrypt/live/git.example.com/fullchain.pem
Key is saved at:         /usr/local/etc/letsencrypt/live/git.example.com/privkey.pem
This certificate expires on 2022-01-10.
These files will be updated when the certificate renews.

Deploying certificate
Successfully deployed certificate for git.example.com to /usr/local/etc/nginx/nginx.conf
Congratulations! You have successfully enabled HTTPS on https://git.example.com

NEXT STEPS:
- The certificate will need to be renewed before it expires. Certbot can automatically
renew the certificate in the background, but you may need to take steps to enable that
functionality. See https://certbot.org/renewal-setup for instructions.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
 * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
 * Donating to EFF:                    https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

You should now be able to connect to git.example.com using HTTPS. Don’t forget to add a cron job to automatically renew your certificate. You can add the following line to your /etc/crontab file:

0 0,12 * * * root python -c 'import random; import time; time.sleep(random.random() * 3600)' && certbot renew -q

To test the Let’s Encrypt renewal process, run:

$ sudo certbot renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /usr/local/etc/letsencrypt/renewal/git.example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Account registered.
Simulating renewal of an existing certificate for git.example.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /usr/local/etc/letsencrypt/renewal/example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Simulating renewal of an existing certificate for example.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations, all simulated renewals succeeded: 
  /usr/local/etc/letsencrypt/live/git.example.com/fullchain.pem (success)
  /usr/local/etc/letsencrypt/live/example.com/fullchain.pem (success)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

To Dos

While that does seem like a lot of work to get some simple static pages served for my Git repositories, I am pretty happy with the result. However, there are still a few more things I’d like to sort out before calling this complete:

Conclusion

Overall, I like the simple look and feel of Stagit along with the navigable code. I don’t know how to program in C but will try to experiment with some changes to see if I can knock off any items in my to do list. If you have ideas or suggestions, please let me know!