1{ config, lib, pkgs, ...}:
2
3with lib;
4
5let
6 cfgs = config.services.cgit;
7
8 settingType = with types; oneOf [ bool int str ];
9
10 genAttrs' = names: f: listToAttrs (map f names);
11
12 regexEscape =
13 let
14 # taken from https://github.com/python/cpython/blob/05cb728d68a278d11466f9a6c8258d914135c96c/Lib/re.py#L251-L266
15 special = [
16 "(" ")" "[" "]" "{" "}" "?" "*" "+" "-" "|" "^" "$" "\\" "." "&" "~"
17 "#" " " "\t" "\n" "\r" "\v" "\f"
18 ];
19 in
20 replaceStrings special (map (c: "\\${c}") special);
21
22 stripLocation = cfg: removeSuffix "/" cfg.nginx.location;
23
24 regexLocation = cfg: regexEscape (stripLocation cfg);
25
26 mkFastcgiPass = cfg: ''
27 ${if cfg.nginx.location == "/" then ''
28 fastcgi_param PATH_INFO $uri;
29 '' else ''
30 fastcgi_split_path_info ^(${regexLocation cfg})(/.+)$;
31 fastcgi_param PATH_INFO $fastcgi_path_info;
32 ''
33 }fastcgi_pass unix:${config.services.fcgiwrap.socketAddress};
34 '';
35
36 cgitrcLine = name: value: "${name}=${
37 if value == true then
38 "1"
39 else if value == false then
40 "0"
41 else
42 toString value
43 }";
44
45 mkCgitrc = cfg: pkgs.writeText "cgitrc" ''
46 # global settings
47 ${concatStringsSep "\n" (
48 mapAttrsToList
49 cgitrcLine
50 ({ virtual-root = cfg.nginx.location; } // cfg.settings)
51 )
52 }
53 ${optionalString (cfg.scanPath != null) (cgitrcLine "scan-path" cfg.scanPath)}
54
55 # repository settings
56 ${concatStrings (
57 mapAttrsToList
58 (url: settings: ''
59 ${cgitrcLine "repo.url" url}
60 ${concatStringsSep "\n" (
61 mapAttrsToList (name: cgitrcLine "repo.${name}") settings
62 )
63 }
64 '')
65 cfg.repos
66 )
67 }
68
69 # extra config
70 ${cfg.extraConfig}
71 '';
72
73 mkCgitReposDir = cfg:
74 if cfg.scanPath != null then
75 cfg.scanPath
76 else
77 pkgs.runCommand "cgit-repos" {
78 preferLocalBuild = true;
79 allowSubstitutes = false;
80 } ''
81 mkdir -p "$out"
82 ${
83 concatStrings (
84 mapAttrsToList
85 (name: value: ''
86 ln -s ${escapeShellArg value.path} "$out"/${escapeShellArg name}
87 '')
88 cfg.repos
89 )
90 }
91 '';
92
93in
94{
95 options = {
96 services.cgit = mkOption {
97 description = mdDoc "Configure cgit instances.";
98 default = {};
99 type = types.attrsOf (types.submodule ({ config, ... }: {
100 options = {
101 enable = mkEnableOption (mdDoc "cgit");
102
103 package = mkPackageOptionMD pkgs "cgit" {};
104
105 nginx.virtualHost = mkOption {
106 description = mdDoc "VirtualHost to serve cgit on, defaults to the attribute name.";
107 type = types.str;
108 default = config._module.args.name;
109 example = "git.example.com";
110 };
111
112 nginx.location = mkOption {
113 description = mdDoc "Location to serve cgit under.";
114 type = types.str;
115 default = "/";
116 example = "/git/";
117 };
118
119 repos = mkOption {
120 description = mdDoc "cgit repository settings, see cgitrc(5)";
121 type = with types; attrsOf (attrsOf settingType);
122 default = {};
123 example = {
124 blah = {
125 path = "/var/lib/git/example";
126 desc = "An example repository";
127 };
128 };
129 };
130
131 scanPath = mkOption {
132 description = mdDoc "A path which will be scanned for repositories.";
133 type = types.nullOr types.path;
134 default = null;
135 example = "/var/lib/git";
136 };
137
138 settings = mkOption {
139 description = mdDoc "cgit configuration, see cgitrc(5)";
140 type = types.attrsOf settingType;
141 default = {};
142 example = literalExpression ''
143 {
144 enable-follow-links = true;
145 source-filter = "''${pkgs.cgit}/lib/cgit/filters/syntax-highlighting.py";
146 }
147 '';
148 };
149
150 extraConfig = mkOption {
151 description = mdDoc "These lines go to the end of cgitrc verbatim.";
152 type = types.lines;
153 default = "";
154 };
155 };
156 }));
157 };
158 };
159
160 config = mkIf (any (cfg: cfg.enable) (attrValues cfgs)) {
161 assertions = mapAttrsToList (vhost: cfg: {
162 assertion = !cfg.enable || (cfg.scanPath == null) != (cfg.repos == {});
163 message = "Exactly one of services.cgit.${vhost}.scanPath or services.cgit.${vhost}.repos must be set.";
164 }) cfgs;
165
166 services.fcgiwrap.enable = true;
167
168 services.nginx.enable = true;
169
170 services.nginx.virtualHosts = mkMerge (mapAttrsToList (_: cfg: {
171 ${cfg.nginx.virtualHost} = {
172 locations = (
173 genAttrs'
174 [ "cgit.css" "cgit.png" "favicon.ico" "robots.txt" ]
175 (name: nameValuePair "= ${stripLocation cfg}/${name}" {
176 extraConfig = ''
177 alias ${cfg.package}/cgit/${name};
178 '';
179 })
180 ) // {
181 "~ ${regexLocation cfg}/.+/(info/refs|git-upload-pack)" = {
182 fastcgiParams = rec {
183 SCRIPT_FILENAME = "${pkgs.git}/libexec/git-core/git-http-backend";
184 GIT_HTTP_EXPORT_ALL = "1";
185 GIT_PROJECT_ROOT = mkCgitReposDir cfg;
186 HOME = GIT_PROJECT_ROOT;
187 };
188 extraConfig = mkFastcgiPass cfg;
189 };
190 "${stripLocation cfg}/" = {
191 fastcgiParams = {
192 SCRIPT_FILENAME = "${cfg.package}/cgit/cgit.cgi";
193 QUERY_STRING = "$args";
194 HTTP_HOST = "$server_name";
195 CGIT_CONFIG = mkCgitrc cfg;
196 };
197 extraConfig = mkFastcgiPass cfg;
198 };
199 };
200 };
201 }) cfgs);
202 };
203}