Originally published on 13 October 2021
Last updated on 13 October 2021
Contents
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.
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.
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:
description, to store the repository’s description. This will be shown on both the initial index page as well as at the top of each repositories page. This file will have been automatically created when you ran the initial git init --bare command.
owner, which will be shown on the index page. Just enter the owner’s name on the first line of the file.
url, which contains the repository’s clone URL. In our configuration, this would look like git://git.example.com/repo_directory.
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 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...]
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:
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.
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
/etc/rc.confThere 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
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.
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) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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:
Figure out how to clone Git repos over HTTPS using Smart HTTP instead of the git protocol we used here. Specifically, I’d like to use the Nginx server I already have. I believe this requires the use of FastCGI but this is not a topic I am familiar with.
Using a post-receive hook to update static files when committing changes. Right now, it’s a pain to have to manually run the example_create.sh script each time I push changes up to the remote repositories. I’d like to automate this process with each commit.
Figure out how to provide raw, or plain text, versions of my files for easier copying and pasting. Here is an example of what I am looking for.
See if there is a way to set the Looks like I got this to work by switching the git user shell back to git-shell while still allowing me to serve the repositories. I think this may be possible assuming I can figure out the first item on my list.git user to /usr/local/bin/git-shell. See the update posted above.
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!