Back to All Posts

Who Needs a Whole-Ass Git Forge, Anyway?

GitHub remains the defacto king of open-source project hosting. But leaving my open-source projects, which I dump a lot of time into, at the mercy of Microsoft's quarterly earnings report has struck me as unwise for a long time. There are a few other less-used hosting providers, but switching to one would just be kicking the can down the road; those quarterly earnings reports will always have their day. In the long run the most stable home for my open-source projects is under my own domain name.

As of today, I'm self-hosting all my public git repos (that I still care about).

dev.mediocregopher.com is their new home.

If you visit the above link one of the first things you might notice (if you're used to GitHub and friends) is the complete lack of interactivity. You can view the code, the branches, the commits, and you can obtain a git clone URL (hint: it's the same as the browser URL). There is no issue tracker, merge requests, wiki, fork button, or any of the rest. There's not even an account system! Instead I've relegated all of that functionality into a simple sentence at the bottom of the page:

Please direct all issues, questions, and patches to me@mediocregopher.com.

Boom, easy, done. Let's be real: if you were using some of my code for something, and you wanted to report a problem to me, would you actually create a new one-off account on my fancy git forge in order to tell me? Or would you just email me? The answer is that you would just email me. I have a half-popular project on GitHub and even that one people just email me about. Email works fine, there's no need for more accounts on more websites.

Does this solution scale? Nope! If I have some project which becomes actually popular, and ends up requiring a real issue tracker and message boards and all the rest, will I have to prop something else up for that? Yes! But I'll solve that problem if and when it comes up, and keep my life simple in the meantime.

So how are these repos actually hosted? And how do I interact with them? They are simple bare clones which are sitting on a hardrive. The hardrive is backed up daily to Backblaze, so I'm not worried about data loss. As far as interacting with them, git is fairly flexible with how you define a remote repository, and it's easy enough to clone a repo by doing:

git clone ssh://user@host/path/to/repo/on/disk

Since I don't expose SSH publicly from my machines, and instead use a VPN between them all, this is the most secure option anyway. Adding in some monster of a codebase with its own authentication system and public SSH server would just be an increase in attack surface in exchange for probably less fine-grained access control than I have now. Overall this is much preferred.

As far as presenting my work to the world, while I've dismissed any full blown forge like GitLab, there were still quite a few to choose from:

The archived git wiki was a great starting point.

My selection process boiled down to a set of requirements:

In the end there were three apparent options. klaus is written in python, but the README says to not expect it to be fast, so I dismissed it. Ginatra is written in ruby, and looks alright, but hasn't seen any activity in 5 years. cgit is written in C (go figure) and is similarly derelict, but C doesn't age as badly as ruby, and there seems to be a mailing list where people have been posting patches long since the maintainer abandoned it. It's also very fast.

So I chose cgit, as you can see from the way it is. Setting up cgit ended up being a bit of a pain, mostly because it's been a while since I mucked around with CGI and I had some difficulty with it. I ended up using lighttpd to deal with CGI, and then reverse proxy from my normal HTTP(s) gateway to that. My lighttpd config looks like this:

    server.modules += ( "mod_cgi", "mod_rewrite", "mod_setenv" )

    server.bind = "${socketPath}"
    server.document-root = "${cgit}/cgit"
    setenv.set-environment = ( "CGIT_CONFIG" => "${cgitrc}" )

    server.errorlog = "${logPath}"
    dir-listing.activate = "disable"

    cgi.assign           = ( "cgit.cgi" => "" )
    mimetype.assign      = ( ".css" => "text/css" )
    url.rewrite-once     = (
        "^/cgit/cgit.css"   => "/cgit.css",
        "^/cgit/cgit.png"   => "/cgit.png",
        "^/([^?/]+/[^?]*)?(?:\?(.*))?$"   => "/cgit.cgi?url=$1&$2",
    )

Short, sweet, and completely unintelligible.

Configuring cgit itself was a bit of a chore. The config file is pretty well documented, but getting it to behave the way I wanted took some effort. I also had to take advantage of a patch from the mailing list to make the "About" page of each repo become the default, rather than the "Summary", as that just makes more sense to me (and whoever wrote the patch). In the end I was able to substitute out the default stylesheet for a slightly modified one of my own, and replace the headers and texts in various places to my liking. As far as actually reading and displaying the git repos there were no issues.

Overall I'm pretty happy with the final result, especially for something which took less than a weekend. There's a few nagging issues though. For one, cgit only does markdown->html conversion for the main README page, it won't do it for other markdown files within the repo, so I'll need to explore other solutions for documentation generation. I'm also not a _huge_ fan of the layout generally, especially how the clone URL is a bit difficult to find.

I'm happy with the result, but cgit definitely gives "old school C hacker" vibes in how it's configured and run. There's room in the space for a more modern alternative which does the same thing and is written in a less accident-prone language, perhaps with a more easily customizable frontend (pluggable themes??) and a builtin HTTP server. As always gemini support would be huge bonus.

Maybe one day I'll find the time to do it myself, but hopefully someone beats me to it!

-----

Published 2024-05-11