feat: add source control identity management
This commit is contained in:
@@ -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
@@ -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 = { };
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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}`.";
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user