1 { config, lib, pkgs, ...}:
3 cfgs = config.services.cgit;
5 settingType = with lib.types; oneOf [ bool int str ];
6 repeatedSettingType = with lib.types; oneOf [ settingType (listOf settingType) ];
8 genAttrs' = names: f: lib.listToAttrs (map f names);
12 # taken from https://github.com/python/cpython/blob/05cb728d68a278d11466f9a6c8258d914135c96c/Lib/re.py#L251-L266
14 "(" ")" "[" "]" "{" "}" "?" "*" "+" "-" "|" "^" "$" "\\" "." "&" "~"
15 "#" " " "\t" "\n" "\r"
20 lib.replaceStrings special (map (c: "\\${c}") special);
22 stripLocation = cfg: lib.removeSuffix "/" cfg.nginx.location;
24 regexLocation = cfg: regexEscape (stripLocation cfg);
26 mkFastcgiPass = name: cfg: ''
27 ${if cfg.nginx.location == "/" then ''
28 fastcgi_param PATH_INFO $uri;
30 fastcgi_split_path_info ^(${regexLocation cfg})(/.+)$;
31 fastcgi_param PATH_INFO $fastcgi_path_info;
33 }fastcgi_pass unix:${config.services.fcgiwrap.instances."cgit-${name}".socket.address};
36 cgitrcLine = name: value: "${name}=${
39 else if value == false then
45 # list value as multiple lines (for "readme" for example)
46 cgitrcEntry = name: value:
47 if lib.isList value then
48 map (cgitrcLine name) value
50 [ (cgitrcLine name value) ];
52 mkCgitrc = cfg: pkgs.writeText "cgitrc" ''
54 ${lib.concatStringsSep "\n" (
55 lib.flatten (lib.mapAttrsToList
57 ({ virtual-root = cfg.nginx.location; } // cfg.settings)
61 ${lib.optionalString (cfg.scanPath != null) (cgitrcLine "scan-path" cfg.scanPath)}
67 ${cgitrcLine "repo.url" url}
68 ${lib.concatStringsSep "\n" (
69 lib.mapAttrsToList (name: cgitrcLine "repo.${name}") settings
81 fcgiwrapUnitName = name: "fcgiwrap-cgit-${name}";
82 fcgiwrapRuntimeDir = name: "/run/${fcgiwrapUnitName name}";
83 gitProjectRoot = name: cfg: if cfg.scanPath != null
85 else "${fcgiwrapRuntimeDir name}/repos";
90 services.cgit = lib.mkOption {
91 description = "Configure cgit instances.";
93 type = lib.types.attrsOf (lib.types.submodule ({ config, ... }: {
95 enable = lib.mkEnableOption "cgit";
97 package = lib.mkPackageOption pkgs "cgit" {};
99 nginx.virtualHost = lib.mkOption {
100 description = "VirtualHost to serve cgit on, defaults to the attribute name.";
101 type = lib.types.str;
102 default = config._module.args.name;
103 example = "git.example.com";
106 nginx.location = lib.mkOption {
107 description = "Location to serve cgit under.";
108 type = lib.types.str;
113 repos = lib.mkOption {
114 description = "cgit repository settings, see cgitrc(5)";
115 type = with lib.types; attrsOf (attrsOf settingType);
119 path = "/var/lib/git/example";
120 desc = "An example repository";
125 scanPath = lib.mkOption {
126 description = "A path which will be scanned for repositories.";
127 type = lib.types.nullOr lib.types.path;
129 example = "/var/lib/git";
132 settings = lib.mkOption {
133 description = "cgit configuration, see cgitrc(5)";
134 type = lib.types.attrsOf repeatedSettingType;
136 example = lib.literalExpression ''
138 enable-follow-links = true;
139 source-filter = "''${pkgs.cgit}/lib/cgit/filters/syntax-highlighting.py";
144 extraConfig = lib.mkOption {
145 description = "These lines go to the end of cgitrc verbatim.";
146 type = lib.types.lines;
150 user = lib.mkOption {
151 description = "User to run the cgit service as.";
152 type = lib.types.str;
156 group = lib.mkOption {
157 description = "Group to run the cgit service as.";
158 type = lib.types.str;
166 config = lib.mkIf (lib.any (cfg: cfg.enable) (lib.attrValues cfgs)) {
167 assertions = lib.mapAttrsToList (vhost: cfg: {
168 assertion = !cfg.enable || (cfg.scanPath == null) != (cfg.repos == {});
169 message = "Exactly one of services.cgit.${vhost}.scanPath or services.cgit.${vhost}.repos must be set.";
172 users = lib.mkMerge (lib.flip lib.mapAttrsToList cfgs (_: cfg: {
173 users.${cfg.user} = {
177 groups.${cfg.group} = { };
180 services.fcgiwrap.instances = lib.flip lib.mapAttrs' cfgs (name: cfg:
181 lib.nameValuePair "cgit-${name}" {
182 process = { inherit (cfg) user group; };
183 socket = { inherit (config.services.nginx) user group; };
187 systemd.services = lib.flip lib.mapAttrs' cfgs (name: cfg:
188 lib.nameValuePair (fcgiwrapUnitName name)
189 (lib.mkIf (cfg.repos != { }) {
190 serviceConfig.RuntimeDirectory = fcgiwrapUnitName name;
192 GIT_PROJECT_ROOT=${lib.escapeShellArg (gitProjectRoot name cfg)}
193 mkdir -p "$GIT_PROJECT_ROOT"
194 cd "$GIT_PROJECT_ROOT"
195 ${lib.concatLines (lib.flip lib.mapAttrsToList cfg.repos (name: repo: ''
196 ln -s ${lib.escapeShellArg repo.path} ${lib.escapeShellArg name}
202 services.nginx.enable = true;
204 services.nginx.virtualHosts = lib.mkMerge (lib.mapAttrsToList (name: cfg: {
205 ${cfg.nginx.virtualHost} = {
208 [ "cgit.css" "cgit.png" "favicon.ico" "robots.txt" ]
209 (fileName: lib.nameValuePair "= ${stripLocation cfg}/${fileName}" {
211 alias ${cfg.package}/cgit/${fileName};
215 "~ ${regexLocation cfg}/.+/(info/refs|git-upload-pack)" = {
216 fastcgiParams = rec {
217 SCRIPT_FILENAME = "${pkgs.git}/libexec/git-core/git-http-backend";
218 GIT_HTTP_EXPORT_ALL = "1";
219 GIT_PROJECT_ROOT = gitProjectRoot name cfg;
220 HOME = GIT_PROJECT_ROOT;
222 extraConfig = mkFastcgiPass name cfg;
224 "${stripLocation cfg}/" = {
226 SCRIPT_FILENAME = "${cfg.package}/cgit/cgit.cgi";
227 QUERY_STRING = "$args";
228 HTTP_HOST = "$server_name";
229 CGIT_CONFIG = mkCgitrc cfg;
231 extraConfig = mkFastcgiPass name cfg;