#!/bin/bash # # script/setup-git-config [--check] # # Render templates/git-config.tmpl into ~/.config/git/config. Per-host values # live in a standard Git config file at # ~/.config/local/git/identity.gitconfig; # Git itself reads and writes those values and injects them into a temporary # copy of the template. The completed file is moved into place atomically. # # Also renders ~/.config/local/git/allowed_signers from that same identity. # config.darwin points gpg.ssh.allowedSignersFile at it; without it Git signs # normally but cannot verify its own commits. # # Codespaces use the same renderer without a local identity: the injected # ~/.gitconfig supplies identity and credentials, signing is disabled, and the # macOS-only include is removed. set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" GIT_TEMPLATE="$REPO_ROOT/templates/git-config.tmpl" GIT_CONFIG="$HOME/.config/git/config" IDENTITY_DIR="$HOME/.config/local/git" IDENTITY_FILE="$IDENTITY_DIR/identity.gitconfig" ALLOWED_SIGNERS="$IDENTITY_DIR/allowed_signers" EXTRA_SIGNERS="$IDENTITY_DIR/allowed_signers.extra" LEGACY_IDENTITY_DIR="$HOME/.config/dotfiles" LEGACY_GIT_IDENTITY_FILE="$LEGACY_IDENTITY_DIR/identity.gitconfig" LEGACY_ENV_IDENTITY_FILE="$LEGACY_IDENTITY_DIR/identity.env" LEGACY_ENV_BACKUP="$IDENTITY_DIR/identity.env.migrated" MODE="write" tmp="" case "${1:-}" in "") ;; --check) MODE="check" ;; *) echo "usage: script/setup-git-config [--check]" >&2 exit 2 ;; esac trap '[ -z "$tmp" ] || rm -f "$tmp"' EXIT write_identity() { local output=$1 email=$2 key=$3 ( umask 077 : > "$output" git config --file "$output" user.email "$email" git config --file "$output" user.signingkey "$key" ) chmod 600 "$output" } validate_signing_key() { case "$1" in ssh-* | sk-ssh-* | sk-ecdsa-* | ecdsa-*) return 0 ;; *) echo "identity.gitconfig user.signingkey must be an SSH public key" >&2 echo " expected a value such as ssh-ed25519 AAAA..." >&2 return 1 ;; esac } migrate_git_identity() { local email key email=$(git config --file "$LEGACY_GIT_IDENTITY_FILE" --get user.email) \ || { echo "legacy identity.gitconfig missing user.email" >&2; return 1; } key=$(git config --file "$LEGACY_GIT_IDENTITY_FILE" --get user.signingkey) \ || { echo "legacy identity.gitconfig missing user.signingkey" >&2; return 1; } [ -n "$email" ] || { echo "legacy identity.gitconfig has empty user.email" >&2; return 1; } [ -n "$key" ] || { echo "legacy identity.gitconfig has empty user.signingkey" >&2; return 1; } mv "$LEGACY_GIT_IDENTITY_FILE" "$IDENTITY_FILE" chmod 600 "$IDENTITY_FILE" echo "Moved $LEGACY_GIT_IDENTITY_FILE to $IDENTITY_FILE" } archive_legacy_env() { if [ -f "$LEGACY_ENV_IDENTITY_FILE" ] && [ ! -e "$LEGACY_ENV_BACKUP" ]; then mv "$LEGACY_ENV_IDENTITY_FILE" "$LEGACY_ENV_BACKUP" chmod 600 "$LEGACY_ENV_BACKUP" echo "Archived $LEGACY_ENV_IDENTITY_FILE at $LEGACY_ENV_BACKUP" fi rmdir "$LEGACY_IDENTITY_DIR" 2>/dev/null || true } migrate_env_identity() { local email key email=$(awk -F= '$1=="EMAIL"{sub(/^EMAIL=/,""); print; exit}' "$LEGACY_ENV_IDENTITY_FILE") key=$(awk -F= '$1=="SIGNINGKEY"{sub(/^SIGNINGKEY=/,""); print; exit}' "$LEGACY_ENV_IDENTITY_FILE") [ -n "$email" ] || { echo "legacy identity.env missing EMAIL" >&2; return 1; } [ -n "$key" ] || { echo "legacy identity.env missing SIGNINGKEY" >&2; return 1; } tmp=$(mktemp "$IDENTITY_FILE.XXXXXX") write_identity "$tmp" "$email" "$key" mv "$tmp" "$IDENTITY_FILE" tmp="" echo "Migrated $LEGACY_ENV_IDENTITY_FILE to $IDENTITY_FILE" } create_identity() { local email key if [ ! -t 0 ]; then echo "setup-git-config: no identity file and no TTY to prompt on" >&2 echo " create $IDENTITY_FILE with user.email and user.signingkey" >&2 return 1 fi echo "First-time git identity setup..." printf "Email: " IFS= read -r email [ -n "$email" ] || { echo "email is required" >&2; return 1; } printf "SSH signing public key: " IFS= read -r key [ -n "$key" ] || { echo "signing key is required" >&2; return 1; } validate_signing_key "$key" tmp=$(mktemp "$IDENTITY_FILE.XXXXXX") write_identity "$tmp" "$email" "$key" mv "$tmp" "$IDENTITY_FILE" tmp="" echo "Wrote $IDENTITY_FILE" } render_config() { local output=$1 email key cp "$GIT_TEMPLATE" "$output" if [ -n "${CODESPACES:-}" ]; then git config --file "$output" --remove-section user git config --file "$output" commit.gpgsign false git config --file "$output" --remove-section include return fi email=$(git config --file "$IDENTITY_FILE" --get user.email) \ || { echo "identity.gitconfig missing user.email" >&2; return 1; } key=$(git config --file "$IDENTITY_FILE" --get user.signingkey) \ || { echo "identity.gitconfig missing user.signingkey" >&2; return 1; } validate_signing_key "$key" git config --file "$output" user.email "$email" git config --file "$output" user.signingkey "$key" } # Render the allowed_signers file that gpg.ssh.allowedSignersFile points at. # Signing and verifying are separate halves of SSH signing; config.darwin turns # on the second one, and this supplies its data. Identity is per-host, so this # is generated next to identity.gitconfig rather than stowed from the repo. render_allowed_signers() { local output=$1 email key email=$(git config --file "$IDENTITY_FILE" --get user.email) \ || { echo "identity.gitconfig missing user.email" >&2; return 1; } key=$(git config --file "$IDENTITY_FILE" --get user.signingkey) \ || { echo "identity.gitconfig missing user.signingkey" >&2; return 1; } # render_config has already rejected keys that are not SSH public keys. printf '%s %s\n' "$email" "$key" > "$output" # Other people's keys go in allowed_signers.extra, which is untracked and # never rewritten — this file is regenerated on every render, so hand-added # entries here would be silently dropped on the next `make regen-git-config`. if [ -f "$EXTRA_SIGNERS" ]; then cat "$EXTRA_SIGNERS" >> "$output" fi } if [ "$MODE" = check ]; then [ -f "$GIT_CONFIG" ] || exit 1 if [ -z "${CODESPACES:-}" ] && [ ! -f "$IDENTITY_FILE" ]; then exit 1 fi tmp=$(mktemp "${TMPDIR:-/tmp}/dotfiles-git-config.XXXXXX") render_config "$tmp" cmp -s "$tmp" "$GIT_CONFIG" || exit 1 rm -f "$tmp" tmp="" if [ -z "${CODESPACES:-}" ]; then [ -f "$ALLOWED_SIGNERS" ] || exit 1 tmp=$(mktemp "${TMPDIR:-/tmp}/dotfiles-allowed-signers.XXXXXX") render_allowed_signers "$tmp" cmp -s "$tmp" "$ALLOWED_SIGNERS" || exit 1 fi exit 0 fi mkdir -p "$HOME/.config/git" if [ -z "${CODESPACES:-}" ]; then mkdir -p "$IDENTITY_DIR" chmod 700 "$IDENTITY_DIR" if [ ! -f "$IDENTITY_FILE" ]; then if [ -f "$LEGACY_GIT_IDENTITY_FILE" ]; then migrate_git_identity elif [ -f "$LEGACY_ENV_IDENTITY_FILE" ]; then migrate_env_identity else create_identity fi fi archive_legacy_env fi tmp=$(mktemp "$GIT_CONFIG.XXXXXX") render_config "$tmp" mv "$tmp" "$GIT_CONFIG" tmp="" if [ -z "${CODESPACES:-}" ]; then tmp=$(mktemp "$ALLOWED_SIGNERS.XXXXXX") render_allowed_signers "$tmp" chmod 600 "$tmp" mv "$tmp" "$ALLOWED_SIGNERS" tmp="" fi if [ -n "${CODESPACES:-}" ]; then echo "Wrote $GIT_CONFIG (Codespaces mode — identity from ~/.gitconfig)" else echo "Wrote $GIT_CONFIG" fi