fix(git): support secure SSH config

This commit is contained in:
Bohdan Triapitsyn
2026-08-02 23:02:14 +03:00
parent 8ebf711f93
commit 134d055ee6
6 changed files with 42 additions and 15 deletions
+8 -4
View File
@@ -55,7 +55,7 @@
"react-dom": "^19.1.1",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"simple-git": "^3.28.0",
"simple-git": "^3.36.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"yaml": "^2.8.1",
@@ -200,7 +200,7 @@
"remark-math": "^6.0.0",
"remend": "^1.2.1",
"shiki": "^3.23.0",
"simple-git": "^3.28.0",
"simple-git": "^3.36.0",
"sonner": "^2.0.7",
"strip-json-comments": "^5.0.3",
"tailwind-merge": "^3.3.1",
@@ -284,7 +284,7 @@
"qrcode-terminal": "^0.12.0",
"reflect-metadata": "^0.2.2",
"sherpa-onnx-node": "1.12.28",
"simple-git": "^3.28.0",
"simple-git": "^3.36.0",
"web-push": "^3.6.7",
"ws": "^8.18.3",
"yaml": "^2.8.1",
@@ -1258,6 +1258,10 @@
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@simple-git/args-pathspec": ["@simple-git/args-pathspec@1.0.3", "", {}, "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA=="],
"@simple-git/argv-parser": ["@simple-git/argv-parser@1.1.1", "", { "dependencies": { "@simple-git/args-pathspec": "^1.0.3" } }, "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw=="],
"@simplewebauthn/browser": ["@simplewebauthn/browser@13.3.0", "", {}, "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ=="],
"@simplewebauthn/server": ["@simplewebauthn/server@13.3.1", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.1", "@peculiar/asn1-rsa": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "@peculiar/x509": "^1.14.3" } }, "sha512-GV/oM/qeycWn8p42JZIMJBsXWQcNFg+nJFzeQTnMA4gN8mXg0+HZFWJerHg8ZN/zlveMS3iV1wzuFpOVWS/46w=="],
@@ -3000,7 +3004,7 @@
"simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
"simple-git": ["simple-git@3.32.3", "", { "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", "debug": "^4.4.0" } }, "sha512-56a5oxFdWlsGygOXHWrG+xjj5w9ZIt2uQbzqiIGdR/6i5iococ7WQ/bNPzWxCJdEUGUCmyMH0t9zMpRJTaKxmw=="],
"simple-git": ["simple-git@3.36.0", "", { "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", "@simple-git/args-pathspec": "^1.0.3", "@simple-git/argv-parser": "^1.1.0", "debug": "^4.4.0" } }, "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q=="],
"simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="],
+1 -1
View File
@@ -134,7 +134,7 @@
"react-dom": "^19.1.1",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"simple-git": "^3.28.0",
"simple-git": "^3.36.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"yaml": "^2.8.1",
+1 -1
View File
@@ -77,7 +77,7 @@
"remark-math": "^6.0.0",
"remend": "^1.2.1",
"shiki": "^3.23.0",
"simple-git": "^3.28.0",
"simple-git": "^3.36.0",
"sonner": "^2.0.7",
"strip-json-comments": "^5.0.3",
"tailwind-merge": "^3.3.1",
+1 -1
View File
@@ -42,7 +42,7 @@
"qrcode-terminal": "^0.12.0",
"reflect-metadata": "^0.2.2",
"sherpa-onnx-node": "1.12.28",
"simple-git": "^3.28.0",
"simple-git": "^3.36.0",
"web-push": "^3.6.7",
"ws": "^8.18.3",
"yaml": "^2.8.1"
+13 -8
View File
@@ -351,12 +351,17 @@ const buildGitEnv = async () => {
return env;
};
const createGit = async (directory) => {
const createGit = async (directory, { allowUnsafeSshCommand = false } = {}) => {
const env = await buildGitEnv();
const spawnOptions = { windowsHide: true };
const binary = getGitBinary();
const hasCustomBinary = typeof binary === 'string' && binary.trim() && binary !== 'git' && binary !== 'git.exe';
const unsafe = hasCustomBinary ? { allowUnsafeCustomBinary: true } : undefined;
const unsafe = hasCustomBinary || allowUnsafeSshCommand
? {
...(hasCustomBinary && { allowUnsafeCustomBinary: true }),
...(allowUnsafeSshCommand && { allowUnsafeSshCommand: true }),
}
: undefined;
// Always pin simple-git to an explicit working directory. Omitting baseDir
// makes simple-git use process.cwd(), which breaks when the OpenChamber
// server was launched from a neutral directory (e.g. $HOME) and the opened
@@ -2047,7 +2052,7 @@ export async function hasLocalIdentity(directory) {
}
export async function setLocalIdentity(directory, profile) {
const git = await createGit(directory);
const git = await createGit(directory, { allowUnsafeSshCommand: true });
try {
@@ -2057,12 +2062,12 @@ export async function setLocalIdentity(directory, profile) {
const authType = profile.authType || 'ssh';
if (authType === 'ssh' && profile.sshKey) {
await git.addConfig(
await git.raw([
'config',
'--local',
'core.sshCommand',
buildSshCommand(profile.sshKey),
false,
'local'
);
buildSshCommand(profile.sshKey)
]);
await git.raw(['config', '--local', '--unset', 'credential.helper']).catch(() => {});
} else if (authType === 'token' && profile.host) {
await git.addConfig(
@@ -19,6 +19,7 @@ import {
resetToCommit,
resolveBaseRefForLog,
revertCommit,
setLocalIdentity,
stageFiles,
unstageFiles,
applyHunk,
@@ -129,6 +130,23 @@ describe('git index path validation', () => {
});
});
describe.runIf(canRunGit())('setLocalIdentity', () => {
it('configures the local SSH command with the targeted simple-git opt-in', async () => {
const { tmpDir } = await createTempRepo();
await setLocalIdentity(tmpDir, {
userName: 'SSH User',
userEmail: 'ssh@example.com',
authType: 'ssh',
sshKey: '/tmp/test key',
});
expect(runGit(tmpDir, ['config', '--local', '--get', 'core.sshCommand']).trim()).toBe(
"ssh -i '/tmp/test key' -o IdentitiesOnly=yes"
);
});
});
// ---------------------------------------------------------------------------
// applyHunk (per-hunk stage / unstage / discard)
// ---------------------------------------------------------------------------