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.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
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!