Files
nixpkgs/nixos/modules/services/networking/cgit.nix
T
Axel Karjalainen d634694bdd nixos/cgit: use alias option for assets
This makes it easier to replace an asset by just setting the Nginx
virtual host's `locations."= robots.txt".alias`. Previously you had to
either replace the extraConfig with mkForce or clear it and use the
`alias` option.
2025-07-29 15:20:24 +03:00

287 lines
8.1 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfgs = config.services.cgit;
settingType =
with lib.types;
oneOf [
bool
int
str
];
repeatedSettingType =
with lib.types;
oneOf [
settingType
(listOf settingType)
];
genAttrs' = names: f: lib.listToAttrs (map f names);
regexEscape =
let
# taken from https://github.com/python/cpython/blob/05cb728d68a278d11466f9a6c8258d914135c96c/Lib/re.py#L251-L266
special = [
"("
")"
"["
"]"
"{"
"}"
"?"
"*"
"+"
"-"
"|"
"^"
"$"
"\\"
"."
"&"
"~"
"#"
" "
"\t"
"\n"
"\r"
" " # \v / 0x0B
" " # \f / 0x0C
];
in
lib.replaceStrings special (map (c: "\\${c}") special);
stripLocation = cfg: lib.removeSuffix "/" cfg.nginx.location;
regexLocation = cfg: regexEscape (stripLocation cfg);
mkFastcgiPass = name: cfg: ''
${
if cfg.nginx.location == "/" then
''
fastcgi_param PATH_INFO $uri;
''
else
''
fastcgi_split_path_info ^(${regexLocation cfg})(/.+)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
''
}fastcgi_pass unix:${config.services.fcgiwrap.instances."cgit-${name}".socket.address};
'';
cgitrcLine =
name: value:
"${name}=${
if value == true then
"1"
else if value == false then
"0"
else
toString value
}";
# list value as multiple lines (for "readme" for example)
cgitrcEntry =
name: value: if lib.isList value then map (cgitrcLine name) value else [ (cgitrcLine name value) ];
mkCgitrc =
cfg:
pkgs.writeText "cgitrc" ''
# global settings
${lib.concatStringsSep "\n" (
lib.flatten (
lib.mapAttrsToList cgitrcEntry ({ virtual-root = cfg.nginx.location; } // cfg.settings)
)
)}
${lib.optionalString (cfg.scanPath != null) (cgitrcLine "scan-path" cfg.scanPath)}
# repository settings
${lib.concatStrings (
lib.mapAttrsToList (url: settings: ''
${cgitrcLine "repo.url" url}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: cgitrcLine "repo.${name}") settings)}
'') cfg.repos
)}
# extra config
${cfg.extraConfig}
'';
fcgiwrapUnitName = name: "fcgiwrap-cgit-${name}";
fcgiwrapRuntimeDir = name: "/run/${fcgiwrapUnitName name}";
gitProjectRoot =
name: cfg: if cfg.scanPath != null then cfg.scanPath else "${fcgiwrapRuntimeDir name}/repos";
in
{
options = {
services.cgit = lib.mkOption {
description = "Configure cgit instances.";
default = { };
type = lib.types.attrsOf (
lib.types.submodule (
{ config, ... }:
{
options = {
enable = lib.mkEnableOption "cgit";
package = lib.mkPackageOption pkgs "cgit" { };
nginx.virtualHost = lib.mkOption {
description = "VirtualHost to serve cgit on, defaults to the attribute name.";
type = lib.types.str;
default = config._module.args.name;
example = "git.example.com";
};
nginx.location = lib.mkOption {
description = "Location to serve cgit under.";
type = lib.types.str;
default = "/";
example = "/git/";
};
repos = lib.mkOption {
description = "cgit repository settings, see {manpage}`cgitrc(5)`";
type = with lib.types; attrsOf (attrsOf settingType);
default = { };
example = {
blah = {
path = "/var/lib/git/example";
desc = "An example repository";
};
};
};
scanPath = lib.mkOption {
description = "A path which will be scanned for repositories.";
type = lib.types.nullOr lib.types.path;
default = null;
example = "/var/lib/git";
};
settings = lib.mkOption {
description = "cgit configuration, see {manpage}`cgitrc(5)`";
type = lib.types.attrsOf repeatedSettingType;
default = { };
example = lib.literalExpression ''
{
enable-follow-links = true;
source-filter = "''${pkgs.cgit}/lib/cgit/filters/syntax-highlighting.py";
}
'';
};
extraConfig = lib.mkOption {
description = "These lines go to the end of cgitrc verbatim.";
type = lib.types.lines;
default = "";
};
user = lib.mkOption {
description = "User to run the cgit service as.";
type = lib.types.str;
default = "cgit";
};
group = lib.mkOption {
description = "Group to run the cgit service as.";
type = lib.types.str;
default = "cgit";
};
};
}
)
);
};
};
config = lib.mkIf (lib.any (cfg: cfg.enable) (lib.attrValues cfgs)) {
assertions = lib.mapAttrsToList (vhost: cfg: {
assertion = !cfg.enable || (cfg.scanPath == null) != (cfg.repos == { });
message = "Exactly one of services.cgit.${vhost}.scanPath or services.cgit.${vhost}.repos must be set.";
}) cfgs;
users = lib.mkMerge (
lib.flip lib.mapAttrsToList cfgs (
_: cfg: {
users.${cfg.user} = {
isSystemUser = true;
inherit (cfg) group;
};
groups.${cfg.group} = { };
}
)
);
services.fcgiwrap.instances = lib.flip lib.mapAttrs' cfgs (
name: cfg:
lib.nameValuePair "cgit-${name}" {
process = { inherit (cfg) user group; };
socket = { inherit (config.services.nginx) user group; };
}
);
systemd.services = lib.flip lib.mapAttrs' cfgs (
name: cfg:
lib.nameValuePair (fcgiwrapUnitName name) (
lib.mkIf (cfg.repos != { }) {
serviceConfig.RuntimeDirectory = fcgiwrapUnitName name;
preStart = ''
GIT_PROJECT_ROOT=${lib.escapeShellArg (gitProjectRoot name cfg)}
mkdir -p "$GIT_PROJECT_ROOT"
cd "$GIT_PROJECT_ROOT"
${lib.concatLines (
lib.flip lib.mapAttrsToList cfg.repos (
name: repo: ''
ln -s ${lib.escapeShellArg repo.path} ${lib.escapeShellArg name}
''
)
)}
'';
}
)
);
services.nginx.enable = true;
services.nginx.virtualHosts = lib.mkMerge (
lib.mapAttrsToList (name: cfg: {
${cfg.nginx.virtualHost} = {
locations =
(genAttrs' [ "cgit.css" "cgit.png" "favicon.ico" "robots.txt" ] (
fileName:
lib.nameValuePair "= ${stripLocation cfg}/${fileName}" {
alias = lib.mkDefault "${cfg.package}/cgit/${fileName}";
}
))
// {
"~ ${regexLocation cfg}/.+/(info/refs|git-upload-pack)" = {
fastcgiParams = rec {
SCRIPT_FILENAME = "${pkgs.git}/libexec/git-core/git-http-backend";
GIT_HTTP_EXPORT_ALL = "1";
GIT_PROJECT_ROOT = gitProjectRoot name cfg;
HOME = GIT_PROJECT_ROOT;
};
extraConfig = mkFastcgiPass name cfg;
};
"${stripLocation cfg}/" = {
fastcgiParams = {
SCRIPT_FILENAME = "${cfg.package}/cgit/cgit.cgi";
QUERY_STRING = "$args";
HTTP_HOST = "$server_name";
CGIT_CONFIG = mkCgitrc cfg;
};
extraConfig = mkFastcgiPass name cfg;
};
};
};
}) cfgs
);
};
}