feat: add SSH commit signing to git identities

Configure commit signing per Git identity
Apply SSH signing settings automatically
Support signing in web and VS Code
This commit is contained in:
Bohdan Triapitsyn
2026-06-18 23:51:40 +03:00
parent 07e6935a3e
commit 4d63278efd
18 changed files with 198 additions and 13 deletions
@@ -1,5 +1,6 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { toast } from '@/components/ui';
import {
@@ -66,6 +67,8 @@ export const GitIdentityEditorDialog: React.FC<GitIdentityEditorDialogProps> = (
const [userEmail, setUserEmail] = React.useState('');
const [authType, setAuthType] = React.useState<GitIdentityAuthType>('ssh');
const [sshKey, setSshKey] = React.useState('');
const [signCommits, setSignCommits] = React.useState(false);
const [signingKey, setSigningKey] = React.useState('');
const [host, setHost] = React.useState('');
const [color, setColor] = React.useState('keyword');
const [icon, setIcon] = React.useState('branch');
@@ -83,6 +86,8 @@ export const GitIdentityEditorDialog: React.FC<GitIdentityEditorDialogProps> = (
setUserEmail('');
setAuthType('token');
setSshKey('');
setSignCommits(false);
setSigningKey('');
setHost(importData.host);
setColor('string');
setIcon('code');
@@ -92,6 +97,8 @@ export const GitIdentityEditorDialog: React.FC<GitIdentityEditorDialogProps> = (
setUserEmail('');
setAuthType('ssh');
setSshKey('');
setSignCommits(false);
setSigningKey('');
setHost('');
setColor('keyword');
setIcon('branch');
@@ -101,6 +108,8 @@ export const GitIdentityEditorDialog: React.FC<GitIdentityEditorDialogProps> = (
setUserEmail(selectedProfile.userEmail);
setAuthType(selectedProfile.authType || 'ssh');
setSshKey(selectedProfile.sshKey || '');
setSignCommits(selectedProfile.signCommits === true);
setSigningKey(selectedProfile.signingKey || '');
setHost(selectedProfile.host || '');
setColor(selectedProfile.color || 'keyword');
setIcon(selectedProfile.icon || 'branch');
@@ -112,6 +121,8 @@ export const GitIdentityEditorDialog: React.FC<GitIdentityEditorDialogProps> = (
setUserEmail(global.userEmail);
setAuthType(global.authType || 'ssh');
setSshKey(global.sshKey || '');
setSignCommits(false);
setSigningKey('');
setHost(global.host || '');
setColor(global.color || 'keyword');
setIcon(global.icon || 'branch');
@@ -128,6 +139,10 @@ export const GitIdentityEditorDialog: React.FC<GitIdentityEditorDialogProps> = (
toast.error(t('settings.gitIdentities.editor.toast.hostRequiredForToken'));
return;
}
if (signCommits && !signingKey.trim()) {
toast.error(t('settings.gitIdentities.editor.toast.signingKeyRequired'));
return;
}
setIsSaving(true);
try {
@@ -137,6 +152,8 @@ export const GitIdentityEditorDialog: React.FC<GitIdentityEditorDialogProps> = (
userEmail: userEmail.trim(),
authType,
sshKey: authType === 'ssh' ? (sshKey.trim() || null) : null,
signCommits,
signingKey: signingKey.trim() || null,
host: authType === 'token' ? (host.trim() || null) : null,
color,
icon,
@@ -382,6 +399,39 @@ export const GitIdentityEditorDialog: React.FC<GitIdentityEditorDialogProps> = (
</div>
)}
<div className="border-t border-border/40 pt-3 space-y-3">
<div className="flex items-start gap-2">
<Checkbox
checked={signCommits}
onChange={setSignCommits}
ariaLabel={t('settings.gitIdentities.editor.field.signCommits')}
/>
<div className="min-w-0">
<div className="typography-ui-label text-foreground">
{t('settings.gitIdentities.editor.field.signCommits')}
</div>
<div className="typography-micro text-muted-foreground/70">
{t('settings.gitIdentities.editor.section.commitSigning')}
</div>
</div>
</div>
<div>
<div className="flex items-center gap-1.5 mb-1.5">
<label className="typography-ui-label text-foreground">
{t('settings.gitIdentities.editor.field.signingKey')}
</label>
</div>
<Input
value={signingKey}
onChange={(e) => setSigningKey(e.target.value)}
placeholder={t('settings.gitIdentities.editor.field.signingKeyPlaceholder')}
disabled={!signCommits}
className="h-8 font-mono text-xs"
/>
</div>
</div>
{authType === 'token' && (
<div>
<div className="flex items-center gap-1.5 mb-1.5">
+2
View File
@@ -296,6 +296,8 @@ export interface GitIdentityProfile {
userEmail: string;
authType?: GitIdentityAuthType;
sshKey?: string | null;
signCommits?: boolean;
signingKey?: string | null;
host?: string | null;
color?: string | null;
icon?: string | null;
@@ -533,6 +533,7 @@ export const settingsDict = {
'settings.gitIdentities.editor.description.globalReadOnly': 'System-wide Git identity (read-only)',
'settings.gitIdentities.editor.description.newProfile': 'Create a new Git identity profile',
'settings.gitIdentities.editor.description.editProfile': 'Edit identity profile settings',
'settings.gitIdentities.editor.section.commitSigning': 'Commit signing',
'settings.gitIdentities.editor.field.profileName': 'Profile Name',
'settings.gitIdentities.editor.field.profileNamePlaceholder': 'Work Profile, Personal, etc.',
'settings.gitIdentities.editor.field.color': 'Color',
@@ -548,6 +549,9 @@ export const settingsDict = {
'settings.gitIdentities.editor.field.sshKeyPath': 'SSH Key Path',
'settings.gitIdentities.editor.field.sshKeyPathTooltip': 'Optional path to private key. e.g. ~/.ssh/id_ed25519',
'settings.gitIdentities.editor.field.sshKeyPathPlaceholder': '~/.ssh/id_ed25519',
'settings.gitIdentities.editor.field.signCommits': 'Sign commits with this identity',
'settings.gitIdentities.editor.field.signingKey': 'Signing key',
'settings.gitIdentities.editor.field.signingKeyPlaceholder': '~/.ssh/id_ed25519.pub',
'settings.gitIdentities.editor.field.host': 'Host',
'settings.gitIdentities.editor.field.hostTooltip': 'Token will be read from ~/.git-credentials for this host.',
'settings.gitIdentities.editor.field.hostPlaceholder': 'github.com',
@@ -556,6 +560,7 @@ export const settingsDict = {
'settings.gitIdentities.editor.actions.save': 'Save',
'settings.gitIdentities.editor.toast.userNameEmailRequired': 'User name and email are required',
'settings.gitIdentities.editor.toast.hostRequiredForToken': 'Host is required for token-based authentication',
'settings.gitIdentities.editor.toast.signingKeyRequired': 'Signing key is required when commit signing is enabled',
'settings.gitIdentities.editor.toast.profileCreated': 'Profile created',
'settings.gitIdentities.editor.toast.profileUpdated': 'Profile updated',
'settings.gitIdentities.editor.toast.createProfileFailed': 'Failed to create profile',
@@ -500,6 +500,7 @@ export const settingsDict = {
"settings.gitIdentities.editor.description.globalReadOnly": "Identidad Git global (solo lectura)",
"settings.gitIdentities.editor.description.newProfile": "Crear una nueva identidad Git",
"settings.gitIdentities.editor.description.editProfile": "Editar la configuración de la identidad",
"settings.gitIdentities.editor.section.commitSigning": "Firma de commits",
"settings.gitIdentities.editor.field.profileName": "Nombre de la identidad",
"settings.gitIdentities.editor.field.profileNamePlaceholder": "Identidad de trabajo, personal, etc.",
"settings.gitIdentities.editor.field.color": "Color",
@@ -515,6 +516,9 @@ export const settingsDict = {
"settings.gitIdentities.editor.field.sshKeyPath": "Ruta de la clave SSH",
"settings.gitIdentities.editor.field.sshKeyPathTooltip": "Ruta opcional a la clave privada. Ejemplo: ~/.ssh/id_ed25519",
"settings.gitIdentities.editor.field.sshKeyPathPlaceholder": "~/.ssh/id_ed25519",
"settings.gitIdentities.editor.field.signCommits": "Firmar commits con esta identidad",
"settings.gitIdentities.editor.field.signingKey": "Clave de firma",
"settings.gitIdentities.editor.field.signingKeyPlaceholder": "~/.ssh/id_ed25519.pub",
"settings.gitIdentities.editor.field.host": "Servidor",
"settings.gitIdentities.editor.field.hostTooltip": "El token se leerá de ~/.git-credentials para este servidor.",
"settings.gitIdentities.editor.field.hostPlaceholder": "github.com",
@@ -523,6 +527,7 @@ export const settingsDict = {
"settings.gitIdentities.editor.actions.save": "Guardar",
"settings.gitIdentities.editor.toast.userNameEmailRequired": "Se requieren nombre de usuario y dirección de correo electrónico",
"settings.gitIdentities.editor.toast.hostRequiredForToken": "Se requiere un servidor para autenticación basada en token",
"settings.gitIdentities.editor.toast.signingKeyRequired": "Se requiere una clave de firma cuando la firma de commits está activada",
"settings.gitIdentities.editor.toast.profileCreated": "Identidad creada",
"settings.gitIdentities.editor.toast.profileUpdated": "Identidad actualizada",
"settings.gitIdentities.editor.toast.createProfileFailed": "No se pudo crear la identidad",
@@ -489,6 +489,7 @@ export const settingsDict = {
'settings.gitIdentities.editor.description.globalReadOnly': 'Identité Git à l\'échelle du système (lecture seule)',
'settings.gitIdentities.editor.description.newProfile': 'Créer un nouveau profil d\'identité Git',
'settings.gitIdentities.editor.description.editProfile': 'Modifier les paramètres du profil d\'identité',
'settings.gitIdentities.editor.section.commitSigning': 'Signature des commits',
'settings.gitIdentities.editor.field.profileName': 'Nom du profil',
'settings.gitIdentities.editor.field.profileNamePlaceholder': 'Profil professionnel, personnel, etc.',
'settings.gitIdentities.editor.field.color': 'Couleur',
@@ -504,6 +505,9 @@ export const settingsDict = {
'settings.gitIdentities.editor.field.sshKeyPath': 'Chemin clé SSH',
'settings.gitIdentities.editor.field.sshKeyPathTooltip': 'Chemin facultatif vers la clé privée. e.g. ~/.ssh/id_ed25519',
'settings.gitIdentities.editor.field.sshKeyPathPlaceholder': '~/.ssh/id_ed25519',
'settings.gitIdentities.editor.field.signCommits': 'Signer les commits avec cette identité',
'settings.gitIdentities.editor.field.signingKey': 'Clé de signature',
'settings.gitIdentities.editor.field.signingKeyPlaceholder': '~/.ssh/id_ed25519.pub',
'settings.gitIdentities.editor.field.host': 'Hôte',
'settings.gitIdentities.editor.field.hostTooltip': 'Le jeton sera lu à partir de ~/.git-credentials pour cet hôte.',
'settings.gitIdentities.editor.field.hostPlaceholder': 'github.com',
@@ -512,6 +516,7 @@ export const settingsDict = {
'settings.gitIdentities.editor.actions.save': 'Sauvegarder',
'settings.gitIdentities.editor.toast.userNameEmailRequired': 'Le nom d\'utilisateur et l\'e-mail sont requis',
'settings.gitIdentities.editor.toast.hostRequiredForToken': 'L\'hôte est requis pour l\'authentification basée sur un jeton',
'settings.gitIdentities.editor.toast.signingKeyRequired': 'Une clé de signature est requise lorsque la signature des commits est activée',
'settings.gitIdentities.editor.toast.profileCreated': 'Profil créé',
'settings.gitIdentities.editor.toast.profileUpdated': 'Profil mis à jour',
'settings.gitIdentities.editor.toast.createProfileFailed': 'Échec de la création du profil',
@@ -500,6 +500,7 @@ export const settingsDict = {
'settings.gitIdentities.editor.description.globalReadOnly': '시스템 전체 Git 자격 증명(읽기 전용)',
'settings.gitIdentities.editor.description.newProfile': '새 Git 자격 증명 프로필을 생성하세요',
'settings.gitIdentities.editor.description.editProfile': 'Git 자격 증명 프로필 설정을 편집하세요',
'settings.gitIdentities.editor.section.commitSigning': '커밋 서명',
'settings.gitIdentities.editor.field.profileName': '프로필 이름',
'settings.gitIdentities.editor.field.profileNamePlaceholder': 'Work Profile, Personal 등',
'settings.gitIdentities.editor.field.color': '색상',
@@ -515,6 +516,9 @@ export const settingsDict = {
'settings.gitIdentities.editor.field.sshKeyPath': 'SSH 키 경로',
'settings.gitIdentities.editor.field.sshKeyPathTooltip': '개인 키의 선택적 경로입니다. 예: ~/.ssh/id_ed25519',
'settings.gitIdentities.editor.field.sshKeyPathPlaceholder': '~/.ssh/id_ed25519',
'settings.gitIdentities.editor.field.signCommits': '이 ID로 커밋 서명',
'settings.gitIdentities.editor.field.signingKey': '서명 키',
'settings.gitIdentities.editor.field.signingKeyPlaceholder': '~/.ssh/id_ed25519.pub',
'settings.gitIdentities.editor.field.host': 'Host',
'settings.gitIdentities.editor.field.hostTooltip': '이 host의 토큰은 ~/.git-credentials에서 읽습니다.',
'settings.gitIdentities.editor.field.hostPlaceholder': 'github.com',
@@ -523,6 +527,7 @@ export const settingsDict = {
'settings.gitIdentities.editor.actions.save': '저장',
'settings.gitIdentities.editor.toast.userNameEmailRequired': '사용자 이름과 이메일은 필수입니다',
'settings.gitIdentities.editor.toast.hostRequiredForToken': '토큰 기반 인증에는 host가 필요합니다',
'settings.gitIdentities.editor.toast.signingKeyRequired': '커밋 서명이 활성화되면 서명 키가 필요합니다',
'settings.gitIdentities.editor.toast.profileCreated': '프로필이 생성되었습니다',
'settings.gitIdentities.editor.toast.profileUpdated': '프로필이 업데이트되었습니다',
'settings.gitIdentities.editor.toast.createProfileFailed': '프로필을 생성하지 못했습니다',
@@ -178,6 +178,7 @@ export const settingsDict = {
'settings.gitIdentities.editor.description.editProfile': 'Edytuj ustawienia profilu tożsamości',
'settings.gitIdentities.editor.description.globalReadOnly': 'Globalna tożsamość Git (tylko do odczytu)',
'settings.gitIdentities.editor.description.newProfile': 'Utwórz nowy profil tożsamości Git',
'settings.gitIdentities.editor.section.commitSigning': 'Podpisywanie commitów',
'settings.gitIdentities.editor.field.authMethod': 'Metoda uwierzytelniania',
'settings.gitIdentities.editor.field.authToken': 'Token',
'settings.gitIdentities.editor.field.color': 'Kolor',
@@ -193,6 +194,9 @@ export const settingsDict = {
'settings.gitIdentities.editor.field.sshKeyPath': 'Ścieżka do klucza SSH',
'settings.gitIdentities.editor.field.sshKeyPathPlaceholder': '~/.ssh/id_ed25519',
'settings.gitIdentities.editor.field.sshKeyPathTooltip': 'Opcjonalna ścieżka do klucza prywatnego, np. ~/.ssh/id_ed25519',
'settings.gitIdentities.editor.field.signCommits': 'Podpisuj commity tą tożsamością',
'settings.gitIdentities.editor.field.signingKey': 'Klucz podpisu',
'settings.gitIdentities.editor.field.signingKeyPlaceholder': '~/.ssh/id_ed25519.pub',
'settings.gitIdentities.editor.field.userName': 'Nazwa użytkownika',
'settings.gitIdentities.editor.field.userNamePlaceholder': 'Jan Kowalski',
'settings.gitIdentities.editor.field.userNameTooltip': 'Nazwa, która pojawi się w wiadomościach commitów Git.',
@@ -204,6 +208,7 @@ export const settingsDict = {
'settings.gitIdentities.editor.toast.deleteProfileFailed': 'Nie udało się usunąć profilu',
'settings.gitIdentities.editor.toast.deleteUnexpectedError': 'Wystąpił błąd podczas usuwania',
'settings.gitIdentities.editor.toast.hostRequiredForToken': 'Host jest wymagany dla uwierzytelniania opartego na tokenie',
'settings.gitIdentities.editor.toast.signingKeyRequired': 'Klucz podpisu jest wymagany, gdy włączone jest podpisywanie commitów',
'settings.gitIdentities.editor.toast.profileCreated': 'Profil utworzony',
'settings.gitIdentities.editor.toast.profileDeleted': 'Profil usunięty',
'settings.gitIdentities.editor.toast.profileUpdated': 'Profil zaktualizowany',
@@ -500,6 +500,7 @@ export const settingsDict = {
"settings.gitIdentities.editor.description.globalReadOnly": "Identidade Git global (somente leitura)",
"settings.gitIdentities.editor.description.newProfile": "Criar uma nova identidade Git",
"settings.gitIdentities.editor.description.editProfile": "Editar as configurações da identidade",
"settings.gitIdentities.editor.section.commitSigning": "Assinatura de commits",
"settings.gitIdentities.editor.field.profileName": "Nome da identidade",
"settings.gitIdentities.editor.field.profileNamePlaceholder": "Identidade de trabalho, pessoal, etc.",
"settings.gitIdentities.editor.field.color": "Cor",
@@ -515,6 +516,9 @@ export const settingsDict = {
"settings.gitIdentities.editor.field.sshKeyPath": "Caminho da chave SSH",
"settings.gitIdentities.editor.field.sshKeyPathTooltip": "Caminho opcional para a chave privada. Exemplo: ~/.ssh/id_ed25519",
"settings.gitIdentities.editor.field.sshKeyPathPlaceholder": "~/.ssh/id_ed25519",
"settings.gitIdentities.editor.field.signCommits": "Assinar commits com esta identidade",
"settings.gitIdentities.editor.field.signingKey": "Chave de assinatura",
"settings.gitIdentities.editor.field.signingKeyPlaceholder": "~/.ssh/id_ed25519.pub",
"settings.gitIdentities.editor.field.host": "Servidor",
"settings.gitIdentities.editor.field.hostTooltip": "O token será lido de ~/.git-credentials para este servidor.",
"settings.gitIdentities.editor.field.hostPlaceholder": "github.com",
@@ -523,6 +527,7 @@ export const settingsDict = {
"settings.gitIdentities.editor.actions.save": "Salvar",
"settings.gitIdentities.editor.toast.userNameEmailRequired": "Nome de usuário e endereço de e-mail são obrigatórios",
"settings.gitIdentities.editor.toast.hostRequiredForToken": "É necessário um servidor para autenticação baseada em token",
"settings.gitIdentities.editor.toast.signingKeyRequired": "É necessária uma chave de assinatura quando a assinatura de commits está ativada",
"settings.gitIdentities.editor.toast.profileCreated": "Identidade criada",
"settings.gitIdentities.editor.toast.profileUpdated": "Identidade atualizada",
"settings.gitIdentities.editor.toast.createProfileFailed": "Não foi possível criar a identidade",
@@ -500,6 +500,7 @@ export const settingsDict = {
"settings.gitIdentities.editor.description.globalReadOnly": "Глобальна Git-ідентичність (лише для читання)",
"settings.gitIdentities.editor.description.newProfile": "Створити новий профіль Git-ідентичності",
"settings.gitIdentities.editor.description.editProfile": "Редагувати налаштування профілю ідентичності",
"settings.gitIdentities.editor.section.commitSigning": "Підписування комітів",
"settings.gitIdentities.editor.field.profileName": "Ім'я профілю",
"settings.gitIdentities.editor.field.profileNamePlaceholder": "Робочий профіль, особистий тощо.",
"settings.gitIdentities.editor.field.color": "Колір",
@@ -515,6 +516,9 @@ export const settingsDict = {
"settings.gitIdentities.editor.field.sshKeyPath": "Шлях до SSH-ключа",
"settings.gitIdentities.editor.field.sshKeyPathTooltip": "Додатковий шлях до закритого ключа. напр. ~/.ssh/id_ed25519",
"settings.gitIdentities.editor.field.sshKeyPathPlaceholder": "~/.ssh/id_ed25519",
"settings.gitIdentities.editor.field.signCommits": "Підписувати коміти цією ідентичністю",
"settings.gitIdentities.editor.field.signingKey": "Ключ підпису",
"settings.gitIdentities.editor.field.signingKeyPlaceholder": "~/.ssh/id_ed25519.pub",
"settings.gitIdentities.editor.field.host": "Хост",
"settings.gitIdentities.editor.field.hostTooltip": "Токен буде зчитано з облікових даних ~/.git для цього хосту.",
"settings.gitIdentities.editor.field.hostPlaceholder": "github.com",
@@ -523,6 +527,7 @@ export const settingsDict = {
"settings.gitIdentities.editor.actions.save": "Зберегти",
"settings.gitIdentities.editor.toast.userNameEmailRequired": "Необхідно вказати ім’я користувача та електронну адресу",
"settings.gitIdentities.editor.toast.hostRequiredForToken": "Для автентифікації на основі токенів потрібен хост",
"settings.gitIdentities.editor.toast.signingKeyRequired": "Потрібен ключ підпису, коли ввімкнено підписування комітів",
"settings.gitIdentities.editor.toast.profileCreated": "Профіль створено",
"settings.gitIdentities.editor.toast.profileUpdated": "Профіль оновлено",
"settings.gitIdentities.editor.toast.createProfileFailed": "Не вдалося створити профіль",
@@ -500,6 +500,7 @@ export const settingsDict = {
'settings.gitIdentities.editor.description.globalReadOnly': '系统范围 Git 身份(只读)',
'settings.gitIdentities.editor.description.newProfile': '创建新的 Git 身份配置',
'settings.gitIdentities.editor.description.editProfile': '编辑身份配置设置',
'settings.gitIdentities.editor.section.commitSigning': '提交签名',
'settings.gitIdentities.editor.field.profileName': '身份名称',
'settings.gitIdentities.editor.field.profileNamePlaceholder': '工作身份、个人身份等',
'settings.gitIdentities.editor.field.color': '颜色',
@@ -515,6 +516,9 @@ export const settingsDict = {
'settings.gitIdentities.editor.field.sshKeyPath': 'SSH 密钥路径',
'settings.gitIdentities.editor.field.sshKeyPathTooltip': '可选私钥路径,例如 ~/.ssh/id_ed25519',
'settings.gitIdentities.editor.field.sshKeyPathPlaceholder': '~/.ssh/id_ed25519',
'settings.gitIdentities.editor.field.signCommits': '使用此身份签名提交',
'settings.gitIdentities.editor.field.signingKey': '签名密钥',
'settings.gitIdentities.editor.field.signingKeyPlaceholder': '~/.ssh/id_ed25519.pub',
'settings.gitIdentities.editor.field.host': 'Host',
'settings.gitIdentities.editor.field.hostTooltip': '该 host 的 token 将从 ~/.git-credentials 读取。',
'settings.gitIdentities.editor.field.hostPlaceholder': 'github.com',
@@ -523,6 +527,7 @@ export const settingsDict = {
'settings.gitIdentities.editor.actions.save': '保存',
'settings.gitIdentities.editor.toast.userNameEmailRequired': '用户名和邮箱为必填项',
'settings.gitIdentities.editor.toast.hostRequiredForToken': '基于 token 认证时 Host 为必填项',
'settings.gitIdentities.editor.toast.signingKeyRequired': '启用提交签名时需要签名密钥',
'settings.gitIdentities.editor.toast.profileCreated': '身份已创建',
'settings.gitIdentities.editor.toast.profileUpdated': '身份已更新',
'settings.gitIdentities.editor.toast.createProfileFailed': '创建身份失败',
@@ -497,6 +497,7 @@
'settings.gitIdentities.editor.description.globalReadOnly': '系統範圍 Git 身分(唯讀)',
'settings.gitIdentities.editor.description.newProfile': '建立新的 Git 身分設定',
'settings.gitIdentities.editor.description.editProfile': '編輯身分設定',
'settings.gitIdentities.editor.section.commitSigning': '提交簽章',
'settings.gitIdentities.editor.field.profileName': '身分名稱',
'settings.gitIdentities.editor.field.profileNamePlaceholder': '工作身分、個人身分等',
'settings.gitIdentities.editor.field.color': '顏色',
@@ -512,6 +513,9 @@
'settings.gitIdentities.editor.field.sshKeyPath': 'SSH 金鑰路徑',
'settings.gitIdentities.editor.field.sshKeyPathTooltip': '可選私鑰路徑,例如 ~/.ssh/id_ed25519',
'settings.gitIdentities.editor.field.sshKeyPathPlaceholder': '~/.ssh/id_ed25519',
'settings.gitIdentities.editor.field.signCommits': '使用此身分簽署提交',
'settings.gitIdentities.editor.field.signingKey': '簽章金鑰',
'settings.gitIdentities.editor.field.signingKeyPlaceholder': '~/.ssh/id_ed25519.pub',
'settings.gitIdentities.editor.field.host': 'Host',
'settings.gitIdentities.editor.field.hostTooltip': '該 host 的 token 將從 ~/.git-credentials 讀取。',
'settings.gitIdentities.editor.field.hostPlaceholder': 'github.com',
@@ -520,6 +524,7 @@
'settings.gitIdentities.editor.actions.save': '儲存',
'settings.gitIdentities.editor.toast.userNameEmailRequired': '使用者名稱和電子郵件為必填項',
'settings.gitIdentities.editor.toast.hostRequiredForToken': '基於 token 驗證時 Host 為必填項',
'settings.gitIdentities.editor.toast.signingKeyRequired': '啟用提交簽章時需要簽章金鑰',
'settings.gitIdentities.editor.toast.profileCreated': '身分已建立',
'settings.gitIdentities.editor.toast.profileUpdated': '身分已更新',
'settings.gitIdentities.editor.toast.createProfileFailed': '建立身分失敗',
+1 -1
View File
@@ -316,7 +316,7 @@ export const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
page: 'git',
titleKey: 'settings.gitIdentities.page.section.title',
descriptionKey: 'settings.gitIdentities.page.empty.description',
keywords: ['identity', 'profile', 'author', 'email', 'credentials'],
keywords: ['identity', 'profile', 'author', 'email', 'credentials', 'signing', 'commit signing', 'ssh signing', 'gpg'],
},
{
id: 'git.changes-view',
@@ -23,6 +23,8 @@ export interface GitIdentityProfile {
userEmail: string;
authType?: GitIdentityAuthType;
sshKey?: string | null;
signCommits?: boolean;
signingKey?: string | null;
host?: string | null;
color?: string | null;
icon?: string | null;
+11 -2
View File
@@ -531,12 +531,14 @@ export async function handleStandardGitBridgeMessage(message: BridgeMessageInput
}
case 'api:git/identity': {
const { directory, method, userName, userEmail, sshKey } = (payload || {}) as {
const { directory, method, userName, userEmail, sshKey, signCommits, signingKey } = (payload || {}) as {
directory?: string;
method?: string;
userName?: string;
userEmail?: string;
sshKey?: string | null;
signCommits?: boolean;
signingKey?: string | null;
};
const dirError = requireDirectory(id, type, directory);
if (dirError) return dirError;
@@ -552,7 +554,14 @@ export async function handleStandardGitBridgeMessage(message: BridgeMessageInput
if (!userName || !userEmail) {
return { id, type, success: false, error: 'userName and userEmail are required' };
}
const result = await gitService.setGitIdentity(directory!, userName, userEmail, sshKey);
const result = await gitService.setGitIdentity(
directory!,
userName,
userEmail,
sshKey,
signCommits === true,
signingKey ?? null
);
return { id, type, success: true, data: result };
}
+14 -1
View File
@@ -3284,12 +3284,15 @@ export async function setGitIdentity(
directory: string,
userName: string,
userEmail: string,
sshKey?: string | null
sshKey?: string | null,
signCommits?: boolean | null,
signingKey?: string | null
): Promise<{ success: boolean }> {
const repo = await getRepository(directory);
// Build SSH command once if needed
const sshCommand = sshKey ? buildSshCommand(sshKey) : null;
const shouldSignCommits = signCommits === true && typeof signingKey === 'string' && signingKey.trim().length > 0;
if (repo) {
try {
@@ -3298,6 +3301,11 @@ export async function setGitIdentity(
if (sshCommand) {
await repo.setConfig('core.sshCommand', sshCommand);
}
if (shouldSignCommits) {
await repo.setConfig('gpg.format', 'ssh');
await repo.setConfig('user.signingkey', signingKey.trim());
await repo.setConfig('commit.gpgsign', 'true');
}
return { success: true };
} catch (error) {
console.error('[GitService] Failed to set identity:', error);
@@ -3310,6 +3318,11 @@ export async function setGitIdentity(
if (sshCommand) {
await execGit(['config', 'core.sshCommand', sshCommand], directory);
}
if (shouldSignCommits) {
await execGit(['config', 'gpg.format', 'ssh'], directory);
await execGit(['config', 'user.signingkey', signingKey.trim()], directory);
await execGit(['config', 'commit.gpgsign', 'true'], directory);
}
return { success: true };
}
+65 -9
View File
@@ -41,6 +41,24 @@ import type {
ResetToCommitResponse,
} from '@openchamber/ui/lib/api/types';
type GitIdentityStoreState = {
profiles: GitIdentityProfile[];
};
type GitIdentityStoreApi = {
getState: () => GitIdentityStoreState;
setState: (
nextState: GitIdentityStoreState | ((state: GitIdentityStoreState) => GitIdentityStoreState),
replace?: boolean
) => void;
};
const getGitIdentityStore = (): GitIdentityStoreApi | undefined => (
window as Window & {
__zustand_git_identities_store__?: GitIdentityStoreApi;
}
).__zustand_git_identities_store__;
export const createVSCodeGitAPI = (): GitAPI => ({
checkIsGitRepository: async (directory: string): Promise<boolean> => {
return sendBridgeMessage<boolean>('api:git/check', { directory });
@@ -311,31 +329,69 @@ export const createVSCodeGitAPI = (): GitAPI => ({
},
setGitIdentity: async (directory: string, profileId: string): Promise<{ success: boolean; profile: GitIdentityProfile }> => {
// For VS Code, we need to resolve the profile from the store
// This is a simplified implementation - the full implementation would need profile lookup
const store = (window as Window & {
__zustand_git_identities_store__?: {
getState: () => {
getProfileById: (id: string) => GitIdentityProfile | undefined;
};
};
}).__zustand_git_identities_store__;
const profile = store?.getState().getProfileById(profileId);
if (!profile) {
return {
success: false,
profile: { id: profileId, name: '', userName: '', userEmail: '' },
};
}
const result = await sendBridgeMessage<{ success: boolean }>('api:git/identity', {
directory,
method: 'POST',
userName: profile.userName,
userEmail: profile.userEmail,
sshKey: profile.sshKey ?? null,
signCommits: profile.signCommits === true,
signingKey: profile.signingKey ?? null,
});
return {
success: false,
profile: { id: profileId, name: '', userName: '', userEmail: '' },
success: result.success === true,
profile,
};
},
// Git identity profile management - these are stored in extension settings
// For simplicity, return empty arrays/objects as these are managed through the settings UI
// Git identity profile management is backed by the webview store in VS Code.
getGitIdentities: async (): Promise<GitIdentityProfile[]> => {
return [];
return getGitIdentityStore()?.getState().profiles ?? [];
},
createGitIdentity: async (profile: GitIdentityProfile): Promise<GitIdentityProfile> => {
const store = getGitIdentityStore();
if (store) {
store.setState((state) => ({
profiles: [...state.profiles.filter((existing) => existing.id !== profile.id), profile],
}));
}
return profile;
},
updateGitIdentity: async (id: string, profile: GitIdentityProfile): Promise<GitIdentityProfile> => {
void id; // Unused for now
const store = getGitIdentityStore();
if (store) {
store.setState((state) => ({
profiles: state.profiles.map((existing) => (existing.id === id ? { ...existing, ...profile, id } : existing)),
}));
}
return profile;
},
deleteGitIdentity: async (id: string): Promise<void> => {
void id; // Unused for now
const store = getGitIdentityStore();
if (store) {
store.setState((state) => ({
profiles: state.profiles.filter((existing) => existing.id !== id),
}));
}
},
getRemotes: async (directory: string): Promise<GitRemote[]> => {
@@ -68,6 +68,8 @@ export function createProfile(profileData) {
userEmail: profileData.userEmail,
authType: profileData.authType || 'ssh',
sshKey: profileData.sshKey || null,
signCommits: profileData.signCommits,
signingKey: profileData.signingKey || null,
host: profileData.host || null,
color: profileData.color || 'keyword',
icon: profileData.icon || 'branch'
+6
View File
@@ -1913,6 +1913,12 @@ export async function setLocalIdentity(directory, profile) {
await git.raw(['config', '--local', '--unset', 'core.sshCommand']).catch(() => {});
}
if (profile.signCommits === true && typeof profile.signingKey === 'string' && profile.signingKey.trim()) {
await git.addConfig('gpg.format', 'ssh', false, 'local');
await git.addConfig('user.signingkey', profile.signingKey.trim(), false, 'local');
await git.addConfig('commit.gpgsign', 'true', false, 'local');
}
return true;
} catch (error) {
console.error('Failed to set Git identity:', error);