aboutsummaryrefslogtreecommitdiff
path: root/nix/per-host/git/configuration.nix
blob: 2a2fa347f58367675171ab74ad408c06dc15c699 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
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; } ];

  };
}