Files
dotfiles/modules/applications/git/home.nix
T
2026-06-25 15:18:53 +09:00

206 lines
5.0 KiB
Nix

{
pkgs,
lib,
config,
...
}:
let
cfg = config.my.applications.git;
hmCfg = config.my.applications.git.homeManager;
sshCfg = config.my.applications.ssh;
gitSshSigningKeyCommand = pkgs.writeShellScript "git-ssh-signing-key" ''
set -u
expand_path() {
case "$1" in
"~")
printf '%s\n' "$HOME"
;;
"~/"*)
printf '%s\n' "$HOME/''${1#"~/"}"
;;
*)
printf '%s\n' "$1"
;;
esac
}
fido_present() {
${pkgs.libfido2}/bin/fido2-token -L 2>/dev/null \
| ${pkgs.gnugrep}/bin/grep -q .
}
is_ssh_public_key() {
case "$1" in
ssh-*|ecdsa-*|sk-*)
return 0
;;
*)
return 1
;;
esac
}
is_fido_public_key() {
case "$1" in
sk-*)
return 0
;;
*)
return 1
;;
esac
}
print_git_key() {
key="$1"
is_ssh_public_key "$key" || return 1
printf 'key::%s\n' "$key"
exit 0
}
pubkey_file_for_identity() {
identity_file="$(expand_path "$1")"
case "$identity_file" in
*.pub)
printf '%s\n' "$identity_file"
;;
*)
printf '%s.pub\n' "$identity_file"
;;
esac
}
print_pubkey_file_as_git_key() {
public_key_file="$1"
[ -r "$public_key_file" ] || return 1
IFS= read -r key < "$public_key_file" || return 1
[ -n "$key" ] || return 1
print_git_key "$key"
}
print_first_usable_agent_key() {
[ -n "''${SSH_AUTH_SOCK:-}" ] || return 1
[ -S "$SSH_AUTH_SOCK" ] || return 1
has_fido=0
if fido_present; then
has_fido=1
fi
${pkgs.openssh}/bin/ssh-add -L 2>/dev/null \
| while IFS= read -r key; do
is_ssh_public_key "$key" || continue
# YubiKey が無いときに agent に残っている sk-* 鍵を選ぶと、
# Git 署名時に "agent refused operation" になる。
if is_fido_public_key "$key" && [ "$has_fido" -ne 1 ]; then
continue
fi
print_git_key "$key"
done
}
add_identity_to_agent_and_print_pubkey() {
identity_file="$(expand_path "$1")"
public_key_file="$(pubkey_file_for_identity "$1")"
[ -r "$identity_file" ] || return 1
[ -r "$public_key_file" ] || return 1
[ -n "''${SSH_AUTH_SOCK:-}" ] || return 1
[ -S "$SSH_AUTH_SOCK" ] || return 1
${pkgs.openssh}/bin/ssh-add -q "$identity_file" >/dev/null 2>&1 || return 1
print_pubkey_file_as_git_key "$public_key_file"
}
# 1. agent にすでにある鍵を優先する。
# ただし YubiKey が無い場合、stale な sk-* 鍵は無視する。
print_first_usable_agent_key || true
# 2. YubiKey が刺さっている場合だけ _sk_rk を追加して使う。
if fido_present; then
add_identity_to_agent_and_print_pubkey ${lib.escapeShellArg sshCfg.fidoIdentityFile} || true
fi
# 3. 最後に通常のローカル鍵を agent に追加して使う。
${lib.concatMapStringsSep "\n" (
identityFile: "add_identity_to_agent_and_print_pubkey ${lib.escapeShellArg identityFile} || true"
) sshCfg.defaultIdentityFiles}
printf '%s\n' "git-ssh-signing-key: no usable SSH signing key found" >&2
exit 1
'';
in
{
options.my.applications.git.homeManager = {
enable = lib.mkEnableOption "git home-manager configuration";
};
config = lib.mkIf hmCfg.enable {
home-manager.sharedModules = [
{
programs.git = {
enable = true;
ignores = [
".direnv/"
".envrc"
"!.envrc.example"
];
settings = {
user.name = cfg.userName;
user.email = cfg.userEmail;
push.default = "simple";
credential.helper = "cache --timeout=7200";
init.defaultBranch = "main";
log.decorate = "full";
log.date = "iso";
merge.conflictStyle = "diff3";
# SSH signing
gpg.format = "ssh";
# Git の SSH signing backend はデフォルトでも ssh-keygen だが、
# store path に固定して PATH 依存を避ける。
gpg.ssh.program = "${pkgs.openssh}/bin/ssh-keygen";
# user.signingKey は固定しない。
# 署名時にこの command が key::ssh-ed25519 ... を返す。
gpg.ssh.defaultKeyCommand = "${gitSshSigningKeyCommand}";
commit.gpgSign = true;
tag.gpgSign = true;
alias = {
br = "branch --sort=-committerdate";
co = "checkout";
df = "diff";
com = "commit -a";
gs = "stash";
gp = "pull";
lg = "log --graph --pretty=format:'%Cred%h%Creset - %C(yellow)%d%Creset %s %C(green)(%cr)%C(bold blue) <%an>%Creset' --abbrev-commit";
st = "status";
};
};
};
}
];
};
}