From 4d63278efd3f9b18f2b981f6b792794fa1182682 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Thu, 18 Jun 2026 23:51:40 +0300 Subject: [PATCH] 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 --- .../GitIdentityEditorDialog.tsx | 50 +++++++++++++ packages/ui/src/lib/api/types.ts | 2 + .../ui/src/lib/i18n/messages/en.settings.ts | 5 ++ .../ui/src/lib/i18n/messages/es.settings.ts | 5 ++ .../ui/src/lib/i18n/messages/fr.settings.ts | 5 ++ .../ui/src/lib/i18n/messages/ko.settings.ts | 5 ++ .../ui/src/lib/i18n/messages/pl.settings.ts | 5 ++ .../src/lib/i18n/messages/pt-BR.settings.ts | 5 ++ .../ui/src/lib/i18n/messages/uk.settings.ts | 5 ++ .../src/lib/i18n/messages/zh-CN.settings.ts | 5 ++ .../src/lib/i18n/messages/zh-TW.settings.ts | 5 ++ packages/ui/src/lib/settings/search.ts | 2 +- .../ui/src/stores/useGitIdentitiesStore.ts | 2 + packages/vscode/src/bridge-git-runtime.ts | 13 +++- packages/vscode/src/gitService.ts | 15 +++- packages/vscode/webview/api/git.ts | 74 ++++++++++++++++--- .../web/server/lib/git/identity-storage.js | 2 + packages/web/server/lib/git/service.js | 6 ++ 18 files changed, 198 insertions(+), 13 deletions(-) diff --git a/packages/ui/src/components/sections/git-identities/GitIdentityEditorDialog.tsx b/packages/ui/src/components/sections/git-identities/GitIdentityEditorDialog.tsx index fe06d63e..409195d4 100644 --- a/packages/ui/src/components/sections/git-identities/GitIdentityEditorDialog.tsx +++ b/packages/ui/src/components/sections/git-identities/GitIdentityEditorDialog.tsx @@ -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 = ( const [userEmail, setUserEmail] = React.useState(''); const [authType, setAuthType] = React.useState('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 = ( setUserEmail(''); setAuthType('token'); setSshKey(''); + setSignCommits(false); + setSigningKey(''); setHost(importData.host); setColor('string'); setIcon('code'); @@ -92,6 +97,8 @@ export const GitIdentityEditorDialog: React.FC = ( setUserEmail(''); setAuthType('ssh'); setSshKey(''); + setSignCommits(false); + setSigningKey(''); setHost(''); setColor('keyword'); setIcon('branch'); @@ -101,6 +108,8 @@ export const GitIdentityEditorDialog: React.FC = ( 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 = ( 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 = ( 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 = ( 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 = ( )} +
+
+ +
+
+ {t('settings.gitIdentities.editor.field.signCommits')} +
+
+ {t('settings.gitIdentities.editor.section.commitSigning')} +
+
+
+ +
+
+ +
+ setSigningKey(e.target.value)} + placeholder={t('settings.gitIdentities.editor.field.signingKeyPlaceholder')} + disabled={!signCommits} + className="h-8 font-mono text-xs" + /> +
+
+ {authType === 'token' && (
diff --git a/packages/ui/src/lib/api/types.ts b/packages/ui/src/lib/api/types.ts index 294c2bbe..9b4021b5 100644 --- a/packages/ui/src/lib/api/types.ts +++ b/packages/ui/src/lib/api/types.ts @@ -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; diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index 1d921c3f..83e9e0a1 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -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', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index 2561bc8d..c78bb4d5 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -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", diff --git a/packages/ui/src/lib/i18n/messages/fr.settings.ts b/packages/ui/src/lib/i18n/messages/fr.settings.ts index fd0c6429..89fc09e7 100644 --- a/packages/ui/src/lib/i18n/messages/fr.settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr.settings.ts @@ -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', diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index a59b6b70..2e4752db 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -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': '프로필을 생성하지 못했습니다', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index 3231d945..a469c319 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -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', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts index 13c79e1e..da72559a 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -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", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index 3cbbd432..41dff574 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -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": "Не вдалося створити профіль", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts index d1d5f3ba..9604ee48 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -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': '创建身份失败', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts index a64548ba..8a380184 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts @@ -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': '建立身分失敗', diff --git a/packages/ui/src/lib/settings/search.ts b/packages/ui/src/lib/settings/search.ts index c6ce968b..b33c20f7 100644 --- a/packages/ui/src/lib/settings/search.ts +++ b/packages/ui/src/lib/settings/search.ts @@ -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', diff --git a/packages/ui/src/stores/useGitIdentitiesStore.ts b/packages/ui/src/stores/useGitIdentitiesStore.ts index 36ef68a8..917a049a 100644 --- a/packages/ui/src/stores/useGitIdentitiesStore.ts +++ b/packages/ui/src/stores/useGitIdentitiesStore.ts @@ -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; diff --git a/packages/vscode/src/bridge-git-runtime.ts b/packages/vscode/src/bridge-git-runtime.ts index 9cba677a..5abb4217 100644 --- a/packages/vscode/src/bridge-git-runtime.ts +++ b/packages/vscode/src/bridge-git-runtime.ts @@ -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 }; } diff --git a/packages/vscode/src/gitService.ts b/packages/vscode/src/gitService.ts index a232ef7c..28ff3cb0 100644 --- a/packages/vscode/src/gitService.ts +++ b/packages/vscode/src/gitService.ts @@ -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 }; } diff --git a/packages/vscode/webview/api/git.ts b/packages/vscode/webview/api/git.ts index 4ade53d5..60e35e46 100644 --- a/packages/vscode/webview/api/git.ts +++ b/packages/vscode/webview/api/git.ts @@ -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 => { return sendBridgeMessage('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 => { - return []; + return getGitIdentityStore()?.getState().profiles ?? []; }, createGitIdentity: async (profile: GitIdentityProfile): Promise => { + 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 => { - 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 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 => { diff --git a/packages/web/server/lib/git/identity-storage.js b/packages/web/server/lib/git/identity-storage.js index b2b98ae5..438d6633 100644 --- a/packages/web/server/lib/git/identity-storage.js +++ b/packages/web/server/lib/git/identity-storage.js @@ -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' diff --git a/packages/web/server/lib/git/service.js b/packages/web/server/lib/git/service.js index ed86fd39..b4c23b9f 100644 --- a/packages/web/server/lib/git/service.js +++ b/packages/web/server/lib/git/service.js @@ -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);