]> Witch of Git - web/blog/blob - posts/2020/hosting-my-own-git.md
Fix a typo in p-nickname
[web/blog] / posts / 2020 / hosting-my-own-git.md
1 ---
2 title: "Hosting My Own Git"
3 date: 2020-02-18 America/Detroit
4 tags:
5 - git
6 - web
7 summary: "I don't want to have to use GitHub for all my personal projects while they still support ICE, so I figure out how to do my own hosting."
8 ---
9
10 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.
11 Then, Jordan Rose made [a post about setting up personal Git hosting][gitweb shared] that I liked, and I learned how easy it was to set up, and about [GitWeb][].
12 So I set up my own shared Git hosting that suited my needs, which you can take a look at at [git.witchoflight.com][].
13
14 [supporting ice]: https://github.com/drop-ice/dear-github-2.0
15 [gitweb shared]: https://belkadan.com/blog/2020/01/Gitweb-on-Shared-Hosting/
16 [gitweb]: https://git-scm.com/docs/gitweb
17 [git.witchoflight.com]: https://git.witchoflight.com
18
19 ### Hosting *Your* Own Git
20
21 It turns out if you only care about SSH access, hosting Git is way easier than I believed it would be.
22 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.
23 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.
24 Then, it's just a matter of using `<user>@<server>:<path>` as your remote.
25 With that, your git and ssh handles all of the connections and transfer, you don't need a special Git server running or anything.
26
27 As a specific example of using this:
28 When I want a new repository on my server "Sunstone," I ssh there and create a new bare repository:
29
30 ```shell-session
31 $ ssh sunstone
32 $ su git
33 $ cd /repos/
34 $ git init --bare fancy-new-repo.git
35 ```
36
37 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.
38 Now I can clone it as `git clone git@sunstone:/repos/fancy-new-repo.git`.
39 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][].
40 There are other fancy things you can do to make it nicer, I'll get to some of those later.
41
42 [the git docs]: https://git-scm.com/book/en/v2/Git-on-the-Server-Setting-Up-the-Server
43
44 ### Sharing With The World
45
46 One of the big things that GitHub offers is the ability for anybody to easily read and clone the code without any particular authorization.
47 It turns out that Git itself offers a solution for this, called [GitWeb][].
48 It's a server that makes readable web pages for Git repositories, and as a side effect you can do plain HTTP(S) clones.
49
50 For this, I followed the setup directions [in the docs][gitweb] and in [Jordan's post][gitweb shared], as well as searching around online for various setups.
51 In the end, I put a few pieces together which were different from what I found.
52 My setup is a bit different from those because I'm on nginx.
53 Like Jordan, I want people to be able to use the nice URLs, and also to clone over HTTPS.
54
55 #### Configuring GitWeb
56
57 First, `/etc/gitweb.conf`:
58
59 ```perl
60 our $projectroot = "/repos";
61 our $site_name = 'Witch of Git';
62 our @git_base_url_list = qw(https://git.witchoflight.com);
63 our $omit_owner = true;
64 $feature{'highlight'}{'default'} = [1];
65 $feature{'pathinfo'}{'default'} = [1];
66 $feature{'search'}{'default'} = [0];
67
68 our %highlight_ext = (
69 %highlight_ext,
70 (map { $_ => $_ } qw(rs lua)),
71 toml => 'ini', # good enough for lazy :?
72 );
73
74 @stylesheets = ("/static/gitweb.css");
75 $logo = "/static/git-logo.png";
76 $favicon = "/static/git-favicon.png";
77 $javascript = "/static/gitweb.js";
78
79 $export_auth_hook = sub { not ($_[0] =~ /\.git\z/) };
80 ```
81
82 This does a few things.
83 First, some basic variables.
84 Where it should look for files, what the title should be, stuff like that.
85 I enable syntax highlighting, and importantly enable `pathinfo`, which changes URLs from messy query strings to readable URLs.
86 If you're not careful, this can cause trouble in the server config later, but I'll go over that.
87
88 I need to modify `%highlight_ext`, which specifies what file extensions map to which syntax highlighting.
89 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:
90
91 ```perl
92 # see files in /usr/share/highlight/langDefs/ directory
93 ```
94
95 Setting `pathinfo` requires that we specify the stylesheets, logo, favicon, and JS explicitly, for some reason.
96
97 The very last line is my own idea here.
98 The `$export_auth_hook` is a callback that checks if a given repo should be exported.
99 I create two copies of all my repos with a symlink, a `repo.git`, and a plain `repo` linked to the first.
100 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.
101 I use the hook here to avoid exporting two copies of each.
102
103 #### Nginx
104
105 I happen to like Nginx as a static file server.
106 I planned to have it already running on my server, so it seemed like the best idea to get GitWeb running through there.
107 Unfortunately, GitWeb is a CGI script, and Nginx doesn't directly support them.
108 I searched around on three different fronts.
109 One, tutorials for Nginx CGI scripts.
110 Another, tutorials for setting up GitWeb with other servers.
111 And also in the middle of those, tutorials actually on GitWeb with Nginx.
112 They didn't completely solve my issues, but I used [a tutorial from the Arch wiki][] and [a tutorial in a gist][].
113
114 Synthesizing all this together, I made my `/etc/nginx/sites-available/gitweb`:
115 ```nginx
116 server {
117 listen 80 default_server;
118 listen [::]:80 default_server;
119 server_name git.witchoflight.com;
120 root /repos/;
121
122 location / {
123 try_files $uri @gitweb;
124 }
125
126 location /static/ {
127 root /usr/share/gitweb/;
128 }
129
130 location @gitweb {
131 include fastcgi_params;
132 gzip off;
133 fastcgi_param SCRIPT_FILENAME /usr/share/gitweb/gitweb.cgi;
134 fastcgi_param PATH_INFO $uri;
135 fastcgi_param GITWEB_CONFIG /etc/gitweb.conf;
136 fastcgi_pass unix:/var/run/fcgiwrap.socket;
137 }
138 }
139 ```
140
141 This will try to route files at the root through GitWeb, and files under `/static/` to the filesystem.
142 GitWeb is set up as a fastcgi client, with fastcgi configured to run through `fcgiwrap.socket`.
143 I pass the request uri in the `PATH_INFO`, seems to be necessary to handle routing with the pretty paths.
144 As the absolute basics, you always have to pass the script filename, and the config.
145 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!
146 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.
147 Speaking of HTTPS...
148
149 [a tutorial from the arch wiki]: https://wiki.archlinux.org/index.php/Gitweb#Nginx
150 [a tutorial in a gist]: https://gist.github.com/mcxiaoke/055af99e86f8e8d3176e
151
152 #### Using HTTPS
153
154 As my final step, I got a cert set up for HTTPS.
155 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][letsencrypt] ended up being quite easy.
156 I just directly followed the steps on their "Get Started" page and things worked first try.
157 I pointed it at my GitWeb Nginx config and it slightly rewrote it to add certs and things, which I left out above.
158
159 [letsencrypt]: https://letsencrypt.org/
160
161 ### Making It Nice
162
163 I do *lots* of hobby projects, so I want it to be nice to set up repos.
164 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.
165
166 First, the `git-hooks/`.
167 I have a dedicated folder for them so the template can symlink that, so I don't need to edit all the different hooks.
168 Currently I just have one, the `post-update` hook.
169 It contains:
170
171 ```bash
172 #!/bin/sh
173 git update-server-info
174 git cat-file blob HEAD:README.md | pulldown-cmark > README.html
175 ```
176
177 We need to `git update-server-info` to prepare the data that HTTP clones and things need.
178 Then, we prepare the `README.html` file that GitWeb displays on the main page of a project.
179 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.
180
181 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.
182 I also change the default branch to `develop` since I prefer that instead of `master`.
183 You can set the template dir with `git config --global init.templatedir <path>`.
184
185 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.
186
187 ```bash
188 #!/bin/bash
189 set -e
190
191 if [ -z $1 ]; then
192 echo "Usage: make-repo <name>"
193 exit 0
194 fi
195 # Strip .git from the name if you specify it, for uniformity
196 repo=$(basename -s.git $1)
197 # We don't want to work as root, but we login as root (oops?)
198 # so it's convenient to automatically switch to the git user
199 if [ "$EUID" -eq 0 ]; then exec su git -c "$0 $1"; fi
200 # Only allow running as the git user, for file permissions purposes
201 if [ $(whoami) != "git" ]; then
202 echo "Please run this tool as the 'git' user"
203 exit 1
204 fi
205 # Don't overwrite existing repositories
206 if [ -d "/repos/$repo.git" ]; then
207 echo "Repository '$1' already exists"
208 exit 2
209 fi
210
211 cd /repos/
212 # Make a bare repo for SSH cloning
213 git init --bare "$repo.git"
214 # We immediately edit the description file that will show up in GitWeb
215 sensible-editor "$repo.git/description"
216 # And have both repo and repo.git point to the same place for cloning
217 ln -s "$repo.git" "$repo"
218 ```
219
220 ### What Now?
221
222 Well, now I have Git hosting!
223 And if you want some, you can probably figure it out too, it's less work than I thought it might be!
224 I've done some minimal styling, making it a narrower column and fixing some things, but I want to do more.
225 That's a project for another day, though.
226
227 Now, go look at my projects.
228 Some of them are cool!
229 I'll try to write about them soon. :)
230 [git.witchoflight.com][].