1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.gitolite;
9 # Use writeTextDir to not leak Nix store hash into file name
10 pubkeyFile = (pkgs.writeTextDir "gitolite-admin.pub" cfg.adminPubkey) + "/gitolite-admin.pub";
11 hooks = lib.concatMapStrings (hook: "${hook} ") cfg.commonHooks;
12in
13{
14 options = {
15 services.gitolite = {
16 enable = lib.mkOption {
17 type = lib.types.bool;
18 default = false;
19 description = ''
20 Enable gitolite management under the
21 `gitolite` user. After
22 switching to a configuration with Gitolite enabled, you can
23 then run `git clone gitolite@host:gitolite-admin.git` to manage it further.
24 '';
25 };
26
27 dataDir = lib.mkOption {
28 type = lib.types.str;
29 default = "/var/lib/gitolite";
30 description = ''
31 The gitolite home directory used to store all repositories. If left as the default value
32 this directory will automatically be created before the gitolite server starts, otherwise
33 the sysadmin is responsible for ensuring the directory exists with appropriate ownership
34 and permissions.
35 '';
36 };
37
38 adminPubkey = lib.mkOption {
39 type = lib.types.str;
40 description = ''
41 Initial administrative public key for Gitolite. This should
42 be an SSH Public Key. Note that this key will only be used
43 once, upon the first initialization of the Gitolite user.
44 The key string cannot have any line breaks in it.
45 '';
46 };
47
48 enableGitAnnex = lib.mkOption {
49 type = lib.types.bool;
50 default = false;
51 description = ''
52 Enable git-annex support. Uses the `extraGitoliteRc` option
53 to apply the necessary configuration.
54 '';
55 };
56
57 commonHooks = lib.mkOption {
58 type = lib.types.listOf lib.types.path;
59 default = [ ];
60 description = ''
61 A list of custom git hooks that get copied to `~/.gitolite/hooks/common`.
62 '';
63 };
64
65 extraGitoliteRc = lib.mkOption {
66 type = lib.types.lines;
67 default = "";
68 example = lib.literalExpression ''
69 '''
70 $RC{UMASK} = 0027;
71 $RC{SITE_INFO} = 'This is our private repository host';
72 push( @{$RC{ENABLE}}, 'Kindergarten' ); # enable the command/feature
73 @{$RC{ENABLE}} = grep { $_ ne 'desc' } @{$RC{ENABLE}}; # disable the command/feature
74 '''
75 '';
76 description = ''
77 Extra configuration to append to the default `~/.gitolite.rc`.
78
79 This should be Perl code that modifies the `%RC`
80 configuration variable. The default `~/.gitolite.rc`
81 content is generated by invoking `gitolite print-default-rc`,
82 and extra configuration from this option is appended to it. The result
83 is placed to Nix store, and the `~/.gitolite.rc` file
84 becomes a symlink to it.
85
86 If you already have a customized (or otherwise changed)
87 `~/.gitolite.rc` file, NixOS will refuse to replace
88 it with a symlink, and the `gitolite-init` initialization service
89 will fail. In this situation, in order to use this option, you
90 will need to take any customizations you may have in
91 `~/.gitolite.rc`, convert them to appropriate Perl
92 statements, add them to this option, and remove the file.
93
94 See also the `enableGitAnnex` option.
95 '';
96 };
97
98 user = lib.mkOption {
99 type = lib.types.str;
100 default = "gitolite";
101 description = ''
102 Gitolite user account. This is the username of the gitolite endpoint.
103 '';
104 };
105
106 description = lib.mkOption {
107 type = lib.types.str;
108 default = "Gitolite user";
109 description = ''
110 Gitolite user account's description.
111 '';
112 };
113
114 group = lib.mkOption {
115 type = lib.types.str;
116 default = "gitolite";
117 description = ''
118 Primary group of the Gitolite user account.
119 '';
120 };
121 };
122 };
123
124 config = lib.mkIf cfg.enable (
125 let
126 manageGitoliteRc = cfg.extraGitoliteRc != "";
127 rcDir = pkgs.runCommand "gitolite-rc" { preferLocalBuild = true; } rcDirScript;
128 rcDirScript = ''
129 mkdir "$out"
130 export HOME=temp-home
131 mkdir -p "$HOME/.gitolite/logs" # gitolite can't run without it
132 '${pkgs.gitolite}'/bin/gitolite print-default-rc >>"$out/gitolite.rc.default"
133 cat <<END >>"$out/gitolite.rc"
134 # This file is managed by NixOS.
135 # Use services.gitolite options to control it.
136
137 END
138 cat "$out/gitolite.rc.default" >>"$out/gitolite.rc"
139 ''
140 + lib.optionalString (cfg.extraGitoliteRc != "") ''
141 echo -n ${lib.escapeShellArg ''
142
143 # Added by NixOS:
144 ${lib.removeSuffix "\n" cfg.extraGitoliteRc}
145
146 # per perl rules, this should be the last line in such a file:
147 1;
148 ''} >>"$out/gitolite.rc"
149 '';
150 in
151 {
152 services.gitolite.extraGitoliteRc = lib.optionalString cfg.enableGitAnnex ''
153 # Enable git-annex support:
154 push( @{$RC{ENABLE}}, 'git-annex-shell ua');
155 '';
156
157 users.users.${cfg.user} = {
158 description = cfg.description;
159 home = cfg.dataDir;
160 uid = config.ids.uids.gitolite;
161 group = cfg.group;
162 useDefaultShell = true;
163 };
164 users.groups.${cfg.group}.gid = config.ids.gids.gitolite;
165
166 systemd.services.gitolite-init = {
167 description = "Gitolite initialization";
168 wantedBy = [ "multi-user.target" ];
169 unitConfig.RequiresMountsFor = cfg.dataDir;
170
171 environment = {
172 GITOLITE_RC = ".gitolite.rc";
173 GITOLITE_RC_DEFAULT = "${rcDir}/gitolite.rc.default";
174 };
175
176 serviceConfig = lib.mkMerge [
177 (lib.mkIf (cfg.dataDir == "/var/lib/gitolite") {
178 StateDirectory = "gitolite gitolite/.gitolite gitolite/.gitolite/logs";
179 StateDirectoryMode = "0750";
180 })
181 {
182 Type = "oneshot";
183 User = cfg.user;
184 Group = cfg.group;
185 WorkingDirectory = "~";
186 RemainAfterExit = true;
187 }
188 ];
189
190 path = [
191 pkgs.gitolite
192 pkgs.git
193 pkgs.perl
194 pkgs.bash
195 pkgs.diffutils
196 config.programs.ssh.package
197 ];
198 script =
199 let
200 rcSetupScriptIfCustomFile =
201 if manageGitoliteRc then
202 ''
203 cat <<END
204 <3>ERROR: NixOS can't apply declarative configuration
205 <3>to your .gitolite.rc file, because it seems to be
206 <3>already customized manually.
207 <3>See the services.gitolite.extraGitoliteRc option
208 <3>in "man configuration.nix" for more information.
209 END
210 # Not sure if the line below addresses the issue directly or just
211 # adds a delay, but without it our error message often doesn't
212 # show up in `systemctl status gitolite-init`.
213 journalctl --flush
214 exit 1
215 ''
216 else
217 ''
218 :
219 '';
220 rcSetupScriptIfDefaultFileOrStoreSymlink =
221 if manageGitoliteRc then
222 ''
223 ln -sf "${rcDir}/gitolite.rc" "$GITOLITE_RC"
224 ''
225 else
226 ''
227 [[ -L "$GITOLITE_RC" ]] && rm -f "$GITOLITE_RC"
228 '';
229 in
230 ''
231 if ( [[ ! -e "$GITOLITE_RC" ]] && [[ ! -L "$GITOLITE_RC" ]] ) ||
232 ( [[ -f "$GITOLITE_RC" ]] && diff -q "$GITOLITE_RC" "$GITOLITE_RC_DEFAULT" >/dev/null ) ||
233 ( [[ -L "$GITOLITE_RC" ]] && [[ "$(readlink "$GITOLITE_RC")" =~ ^/nix/store/ ]] )
234 then
235 ''
236 + rcSetupScriptIfDefaultFileOrStoreSymlink
237 + ''
238 else
239 ''
240 + rcSetupScriptIfCustomFile
241 + ''
242 fi
243
244 if [ ! -d repositories ]; then
245 gitolite setup -pk ${pubkeyFile}
246 fi
247 if [ -n "${hooks}" ]; then
248 cp -f ${hooks} .gitolite/hooks/common/
249 chmod +x .gitolite/hooks/common/*
250 fi
251 gitolite setup # Upgrade if needed
252 '';
253 };
254
255 environment.systemPackages = [
256 pkgs.gitolite
257 pkgs.git
258 ]
259 ++ lib.optional cfg.enableGitAnnex pkgs.git-annex;
260 }
261 );
262}