aboutsummaryrefslogtreecommitdiff
path: root/nix/per-host/git/configuration.nix
diff options
context:
space:
mode:
authorKumar Damani <me@kumardamani.net>2026-08-02 01:43:25 +0000
committerKumar Damani <me@kumardamani.net>2026-08-02 01:43:25 +0000
commit43930cb8c22bc28ed64362845c5f4ebf54faabf2 (patch)
tree7351357bd38139b0267f1a4cdeff4b5143f5e89a /nix/per-host/git/configuration.nix
parent3e139a0e702513f6e1145158b2695b43c8251b92 (diff)
added git with cgit
Diffstat (limited to 'nix/per-host/git/configuration.nix')
-rw-r--r--nix/per-host/git/configuration.nix141
1 files changed, 141 insertions, 0 deletions
diff --git a/nix/per-host/git/configuration.nix b/nix/per-host/git/configuration.nix
new file mode 100644
index 0000000..2a2fa34
--- /dev/null
+++ b/nix/per-host/git/configuration.nix
@@ -0,0 +1,141 @@
+{ config, pkgs, modulesPath, lib, system, ... }:
+
+let
+ # Stock cgit.css from the package with upstream dark-mode overrides
+ # taken from https://git.zx2c4.com/cgit.css).
+ # Follows the browser's prefers-color-scheme, light or dark.
+ cgitCss = pkgs.writeText "cgit.css" (
+ builtins.readFile "${pkgs.cgit}/cgit/cgit.css"
+ + builtins.readFile ./cgit-dark.css
+ );
+
+ # The "manage" command is the default
+ gitShellCommands = ./git-shell-commands;
+in
+{
+ config = {
+ sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
+ sops.defaultSopsFile = ../../secrets/git.yaml;
+
+ networking.hostName = "git";
+ networking.firewall.allowedTCPPorts = [ 80 ];
+
+ services.cgit."git" = {
+ enable = true;
+ # Any bare repo dropped in /srv/git shows up in cgit automatically,
+ # but ONLY if it contains a git-daemon-export-ok marker file
+ # (private-by-default; see settings.strict-export and
+ # gitHttpBackend.checkExportOkFiles)
+ scanPath = "/srv/git";
+ gitHttpBackend.checkExportOkFiles = true;
+ settings = {
+ # repos without a git-daemon-export-ok file are neither browsable
+ # in cgit nor cloneable over HTTP (git-http-backend checks the same
+ # file via checkExportOkFiles above)
+ strict-export = "git-daemon-export-ok";
+ root-title = "My git browser";
+ root-desc = "git repositories";
+ repository-sort = "age";
+ enable-index-links = true;
+ remove-suffix = true;
+ # read gitweb.owner/description etc. from each repo's git config
+ enable-git-config = true;
+ clone-url = "https://git.kumardamani.net/$CGIT_REPO_URL git@git:$CGIT_REPO_URL";
+ robots = "noindex, nofollow";
+ # render the repo README on its About page (first match wins),
+ readme = [ ":README.md" ":readme.md" ":README.rst" ":README.txt" ":README" ];
+ about-filter = "${pkgs.cgit}/lib/cgit/filters/about-formatting.sh";
+ "mimetype.gif" = "image/gif";
+ "mimetype.png" = "image/png";
+ "mimetype.jpg" = "image/jpeg";
+ "mimetype.jpeg" = "image/jpeg";
+ "mimetype.webp" = "image/webp";
+ "mimetype.svg" = "image/svg+xml";
+ "mimetype.ico" = "image/x-icon";
+ "mimetype.pdf" = "application/pdf";
+ "mimetype.mp4" = "video/mp4";
+ "mimetype.webm" = "video/webm";
+ enable-commit-graph = true;
+ branch-sort = "age";
+ local-time = true;
+ source-filter = "${pkgs.cgit}/lib/cgit/filters/syntax-highlighting.py";
+ };
+ };
+
+ systemd.tmpfiles.rules = [
+ "d /srv/git 0755 git git -"
+ "Z /srv/git 0755 git git -"
+ # git-shell subcommands (see gitShellCommands above)
+ "d /srv/git/git-shell-commands 0755 git git -"
+ "L+ /srv/git/git-shell-commands/create - - - - ${gitShellCommands}/create"
+ "L+ /srv/git/git-shell-commands/publish - - - - ${gitShellCommands}/publish"
+ "L+ /srv/git/git-shell-commands/hide - - - - ${gitShellCommands}/hide"
+ "L+ /srv/git/git-shell-commands/desc - - - - ${gitShellCommands}/desc"
+ "L+ /srv/git/git-shell-commands/manage - - - - ${gitShellCommands}/manage"
+ # bare interactive ssh (ssh -t git@...) runs the manager instead of
+ # git-shell's "not enabled" error. git push/clone use -c, unaffected.
+ "L+ /srv/git/git-shell-commands/no-interactive-login - - - - ${gitShellCommands}/manage"
+ ];
+
+ users.users.git = {
+ isNormalUser = true;
+ group = "git";
+ home = "/srv/git"; # makes git@git:<repo>.git resolve to /srv/git/<repo>.git
+ createHome = false;
+ shell = "${pkgs.git}/bin/git-shell";
+ openssh.authorizedKeys.keys = [
+ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAYYjB+fK4JFlJSY6Ax48p38eSuDG12VQVqP+WSrWGuO"
+ ];
+ };
+ users.groups.git = { };
+
+ # Repos are owned by the git user; the cgit user (cgit.cgi /
+ # git-http-backend) only reads them. Exempt them from git's
+ # "dubious ownership" refusal. The templateDir gives every new repo
+ # the post-receive hook below.
+ environment.etc."gitconfig".text = ''
+ [safe]
+ directory = /srv/git/*
+ [init]
+ templateDir = /etc/git-template
+ '';
+
+ # Copy of the official hook:
+ # https://git.zx2c4.com/cgit/tree/contrib/hooks/post-receive.agefile
+ environment.etc."git-template/hooks/post-receive" = {
+ mode = "0755";
+ text = ''
+ #!/bin/sh
+ #
+ # An example hook to update the "agefile" for CGit's idle time calculation.
+ #
+ # This hook assumes that you are using the default agefile location of
+ # "info/web/last-modified". If you change the value in your cgitrc then you
+ # must also change it here.
+ #
+ # To install the hook, copy (or link) it to the file "hooks/post-receive" in
+ # each of your repositories.
+ #
+
+ agefile="$(git rev-parse --git-dir)"/info/web/last-modified
+
+ mkdir -p "$(dirname "$agefile")" &&
+ git for-each-ref \
+ --sort=-authordate --count=1 \
+ --format='%(authordate:iso8601)' \
+ >"$agefile"
+ '';
+ };
+
+ services.nginx.virtualHosts."git".locations."~ /.+/(git-receive-pack)" = {
+ return = "403";
+ };
+
+ # Serve the light+dark stylesheet instead of the package's light-only one
+ services.nginx.virtualHosts."git".locations."= /cgit.css".alias = cgitCss;
+
+ # Headroom for large git operations
+ swapDevices = [ { device = "/var/lib/swapfile"; size = 2048; } ];
+
+ };
+}