mirror of
https://github.com/nix-community/home-manager.git
synced 2026-06-05 21:02:51 +00:00
This allows the git config to be used by programs that have git installed natively and where other programs access git but don't have the nix path available, so cannot access git-lfs.
649 lines
19 KiB
Nix
649 lines
19 KiB
Nix
{
|
||
config,
|
||
lib,
|
||
pkgs,
|
||
...
|
||
}:
|
||
let
|
||
inherit (lib)
|
||
concatStringsSep
|
||
mkDefault
|
||
mkEnableOption
|
||
mkIf
|
||
mkOption
|
||
mkOptionDefault
|
||
types
|
||
;
|
||
|
||
cfg = config.programs.git;
|
||
|
||
signingFormatStateVersionDefault = lib.hm.deprecations.mkStateVersionOptionDefault {
|
||
inherit (config.home) stateVersion;
|
||
since = "25.05";
|
||
optionPath = [
|
||
"programs"
|
||
"git"
|
||
"signing"
|
||
"format"
|
||
];
|
||
legacy.value = "openpgp";
|
||
current.value = null;
|
||
};
|
||
in
|
||
{
|
||
meta.maintainers = with lib.maintainers; [
|
||
khaneliman
|
||
rycee
|
||
];
|
||
|
||
options =
|
||
let
|
||
gitIniType =
|
||
with types;
|
||
let
|
||
primitiveType = either str (either bool int);
|
||
multipleType = either primitiveType (listOf primitiveType);
|
||
sectionType = attrsOf multipleType;
|
||
supersectionType = attrsOf (either multipleType sectionType);
|
||
in
|
||
attrsOf supersectionType;
|
||
in
|
||
{
|
||
programs.git = {
|
||
enable = mkEnableOption "Git";
|
||
|
||
package = lib.mkPackageOption pkgs "git" {
|
||
nullable = true;
|
||
example = "pkgs.gitFull";
|
||
extraDescription = ''
|
||
Use {var}`pkgs.gitFull`
|
||
to gain access to {command}`git send-email` for instance.
|
||
'';
|
||
};
|
||
|
||
signing = {
|
||
key = mkOption {
|
||
type = types.nullOr types.str;
|
||
default = null;
|
||
description = ''
|
||
The default signing key fingerprint.
|
||
|
||
Set to `null` to let the signer decide what signing key
|
||
to use depending on commit’s author.
|
||
'';
|
||
};
|
||
|
||
format = mkOption {
|
||
type = types.nullOr (
|
||
types.enum [
|
||
"openpgp"
|
||
"ssh"
|
||
"x509"
|
||
]
|
||
);
|
||
inherit (signingFormatStateVersionDefault) defaultText;
|
||
description = ''
|
||
The signing method to use when signing commits and tags.
|
||
Valid values are `openpgp` (OpenPGP/GnuPG), `ssh` (SSH), and `x509` (X.509 certificates).
|
||
'';
|
||
};
|
||
|
||
signByDefault = mkOption {
|
||
type = types.nullOr types.bool;
|
||
default = null;
|
||
description = "Whether commits and tags should be signed by default.";
|
||
};
|
||
|
||
signer = mkOption {
|
||
type = types.nullOr types.str;
|
||
description = "Path to signer binary to use.";
|
||
};
|
||
};
|
||
|
||
settings = mkOption {
|
||
type = with types; either gitIniType (listOf gitIniType);
|
||
default = { };
|
||
example = [
|
||
{
|
||
core = {
|
||
whitespace = "trailing-space,space-before-tab";
|
||
};
|
||
url."ssh://git@host".insteadOf = "otherhost";
|
||
}
|
||
{
|
||
credential."https://example.com".helper = "";
|
||
}
|
||
{
|
||
credential."https://example.com".helper = "oauth";
|
||
}
|
||
];
|
||
description = ''
|
||
Configuration written to {file}`$XDG_CONFIG_HOME/git/config`.
|
||
This may be either a single attrset of Git settings or an ordered
|
||
list of attrset fragments when repeated sections or explicit
|
||
ordering matter.
|
||
|
||
See {manpage}`git-config(1)` for details.
|
||
'';
|
||
};
|
||
|
||
hooks = mkOption {
|
||
type = types.attrsOf types.path;
|
||
default = { };
|
||
example = lib.literalExpression ''
|
||
{
|
||
pre-commit = ./pre-commit-script;
|
||
}
|
||
'';
|
||
description = ''
|
||
Configuration helper for Git hooks.
|
||
See <https://git-scm.com/docs/githooks>
|
||
for reference.
|
||
'';
|
||
};
|
||
|
||
iniContent = mkOption {
|
||
type = gitIniType;
|
||
internal = true;
|
||
};
|
||
|
||
ignores = mkOption {
|
||
type = types.listOf types.str;
|
||
default = [ ];
|
||
example = [
|
||
"*~"
|
||
"*.swp"
|
||
];
|
||
description = "List of paths that should be globally ignored.";
|
||
};
|
||
|
||
attributes = mkOption {
|
||
type = types.listOf types.str;
|
||
default = [ ];
|
||
example = [ "*.pdf diff=pdf" ];
|
||
description = "List of defining attributes set globally.";
|
||
};
|
||
|
||
includes = mkOption {
|
||
type = types.listOf (
|
||
types.submodule (
|
||
{ config, ... }:
|
||
{
|
||
options = {
|
||
condition = mkOption {
|
||
type = types.nullOr types.str;
|
||
default = null;
|
||
description = ''
|
||
Include this configuration only when {var}`condition`
|
||
matches. Allowed conditions are described in
|
||
{manpage}`git-config(1)`.
|
||
'';
|
||
};
|
||
|
||
path = mkOption {
|
||
type = with types; either str path;
|
||
description = "Path of the configuration file to include.";
|
||
};
|
||
|
||
contents = mkOption {
|
||
type = types.attrsOf types.anything;
|
||
default = { };
|
||
example = {
|
||
user = {
|
||
email = "bob@work.example.com";
|
||
name = "Bob Work";
|
||
signingKey = "1A2B3C4D5E6F7G8H";
|
||
};
|
||
commit = {
|
||
gpgSign = true;
|
||
};
|
||
};
|
||
description = ''
|
||
Configuration to include. If empty then a path must be given.
|
||
|
||
This follows the configuration structure as described in
|
||
{manpage}`git-config(1)`.
|
||
'';
|
||
};
|
||
|
||
contentSuffix = mkOption {
|
||
type = types.str;
|
||
default = "gitconfig";
|
||
description = ''
|
||
Nix store name for the git configuration text file,
|
||
when generating the configuration text from nix options.
|
||
'';
|
||
|
||
};
|
||
};
|
||
config.path = mkIf (config.contents != { }) (
|
||
mkDefault (
|
||
pkgs.writeText (lib.hm.strings.storeFileName config.contentSuffix) (
|
||
lib.generators.toGitINI config.contents
|
||
)
|
||
)
|
||
);
|
||
}
|
||
)
|
||
);
|
||
default = [ ];
|
||
example = [
|
||
{ path = "~/path/to/config.inc"; }
|
||
{
|
||
path = "~/path/to/conditional.inc";
|
||
condition = "gitdir:~/src/dir";
|
||
}
|
||
];
|
||
description = "List of configuration files to include.";
|
||
};
|
||
|
||
lfs = {
|
||
enable = mkEnableOption "Git Large File Storage";
|
||
|
||
package = lib.mkPackageOption pkgs "git-lfs" { nullable = true; };
|
||
|
||
skipSmudge = mkOption {
|
||
type = types.bool;
|
||
default = false;
|
||
description = ''
|
||
Skip automatic downloading of objects on clone or pull.
|
||
This requires a manual {command}`git lfs pull`
|
||
every time a new commit is checked out on your repository.
|
||
'';
|
||
};
|
||
};
|
||
|
||
maintenance = {
|
||
enable = mkEnableOption "" // {
|
||
description = ''
|
||
Enable the automatic {command}`git maintenance`.
|
||
|
||
If you have SSH remotes, set {option}`programs.git.package` to a
|
||
git version with SSH support (eg: `pkgs.gitFull`).
|
||
|
||
See <https://git-scm.com/docs/git-maintenance>.
|
||
'';
|
||
};
|
||
|
||
repositories = mkOption {
|
||
type = with types; listOf str;
|
||
default = [ ];
|
||
description = ''
|
||
Repositories on which {command}`git maintenance` should run.
|
||
|
||
Should be a list of absolute paths.
|
||
'';
|
||
};
|
||
|
||
timers = mkOption {
|
||
type = types.attrsOf types.str;
|
||
default = {
|
||
hourly = "*-*-* 1..23:53:00";
|
||
daily = "Tue..Sun *-*-* 0:53:00";
|
||
weekly = "Mon 0:53:00";
|
||
};
|
||
description = ''
|
||
Systemd timers to create for scheduled {command}`git maintenance`.
|
||
|
||
Key is passed to `--schedule` argument in {command}`git maintenance run`
|
||
and value is passed to `Timer.OnCalendar` in `systemd.user.timers`.
|
||
'';
|
||
};
|
||
};
|
||
};
|
||
};
|
||
|
||
imports =
|
||
let
|
||
oldPrefix = [
|
||
"programs"
|
||
"git"
|
||
];
|
||
newPrefix = [
|
||
"programs"
|
||
"git"
|
||
"settings"
|
||
];
|
||
in
|
||
[
|
||
(lib.mkRenamedOptionModule
|
||
[ "programs" "git" "signing" "gpgPath" ]
|
||
[
|
||
"programs"
|
||
"git"
|
||
"signing"
|
||
"signer"
|
||
]
|
||
)
|
||
(lib.mkRenamedOptionModule [ "programs" "git" "extraConfig" ] [ "programs" "git" "settings" ])
|
||
]
|
||
++ (lib.hm.deprecations.mkSettingsRenamedOptionModules oldPrefix newPrefix
|
||
{
|
||
transform = x: x;
|
||
}
|
||
[
|
||
{
|
||
old = [ "userName" ];
|
||
new = [
|
||
"user"
|
||
"name"
|
||
];
|
||
}
|
||
{
|
||
old = [ "userEmail" ];
|
||
new = [
|
||
"user"
|
||
"email"
|
||
];
|
||
}
|
||
{
|
||
old = [ "aliases" ];
|
||
new = [ "alias" ];
|
||
}
|
||
]
|
||
);
|
||
|
||
config = mkIf cfg.enable (
|
||
let
|
||
settingsFragments = lib.filter (fragment: fragment != { }) (
|
||
if builtins.isList cfg.settings then cfg.settings else [ ]
|
||
);
|
||
renderedIniFragments = lib.filter (text: lib.match "[[:space:]]*" text == null) (
|
||
[ (lib.generators.toGitINI cfg.iniContent) ] ++ map lib.generators.toGitINI settingsFragments
|
||
);
|
||
in
|
||
lib.mkMerge [
|
||
{
|
||
home.packages = lib.optionals (cfg.package != null) [ cfg.package ];
|
||
|
||
assertions = [
|
||
(
|
||
let
|
||
configOf =
|
||
{
|
||
name,
|
||
gitIntegrationOption ? [ "enableGitIntegration" ],
|
||
}:
|
||
{
|
||
name = "programs.${name}.${lib.concatStringsSep "." gitIntegrationOption}";
|
||
value =
|
||
config.programs.${name}.enable && lib.getAttrFromPath gitIntegrationOption config.programs.${name};
|
||
};
|
||
enabled = builtins.filter (x: x.value) (
|
||
map configOf [
|
||
{ name = "delta"; }
|
||
{ name = "diff-highlight"; }
|
||
{ name = "diff-so-fancy"; }
|
||
{
|
||
name = "difftastic";
|
||
gitIntegrationOption = [
|
||
"git"
|
||
"enable"
|
||
];
|
||
}
|
||
{ name = "patdiff"; }
|
||
{ name = "riff"; }
|
||
]
|
||
);
|
||
in
|
||
{
|
||
assertion = lib.length enabled <= 1;
|
||
message = ''
|
||
Only one of the following options can be enabled at a time.
|
||
- ${lib.concatStringsSep "\n - " (map (x: "`${x.name}'") enabled)}
|
||
'';
|
||
}
|
||
)
|
||
];
|
||
|
||
xdg.configFile = {
|
||
"git/config".text = concatStringsSep "\n" renderedIniFragments;
|
||
|
||
"git/ignore" = mkIf (cfg.ignores != [ ]) {
|
||
text = concatStringsSep "\n" cfg.ignores + "\n";
|
||
};
|
||
|
||
"git/attributes" = mkIf (cfg.attributes != [ ]) {
|
||
text = concatStringsSep "\n" cfg.attributes + "\n";
|
||
};
|
||
};
|
||
}
|
||
|
||
{
|
||
programs.git.iniContent =
|
||
let
|
||
hasSmtp = _name: account: account.enable && account.smtp != null;
|
||
|
||
genIdentity =
|
||
name: account:
|
||
let
|
||
inherit (account)
|
||
address
|
||
realName
|
||
smtp
|
||
userName
|
||
;
|
||
in
|
||
lib.nameValuePair "sendemail.${name}" (
|
||
if account.msmtp.enable then
|
||
{
|
||
sendmailCmd = lib.getExe config.programs.msmtp.package;
|
||
envelopeSender = "auto";
|
||
from = "${realName} <${address}>";
|
||
}
|
||
else
|
||
{
|
||
smtpEncryption =
|
||
if smtp.tls.enable then
|
||
(if smtp.tls.useStartTls || lib.versionOlder config.home.stateVersion "20.09" then "tls" else "ssl")
|
||
else
|
||
"";
|
||
smtpSslCertPath = mkIf smtp.tls.enable (toString smtp.tls.certificatesFile);
|
||
smtpServer = smtp.host;
|
||
smtpUser = userName;
|
||
from = "${realName} <${address}>";
|
||
}
|
||
// lib.optionalAttrs (smtp.port != null) {
|
||
smtpServerPort = smtp.port;
|
||
}
|
||
);
|
||
in
|
||
lib.mapAttrs' genIdentity (lib.filterAttrs hasSmtp config.accounts.email.accounts);
|
||
}
|
||
|
||
(mkIf (cfg.signing != { }) {
|
||
programs.git = {
|
||
signing = {
|
||
format = mkOptionDefault signingFormatStateVersionDefault.effectiveDefault;
|
||
signer =
|
||
let
|
||
defaultSigners = {
|
||
openpgp = lib.getExe config.programs.gpg.package;
|
||
ssh = lib.getExe' pkgs.openssh "ssh-keygen";
|
||
x509 = lib.getExe' config.programs.gpg.package "gpgsm";
|
||
};
|
||
in
|
||
mkIf (cfg.signing.format != null) (mkOptionDefault defaultSigners.${cfg.signing.format});
|
||
};
|
||
|
||
iniContent = lib.mkMerge [
|
||
(mkIf (cfg.signing.key != null) {
|
||
user.signingKey = mkDefault cfg.signing.key;
|
||
})
|
||
(mkIf (cfg.signing.signByDefault != null) {
|
||
commit.gpgSign = mkDefault cfg.signing.signByDefault;
|
||
tag.gpgSign = mkDefault cfg.signing.signByDefault;
|
||
})
|
||
(mkIf (cfg.signing.format != null) {
|
||
gpg = {
|
||
format = mkDefault cfg.signing.format;
|
||
${cfg.signing.format}.program = mkDefault cfg.signing.signer;
|
||
};
|
||
})
|
||
];
|
||
};
|
||
})
|
||
|
||
(mkIf (cfg.hooks != { }) {
|
||
programs.git.iniContent = {
|
||
core.hooksPath =
|
||
let
|
||
entries = lib.mapAttrsToList (name: path: { inherit name path; }) cfg.hooks;
|
||
in
|
||
toString (pkgs.linkFarm "git-hooks" entries);
|
||
};
|
||
})
|
||
|
||
(mkIf (!builtins.isList cfg.settings && cfg.settings != { }) {
|
||
programs.git.iniContent = cfg.settings;
|
||
})
|
||
|
||
(mkIf (cfg.includes != [ ]) {
|
||
xdg.configFile."git/config".text =
|
||
let
|
||
include =
|
||
i:
|
||
with i;
|
||
if condition != null then
|
||
{
|
||
includeIf.${condition}.path = "${path}";
|
||
}
|
||
else
|
||
{
|
||
include.path = "${path}";
|
||
};
|
||
in
|
||
lib.mkAfter (concatStringsSep "\n" (map lib.generators.toGitINI (map include cfg.includes)));
|
||
})
|
||
|
||
(mkIf cfg.lfs.enable {
|
||
home.packages = lib.mkIf (cfg.lfs.package != null) [ cfg.lfs.package ];
|
||
|
||
programs.git.iniContent.filter.lfs =
|
||
let
|
||
skipArg = lib.optional cfg.lfs.skipSmudge "--skip";
|
||
lfsPath = if cfg.lfs.package != null then lib.getExe cfg.lfs.package else "git-lfs";
|
||
in
|
||
{
|
||
clean = "${lfsPath} clean -- %f";
|
||
process = concatStringsSep " " (
|
||
[
|
||
lfsPath
|
||
"filter-process"
|
||
]
|
||
++ skipArg
|
||
);
|
||
required = true;
|
||
smudge = concatStringsSep " " (
|
||
[
|
||
lfsPath
|
||
"smudge"
|
||
]
|
||
++ skipArg
|
||
++ [
|
||
"--"
|
||
"%f"
|
||
]
|
||
);
|
||
};
|
||
})
|
||
|
||
(mkIf cfg.maintenance.enable {
|
||
programs.git.iniContent.maintenance.repo = cfg.maintenance.repositories;
|
||
|
||
systemd.user.services."git-maintenance@" = {
|
||
Unit = {
|
||
Description = "Optimize Git repositories data";
|
||
Documentation = [ "man:git-maintenance(1)" ];
|
||
};
|
||
|
||
Service = {
|
||
Type = "oneshot";
|
||
ExecStart =
|
||
let
|
||
exe = if cfg.package != null then lib.getExe cfg.package else "git";
|
||
in
|
||
''
|
||
"${exe}" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%i
|
||
'';
|
||
LockPersonality = "yes";
|
||
MemoryDenyWriteExecute = "yes";
|
||
NoNewPrivileges = "yes";
|
||
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_VSOCK";
|
||
RestrictNamespaces = "yes";
|
||
RestrictRealtime = "yes";
|
||
RestrictSUIDSGID = "yes";
|
||
SystemCallArchitectures = "native";
|
||
SystemCallFilter = "@system-service";
|
||
};
|
||
};
|
||
|
||
systemd.user.timers =
|
||
let
|
||
toSystemdTimer =
|
||
name: time:
|
||
lib.attrsets.nameValuePair "git-maintenance@${name}" {
|
||
Unit.Description = "Optimize Git repositories data";
|
||
|
||
Timer = {
|
||
OnCalendar = time;
|
||
Persistent = true;
|
||
};
|
||
|
||
Install.WantedBy = [ "timers.target" ];
|
||
};
|
||
in
|
||
lib.attrsets.mapAttrs' toSystemdTimer cfg.maintenance.timers;
|
||
|
||
launchd.agents =
|
||
let
|
||
baseArguments = [
|
||
"${if cfg.package != null then lib.getExe cfg.package else "git"}"
|
||
"for-each-repo"
|
||
"--keep-going"
|
||
"--config=maintenance.repo"
|
||
"maintenance"
|
||
"run"
|
||
];
|
||
in
|
||
{
|
||
"git-maintenance-hourly" = {
|
||
enable = true;
|
||
config = {
|
||
ProgramArguments = baseArguments ++ [ "--schedule=hourly" ];
|
||
StartCalendarInterval = map (hour: {
|
||
Hour = hour;
|
||
Minute = 53;
|
||
}) (lib.range 1 23);
|
||
};
|
||
};
|
||
"git-maintenance-daily" = {
|
||
enable = true;
|
||
config = {
|
||
ProgramArguments = baseArguments ++ [ "--schedule=daily" ];
|
||
StartCalendarInterval = map (weekday: {
|
||
Weekday = weekday;
|
||
Hour = 0;
|
||
Minute = 53;
|
||
}) (lib.range 1 6);
|
||
};
|
||
};
|
||
"git-maintenance-weekly" = {
|
||
enable = true;
|
||
config = {
|
||
ProgramArguments = baseArguments ++ [ "--schedule=weekly" ];
|
||
StartCalendarInterval = [
|
||
{
|
||
Weekday = 0;
|
||
Hour = 0;
|
||
Minute = 53;
|
||
}
|
||
];
|
||
};
|
||
};
|
||
};
|
||
})
|
||
]
|
||
);
|
||
}
|