1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfgs = config.services.cgit;
9
10 settingType =
11 with lib.types;
12 oneOf [
13 bool
14 int
15 str
16 ];
17 repeatedSettingType =
18 with lib.types;
19 oneOf [
20 settingType
21 (listOf settingType)
22 ];
23
24 genAttrs' = names: f: lib.listToAttrs (map f names);
25
26 regexEscape =
27 let
28 # taken from https://github.com/python/cpython/blob/05cb728d68a278d11466f9a6c8258d914135c96c/Lib/re.py#L251-L266
29 special = [
30 "("
31 ")"
32 "["
33 "]"
34 "{"
35 "}"
36 "?"
37 "*"
38 "+"
39 "-"
40 "|"
41 "^"
42 "$"
43 "\\"
44 "."
45 "&"
46 "~"
47 "#"
48 " "
49 "\t"
50 "\n"
51 "\r"
52 "" # \v / 0x0B
53 "" # \f / 0x0C
54 ];
55 in
56 lib.replaceStrings special (map (c: "\\${c}") special);
57
58 stripLocation = cfg: lib.removeSuffix "/" cfg.nginx.location;
59
60 regexLocation = cfg: regexEscape (stripLocation cfg);
61
62 mkFastcgiPass = name: cfg: ''
63 ${
64 if cfg.nginx.location == "/" then
65 ''
66 fastcgi_param PATH_INFO $uri;
67 ''
68 else
69 ''
70 fastcgi_split_path_info ^(${regexLocation cfg})(/.+)$;
71 fastcgi_param PATH_INFO $fastcgi_path_info;
72 ''
73 }fastcgi_pass unix:${config.services.fcgiwrap.instances."cgit-${name}".socket.address};
74 '';
75
76 cgitrcLine =
77 name: value:
78 "${name}=${
79 if value == true then
80 "1"
81 else if value == false then
82 "0"
83 else
84 toString value
85 }";
86
87 # list value as multiple lines (for "readme" for example)
88 cgitrcEntry =
89 name: value: if lib.isList value then map (cgitrcLine name) value else [ (cgitrcLine name value) ];
90
91 mkCgitrc =
92 cfg:
93 pkgs.writeText "cgitrc" ''
94 # global settings
95 ${lib.concatStringsSep "\n" (
96 lib.flatten (
97 lib.mapAttrsToList cgitrcEntry ({ virtual-root = cfg.nginx.location; } // cfg.settings)
98 )
99 )}
100 ${lib.optionalString (cfg.scanPath != null) (cgitrcLine "scan-path" cfg.scanPath)}
101
102 # repository settings
103 ${lib.concatStrings (
104 lib.mapAttrsToList (url: settings: ''
105 ${cgitrcLine "repo.url" url}
106 ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: cgitrcLine "repo.${name}") settings)}
107 '') cfg.repos
108 )}
109
110 # extra config
111 ${cfg.extraConfig}
112 '';
113
114 fcgiwrapUnitName = name: "fcgiwrap-cgit-${name}";
115 fcgiwrapRuntimeDir = name: "/run/${fcgiwrapUnitName name}";
116 gitProjectRoot =
117 name: cfg: if cfg.scanPath != null then cfg.scanPath else "${fcgiwrapRuntimeDir name}/repos";
118
119in
120{
121 options = {
122 services.cgit = lib.mkOption {
123 description = "Configure cgit instances.";
124 default = { };
125 type = lib.types.attrsOf (
126 lib.types.submodule (
127 { config, ... }:
128 {
129 options = {
130 enable = lib.mkEnableOption "cgit";
131
132 package = lib.mkPackageOption pkgs "cgit" { };
133
134 nginx.virtualHost = lib.mkOption {
135 description = "VirtualHost to serve cgit on, defaults to the attribute name.";
136 type = lib.types.str;
137 default = config._module.args.name;
138 example = "git.example.com";
139 };
140
141 nginx.location = lib.mkOption {
142 description = "Location to serve cgit under.";
143 type = lib.types.str;
144 default = "/";
145 example = "/git/";
146 };
147
148 repos = lib.mkOption {
149 description = "cgit repository settings, see {manpage}`cgitrc(5)`";
150 type = with lib.types; attrsOf (attrsOf settingType);
151 default = { };
152 example = {
153 blah = {
154 path = "/var/lib/git/example";
155 desc = "An example repository";
156 };
157 };
158 };
159
160 scanPath = lib.mkOption {
161 description = "A path which will be scanned for repositories.";
162 type = lib.types.nullOr lib.types.path;
163 default = null;
164 example = "/var/lib/git";
165 };
166
167 settings = lib.mkOption {
168 description = "cgit configuration, see {manpage}`cgitrc(5)`";
169 type = lib.types.attrsOf repeatedSettingType;
170 default = { };
171 example = lib.literalExpression ''
172 {
173 enable-follow-links = true;
174 source-filter = "''${pkgs.cgit}/lib/cgit/filters/syntax-highlighting.py";
175 }
176 '';
177 };
178
179 extraConfig = lib.mkOption {
180 description = "These lines go to the end of cgitrc verbatim.";
181 type = lib.types.lines;
182 default = "";
183 };
184
185 user = lib.mkOption {
186 description = "User to run the cgit service as.";
187 type = lib.types.str;
188 default = "cgit";
189 };
190
191 group = lib.mkOption {
192 description = "Group to run the cgit service as.";
193 type = lib.types.str;
194 default = "cgit";
195 };
196 };
197 }
198 )
199 );
200 };
201 };
202
203 config = lib.mkIf (lib.any (cfg: cfg.enable) (lib.attrValues cfgs)) {
204 assertions = lib.mapAttrsToList (vhost: cfg: {
205 assertion = !cfg.enable || (cfg.scanPath == null) != (cfg.repos == { });
206 message = "Exactly one of services.cgit.${vhost}.scanPath or services.cgit.${vhost}.repos must be set.";
207 }) cfgs;
208
209 users = lib.mkMerge (
210 lib.flip lib.mapAttrsToList cfgs (
211 _: cfg: {
212 users.${cfg.user} = {
213 isSystemUser = true;
214 inherit (cfg) group;
215 };
216 groups.${cfg.group} = { };
217 }
218 )
219 );
220
221 services.fcgiwrap.instances = lib.flip lib.mapAttrs' cfgs (
222 name: cfg:
223 lib.nameValuePair "cgit-${name}" {
224 process = { inherit (cfg) user group; };
225 socket = { inherit (config.services.nginx) user group; };
226 }
227 );
228
229 systemd.services = lib.flip lib.mapAttrs' cfgs (
230 name: cfg:
231 lib.nameValuePair (fcgiwrapUnitName name) (
232 lib.mkIf (cfg.repos != { }) {
233 serviceConfig.RuntimeDirectory = fcgiwrapUnitName name;
234 preStart = ''
235 GIT_PROJECT_ROOT=${lib.escapeShellArg (gitProjectRoot name cfg)}
236 mkdir -p "$GIT_PROJECT_ROOT"
237 cd "$GIT_PROJECT_ROOT"
238 ${lib.concatLines (
239 lib.flip lib.mapAttrsToList cfg.repos (
240 name: repo: ''
241 ln -s ${lib.escapeShellArg repo.path} ${lib.escapeShellArg name}
242 ''
243 )
244 )}
245 '';
246 }
247 )
248 );
249
250 services.nginx.enable = true;
251
252 services.nginx.virtualHosts = lib.mkMerge (
253 lib.mapAttrsToList (name: cfg: {
254 ${cfg.nginx.virtualHost} = {
255 locations =
256 (genAttrs' [ "cgit.css" "cgit.png" "favicon.ico" "robots.txt" ] (
257 fileName:
258 lib.nameValuePair "= ${stripLocation cfg}/${fileName}" {
259 extraConfig = ''
260 alias ${cfg.package}/cgit/${fileName};
261 '';
262 }
263 ))
264 // {
265 "~ ${regexLocation cfg}/.+/(info/refs|git-upload-pack)" = {
266 fastcgiParams = rec {
267 SCRIPT_FILENAME = "${pkgs.git}/libexec/git-core/git-http-backend";
268 GIT_HTTP_EXPORT_ALL = "1";
269 GIT_PROJECT_ROOT = gitProjectRoot name cfg;
270 HOME = GIT_PROJECT_ROOT;
271 };
272 extraConfig = mkFastcgiPass name cfg;
273 };
274 "${stripLocation cfg}/" = {
275 fastcgiParams = {
276 SCRIPT_FILENAME = "${cfg.package}/cgit/cgit.cgi";
277 QUERY_STRING = "$args";
278 HTTP_HOST = "$server_name";
279 CGIT_CONFIG = mkCgitrc cfg;
280 };
281 extraConfig = mkFastcgiPass name cfg;
282 };
283 };
284 };
285 }) cfgs
286 );
287 };
288}