feat: add source control identity management

This commit is contained in:
2026-04-22 01:02:27 +02:00
parent be6ad78637
commit 86446fa797
10 changed files with 327 additions and 30 deletions
+3
View File
@@ -9,6 +9,7 @@
let let
user = config.meta.user; user = config.meta.user;
primaryEmail = builtins.head (lib.filter (email: email.primary) (builtins.attrValues user.emails)); primaryEmail = builtins.head (lib.filter (email: email.primary) (builtins.attrValues user.emails));
usesScopedIdentity = user != null && user.sourceControl.profiles != { };
in in
{ {
programs.git = { programs.git = {
@@ -20,6 +21,8 @@
]; ];
settings = { settings = {
init.defaultBranch = "main"; init.defaultBranch = "main";
}
// lib.optionalAttrs (!usesScopedIdentity) {
user = { user = {
name = user.realName; name = user.realName;
email = primaryEmail.address; email = primaryEmail.address;
+103 -10
View File
@@ -26,6 +26,69 @@ let
} }
); );
sourceControlHostKeyType = lib.types.submodule (
{ ... }:
{
options = {
publicKey = lib.mkOption {
type = lib.types.str;
};
privateKeyPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
};
};
}
);
sourceControlHostUserType = lib.types.submodule (
{ ... }:
{
options = {
personal = mkNullableOption sourceControlHostKeyType;
work = mkNullableOption sourceControlHostKeyType;
};
}
);
hostSourceControlType = lib.types.submodule (
{ ... }:
{
options.users = lib.mkOption {
type = lib.types.attrsOf sourceControlHostUserType;
default = { };
};
}
);
sourceControlProfileType = lib.types.submodule (
{ ... }:
{
options = { };
}
);
sourceControlType = lib.types.submodule (
{ ... }:
{
options = {
profiles = lib.mkOption {
type = lib.types.attrsOf sourceControlProfileType;
default = { };
};
projectScope = lib.mkOption {
type = lib.types.enum [
"personal"
"work"
];
default = "personal";
};
};
}
);
userType = lib.types.submodule ( userType = lib.types.submodule (
{ config, ... }: { config, ... }:
{ {
@@ -54,6 +117,11 @@ let
emails = lib.mkOption { emails = lib.mkOption {
type = lib.types.attrsOf emailType; type = lib.types.attrsOf emailType;
}; };
sourceControl = lib.mkOption {
type = sourceControlType;
default = { };
};
}; };
} }
); );
@@ -111,20 +179,28 @@ let
{ ... }: { ... }:
{ {
options = { options = {
accelProfile = mkNullableOption (lib.types.nullOr (lib.types.enum [ accelProfile = mkNullableOption (
lib.types.nullOr (
lib.types.enum [
"adaptive" "adaptive"
"flat" "flat"
])); ]
)
);
accelSpeed = mkNullableOption (lib.types.nullOr lib.types.float); accelSpeed = mkNullableOption (lib.types.nullOr lib.types.float);
leftHanded = mkNullableOption (lib.types.nullOr lib.types.bool); leftHanded = mkNullableOption (lib.types.nullOr lib.types.bool);
middleEmulation = mkNullableOption (lib.types.nullOr lib.types.bool); middleEmulation = mkNullableOption (lib.types.nullOr lib.types.bool);
naturalScrolling = mkNullableOption (lib.types.nullOr lib.types.bool); naturalScrolling = mkNullableOption (lib.types.nullOr lib.types.bool);
scrollMethod = mkNullableOption (lib.types.nullOr (lib.types.enum [ scrollMethod = mkNullableOption (
lib.types.nullOr (
lib.types.enum [
"no-scroll" "no-scroll"
"two-finger" "two-finger"
"edge" "edge"
"on-button-down" "on-button-down"
])); ]
)
);
}; };
} }
); );
@@ -133,25 +209,37 @@ let
{ ... }: { ... }:
{ {
options = { options = {
accelProfile = mkNullableOption (lib.types.nullOr (lib.types.enum [ accelProfile = mkNullableOption (
lib.types.nullOr (
lib.types.enum [
"adaptive" "adaptive"
"flat" "flat"
])); ]
)
);
accelSpeed = mkNullableOption (lib.types.nullOr lib.types.float); accelSpeed = mkNullableOption (lib.types.nullOr lib.types.float);
clickMethod = mkNullableOption (lib.types.nullOr (lib.types.enum [ clickMethod = mkNullableOption (
lib.types.nullOr (
lib.types.enum [
"button-areas" "button-areas"
"clickfinger" "clickfinger"
])); ]
)
);
disableWhileTyping = mkNullableOption (lib.types.nullOr lib.types.bool); disableWhileTyping = mkNullableOption (lib.types.nullOr lib.types.bool);
leftHanded = mkNullableOption (lib.types.nullOr lib.types.bool); leftHanded = mkNullableOption (lib.types.nullOr lib.types.bool);
middleEmulation = mkNullableOption (lib.types.nullOr lib.types.bool); middleEmulation = mkNullableOption (lib.types.nullOr lib.types.bool);
naturalScrolling = mkNullableOption (lib.types.nullOr lib.types.bool); naturalScrolling = mkNullableOption (lib.types.nullOr lib.types.bool);
scrollMethod = mkNullableOption (lib.types.nullOr (lib.types.enum [ scrollMethod = mkNullableOption (
lib.types.nullOr (
lib.types.enum [
"no-scroll" "no-scroll"
"two-finger" "two-finger"
"edge" "edge"
"on-button-down" "on-button-down"
])); ]
)
);
tapping = mkNullableOption (lib.types.nullOr lib.types.bool); tapping = mkNullableOption (lib.types.nullOr lib.types.bool);
}; };
} }
@@ -196,6 +284,11 @@ let
type = lib.types.attrsOf userType; type = lib.types.attrsOf userType;
default = { }; default = { };
}; };
sourceControl = lib.mkOption {
type = hostSourceControlType;
default = { };
};
}; };
} }
); );
+160
View File
@@ -0,0 +1,160 @@
{ config, lib, ... }:
let
homeModules = config.flake.modules.homeManager;
serviceHostNames = {
github = "github.com";
gitlab = "gitlab.com";
};
in
{
flake.modules.homeManager.source-control =
{
config,
lib,
...
}:
let
host = config.meta.host;
user = config.meta.user;
sourceControl = user.sourceControl;
hostSourceControlUsers = host.sourceControl.users;
hostUserSourceControl =
if lib.hasAttr user.name hostSourceControlUsers then hostSourceControlUsers.${user.name} else { };
profileNames = builtins.attrNames sourceControl.profiles;
parsedProfiles = map (
name:
let
matches = builtins.match "(github|gitlab)-(personal|work)" name;
in
{
inherit matches name;
isValid = matches != null;
scope = if matches == null then null else builtins.elemAt matches 1;
service = if matches == null then null else builtins.elemAt matches 0;
}
) profileNames;
validProfiles = builtins.filter (profile: profile.isValid) parsedProfiles;
invalidProfileNames = map (profile: profile.name) (
builtins.filter (profile: !profile.isValid) parsedProfiles
);
emailNamesForScope = {
personal = [
"personal"
"main"
];
work = [ "work" ];
};
scopeEmails =
scope:
map (name: user.emails.${name}) (
builtins.filter (name: lib.hasAttr name user.emails) emailNamesForScope.${scope}
);
emailForScope =
scope:
let
emails = scopeEmails scope;
in
if builtins.length emails == 1 then (builtins.head emails).address else null;
scopeConfig =
scope: if lib.hasAttr scope hostUserSourceControl then hostUserSourceControl.${scope} else null;
privateKeyPathForScope =
scope:
let
keyConfig = scopeConfig scope;
in
if keyConfig == null || keyConfig.privateKeyPath == null then
"~/.ssh/id_${scope}"
else
keyConfig.privateKeyPath;
scopePublicKey =
scope:
let
keyConfig = scopeConfig scope;
in
if keyConfig == null then null else keyConfig.publicKey;
scopesInUse = lib.unique (
[
"personal"
sourceControl.projectScope
]
++ map (profile: profile.scope) validProfiles
);
missingKeyScopes = builtins.filter (scope: scopePublicKey scope == null) scopesInUse;
invalidEmailScopes = builtins.filter (scope: emailForScope scope == null) scopesInUse;
allowedSignersLines = map (scope: "${emailForScope scope} ${scopePublicKey scope}") (
builtins.filter (scope: emailForScope scope != null && scopePublicKey scope != null) scopesInUse
);
gitConfigForScope = scope: {
gpg.ssh.allowedSignersFile = "${config.xdg.configHome}/git/allowed_signers";
user = {
name = user.realName;
email = emailForScope scope;
signingKey = "${privateKeyPathForScope scope}.pub";
};
};
gitRoots = [
{
root = user.nixosConfigurationPath;
scope = "personal";
}
{
root = config.xdg.userDirs.projects;
scope = sourceControl.projectScope;
}
];
in
{
imports = [ homeModules.git ];
assertions = [
{
assertion = invalidProfileNames == [ ];
message = "Invalid source control profiles for `${user.name}`: ${lib.concatStringsSep ", " invalidProfileNames}. Expected `<service>-<scope>` using github/gitlab and personal/work.";
}
{
assertion = missingKeyScopes == [ ];
message = "Missing source control keys for `${user.name}` scopes: ${lib.concatStringsSep ", " missingKeyScopes}.";
}
{
assertion = invalidEmailScopes == [ ];
message = "Expected exactly one email selected by name for `${user.name}` scopes: ${lib.concatStringsSep ", " invalidEmailScopes}. Personal uses `personal` or `main`; work uses `work`.";
}
];
xdg.configFile."git/allowed_signers".text = lib.concatStringsSep "\n" (
allowedSignersLines ++ [ "" ]
);
programs.git.includes = map (gitRoot: {
condition = "gitdir:${gitRoot.root}/";
contents = gitConfigForScope gitRoot.scope;
}) gitRoots;
programs.ssh = {
enable = true;
matchBlocks = lib.listToAttrs (
map (
profile:
lib.nameValuePair profile.name {
hostname = serviceHostNames.${profile.service};
identitiesOnly = true;
identityFile = privateKeyPathForScope profile.scope;
user = "git";
}
) validProfiles
);
};
};
}
+3 -3
View File
@@ -17,8 +17,7 @@ in
}; };
hasTerminalPackage = terminalPackage != null; hasTerminalPackage = terminalPackage != null;
hasMainProgram = hasTerminalPackage && terminalPackage ? meta.mainProgram; hasMainProgram = hasTerminalPackage && terminalPackage ? meta.mainProgram;
terminalDesktopId = terminalDesktopId = if hasMainProgram then "${terminalPackage.meta.mainProgram}.desktop" else null;
if hasMainProgram then "${terminalPackage.meta.mainProgram}.desktop" else null;
in in
{ {
assertions = [ assertions = [
@@ -31,7 +30,8 @@ in
message = "Terminal package `${lib.showAttrPath config.meta.user.terminalPackagePath}` must define `meta.mainProgram`."; message = "Terminal package `${lib.showAttrPath config.meta.user.terminalPackagePath}` must define `meta.mainProgram`.";
} }
{ {
assertion = hasMainProgram && builtins.pathExists "${terminalPackage}/share/applications/${terminalDesktopId}"; assertion =
hasMainProgram && builtins.pathExists "${terminalPackage}/share/applications/${terminalDesktopId}";
message = "Terminal package `${lib.showAttrPath config.meta.user.terminalPackagePath}` must provide `${terminalDesktopId}`."; message = "Terminal package `${lib.showAttrPath config.meta.user.terminalPackagePath}` must provide `${terminalDesktopId}`.";
} }
{ {
+1 -1
View File
@@ -35,7 +35,6 @@ in
homeModules.ai homeModules.ai
homeModules.clipboard homeModules.clipboard
homeModules.dev-tools homeModules.dev-tools
homeModules.git
homeModules.local-apps homeModules.local-apps
homeModules.mpv homeModules.mpv
homeModules.neovim homeModules.neovim
@@ -46,6 +45,7 @@ in
homeModules.podman homeModules.podman
homeModules.shell homeModules.shell
homeModules.sops homeModules.sops
homeModules.source-control
homeModules.ssh-client homeModules.ssh-client
homeModules.terminal homeModules.terminal
homeModules.theme homeModules.theme
+2 -1
View File
@@ -42,7 +42,8 @@ in
}; };
environment.systemPackages = [ environment.systemPackages = [
] ++ lib.optional (terminalPackage != null && lib.elem "terminfo" terminalPackage.outputs) ( ]
++ lib.optional (terminalPackage != null && lib.elem "terminfo" terminalPackage.outputs) (
lib.getOutput "terminfo" terminalPackage lib.getOutput "terminfo" terminalPackage
); );
}; };
+9
View File
@@ -29,6 +29,15 @@ in
mouse.accelSpeed = 0.4; mouse.accelSpeed = 0.4;
}; };
sourceControl.users = {
kiri.personal.publicKey = "";
ergon = {
personal.publicKey = "";
work.publicKey = "";
};
};
users = { users = {
inherit (metaLib.users) inherit (metaLib.users)
ergon ergon
+9
View File
@@ -30,6 +30,15 @@ in
mouse.accelSpeed = 0.4; mouse.accelSpeed = 0.4;
}; };
sourceControl.users = {
kiri.personal.publicKey = "";
ergon = {
personal.publicKey = "";
work.publicKey = "";
};
};
users = { users = {
inherit (metaLib.users) inherit (metaLib.users)
ergon ergon
+2
View File
@@ -9,6 +9,7 @@ let
name, name,
displays ? { }, displays ? { },
input ? { }, input ? { },
sourceControl ? { },
users ? { }, users ? { },
imports ? [ ], imports ? [ ],
stateVersion ? "24.05", stateVersion ? "24.05",
@@ -19,6 +20,7 @@ let
displays displays
input input
name name
sourceControl
users users
; ;
}; };
+21 -1
View File
@@ -6,7 +6,7 @@ let
homeDirectory = "/home/kiri"; homeDirectory = "/home/kiri";
terminalPackagePath = [ "kitty" ]; terminalPackagePath = [ "kitty" ];
emails = { emails = {
main = { personal = {
address = "mail@jelles.net"; address = "mail@jelles.net";
primary = true; primary = true;
type = "mxrouting"; type = "mxrouting";
@@ -27,6 +27,12 @@ let
type = "office365"; type = "office365";
}; };
}; };
sourceControl = {
profiles = {
github-personal = { };
gitlab-personal = { };
};
};
}; };
ergon = { ergon = {
@@ -35,12 +41,26 @@ let
homeDirectory = "/home/ergon"; homeDirectory = "/home/ergon";
terminalPackagePath = [ "kitty" ]; terminalPackagePath = [ "kitty" ];
emails = { emails = {
personal = {
address = "mail@jelles.net";
primary = false;
type = "mxrouting";
};
work = { work = {
address = "jelle.spreeuwenberg@yookr.org"; address = "jelle.spreeuwenberg@yookr.org";
primary = true; primary = true;
type = "office365"; type = "office365";
}; };
}; };
sourceControl = {
profiles = {
github-personal = { };
github-work = { };
gitlab-personal = { };
};
projectScope = "work";
};
}; };
in in
{ {