Witch of Light

Webrings    Archive    RSS
A collection of Cassie's harebrained schemes.

Hosting My Own Git

By Cassie Jones, posted

I’ve wanted to get off GitHub at least for new projects due to their supporting ICE, but I hadn’t figured what I was going to do until recently. Then, Jordan Rose made a post about setting up personal Git hosting that I liked, and I learned how easy it was to set up, and about GitWeb. So I set up my own shared Git hosting that suited my needs, which you can take a look at at git.witchoflight.com.

Hosting Your Own Git

It turns out if you only care about SSH access, hosting Git is way easier than I believed it would be. If you have a server with SSH access, as the user you want to use Git as, run git init --bare to create a repository. On my server, I created a git user, and a /repos/ folder owned by the git user where I could create all of my repos. Then, it’s just a matter of using <user>@<server>:<path> as your remote. With that, your git and ssh handles all of the connections and transfer, you don’t need a special Git server running or anything.

As a specific example of using this: When I want a new repository on my server “Sunstone,” I ssh there and create a new bare repository:

$ ssh sunstone
$ su git
$ cd /repos/
$ git init --bare fancy-new-repo.git

I include the .git suffix as a personal preference, it’s just part of the path and doesn’t do anything special, so you can leave it off if you prefer. Now I can clone it as git clone git@sunstone:/repos/fancy-new-repo.git. You can do all sorts of setup to make this work fine for letting multiple users push, including users that don’t have access to most of the computer, but I’ll leave those kinds of things to the Git docs. There are other fancy things you can do to make it nicer, I’ll get to some of those later.

Sharing With The World

One of the big things that GitHub offers is the ability for anybody to easily read and clone the code without any particular authorization. It turns out that Git itself offers a solution for this, called GitWeb. It’s a server that makes readable web pages for Git repositories, and as a side effect you can do plain HTTP(S) clones.

For this, I followed the setup directions in the docs and in Jordan’s post, as well as searching around online for various setups. In the end, I put a few pieces together which were different from what I found. My setup is a bit different from those because I’m on nginx. Like Jordan, I want people to be able to use the nice URLs, and also to clone over HTTPS.

Configuring GitWeb

First, /etc/gitweb.conf:

our $projectroot = "/repos";
our $site_name = 'Witch of Git';
our @git_base_url_list = qw(https://git.witchoflight.com);
our $omit_owner = true;
$feature{'highlight'}{'default'} = [1];
$feature{'pathinfo'}{'default'} = [1];
$feature{'search'}{'default'} = [0];

our %highlight_ext = (
    %highlight_ext,
    (map { $_ => $_ } qw(rs lua)),
    toml => 'ini',  # good enough for lazy :?
);

@stylesheets = ("/static/gitweb.css");
$logo = "/static/git-logo.png";
$favicon = "/static/git-favicon.png";
$javascript = "/static/gitweb.js";

$export_auth_hook = sub { not ($_[0] =~ /\.git\z/) };

This does a few things. First, some basic variables. Where it should look for files, what the title should be, stuff like that. I enable syntax highlighting, and importantly enable pathinfo, which changes URLs from messy query strings to readable URLs. If you’re not careful, this can cause trouble in the server config later, but I’ll go over that.

I need to modify %highlight_ext, which specifies what file extensions map to which syntax highlighting. This is based on looking at /usr/share/gitweb/gitweb.cgi which sets up all the features that you can modify, and it has a comment:

# see files in /usr/share/highlight/langDefs/ directory

Setting pathinfo requires that we specify the stylesheets, logo, favicon, and JS explicitly, for some reason.

The very last line is my own idea here. The $export_auth_hook is a callback that checks if a given repo should be exported. I create two copies of all my repos with a symlink, a repo.git, and a plain repo linked to the first. I create the former because I like having my remotes have .git on them, and I create the latter because it makes for nicer URLs in GitWeb. I use the hook here to avoid exporting two copies of each.

Nginx

I happen to like Nginx as a static file server. I planned to have it already running on my server, so it seemed like the best idea to get GitWeb running through there. Unfortunately, GitWeb is a CGI script, and Nginx doesn’t directly support them. I searched around on three different fronts. One, tutorials for Nginx CGI scripts. Another, tutorials for setting up GitWeb with other servers. And also in the middle of those, tutorials actually on GitWeb with Nginx. They didn’t completely solve my issues, but I used a tutorial from the Arch wiki and a tutorial in a gist.

Synthesizing all this together, I made my /etc/nginx/sites-available/gitweb:

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name git.witchoflight.com;
    root /repos/;

    location / {
        try_files $uri @gitweb;
    }

    location /static/ {
        root /usr/share/gitweb/;
    }

    location @gitweb {
        include fastcgi_params;
        gzip off;
        fastcgi_param SCRIPT_FILENAME /usr/share/gitweb/gitweb.cgi;
        fastcgi_param PATH_INFO $uri;
        fastcgi_param GITWEB_CONFIG /etc/gitweb.conf;
        fastcgi_pass unix:/var/run/fcgiwrap.socket;
    }
}

This will try to route files at the root through GitWeb, and files under /static/ to the filesystem. GitWeb is set up as a fastcgi client, with fastcgi configured to run through fcgiwrap.socket. I pass the request uri in the PATH_INFO, seems to be necessary to handle routing with the pretty paths. As the absolute basics, you always have to pass the script filename, and the config. I actually got stuck for about an hour here with a mismatch between fcgiwrap.socket and fcgiwrap.sock, so check your spellings carefully while you configure things! With this setup, GitWeb will correctly format HTML pages when it should, and also correctly pass through files inside the repositories so that HTTP(S) cloning works fine. Speaking of HTTPS…

Using HTTPS

As my final step, I got a cert set up for HTTPS. I’ve never set up my own HTTPS before, just used it if it was provided by my hosting, but setting one up with Let’s Encrypt ended up being quite easy. I just directly followed the steps on their “Get Started” page and things worked first try. I pointed it at my GitWeb Nginx config and it slightly rewrote it to add certs and things, which I left out above.

Making It Nice

I do lots of hobby projects, so I want it to be nice to set up repos. In the git user’s home directory, I have a git-hooks/ folder that holds all of the “universal” base hooks I come up with, a git-template/ folder that stores the template for new git repos, and a make-repo script which does the setup for new repositories.

First, the git-hooks/. I have a dedicated folder for them so the template can symlink that, so I don’t need to edit all the different hooks. Currently I just have one, the post-update hook. It contains:

#!/bin/sh
git update-server-info
git cat-file blob HEAD:README.md | pulldown-cmark > README.html

We need to git update-server-info to prepare the data that HTTP clones and things need. Then, we prepare the README.html file that GitWeb displays on the main page of a project. For this, git cat-file blob HEAD:README.md grabs the latest README.md from the main branch, and then we render it to HTML.

In the git-template/ folder, I make symlinks from the ~/git-hooks/ folder into the git-template/hooks/ folder so they just need to be updated in one place. I also change the default branch to develop since I prefer that instead of master. You can set the template dir with git config --global init.templatedir <path>.

And then the make-repo script I just have set up so that it will create both repo and repo.git in the right place when I ask for it, and let me rewrite the description immediately.

#!/bin/bash
set -e

if [ -z $1 ]; then
	echo "Usage: make-repo <name>"
	exit 0
fi
# Strip .git from the name if you specify it, for uniformity
repo=$(basename -s.git $1)
# We don't want to work as root, but we login as root (oops?)
# so it's convenient to automatically switch to the git user
if [ "$EUID" -eq 0 ]; then exec su git -c "$0 $1"; fi
# Only allow running as the git user, for file permissions purposes
if [ $(whoami) != "git" ]; then
	echo "Please run this tool as the 'git' user"
	exit 1
fi
# Don't overwrite existing repositories
if [ -d "/repos/$repo.git" ]; then
	echo "Repository '$1' already exists"
	exit 2
fi

cd /repos/
# Make a bare repo for SSH cloning
git init --bare "$repo.git"
# We immediately edit the description file that will show up in GitWeb
sensible-editor "$repo.git/description"
# And have both repo and repo.git point to the same place for cloning
ln -s "$repo.git" "$repo"

What Now?

Well, now I have Git hosting! And if you want some, you can probably figure it out too, it’s less work than I thought it might be! I’ve done some minimal styling, making it a narrower column and fixing some things, but I want to do more. That’s a project for another day, though.

Now, go look at my projects. Some of them are cool! I’ll try to write about them soon. :) git.witchoflight.com.