Configure commit signing per Git identity Apply SSH signing settings automatically Support signing in web and VS Code
579 lines
23 KiB
TypeScript
579 lines
23 KiB
TypeScript
import * as gitService from './gitService';
|
|
import type { BridgeResponse } from './bridge';
|
|
|
|
type BridgeMessageInput = {
|
|
id: string;
|
|
type: string;
|
|
payload?: unknown;
|
|
};
|
|
|
|
const requireDirectory = (id: string, type: string, directory?: string): BridgeResponse | null => {
|
|
if (!directory) {
|
|
return { id, type, success: false, error: 'Directory is required' };
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const isValidCommitHash = (hash: string | undefined): hash is string => (
|
|
typeof hash === 'string' && /^[0-9a-fA-F]{7,40}$/.test(hash)
|
|
);
|
|
|
|
export async function handleStandardGitBridgeMessage(message: BridgeMessageInput): Promise<BridgeResponse | null> {
|
|
const { id, type, payload } = message;
|
|
|
|
switch (type) {
|
|
case 'api:git/check': {
|
|
const { directory } = (payload || {}) as { directory?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
const isRepo = await gitService.checkIsGitRepository(directory!);
|
|
return { id, type, success: true, data: isRepo };
|
|
}
|
|
|
|
case 'api:git/worktree-type': {
|
|
const { directory } = (payload || {}) as { directory?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
const isLinked = await gitService.isLinkedWorktree(directory!);
|
|
return { id, type, success: true, data: isLinked };
|
|
}
|
|
|
|
case 'api:git/status': {
|
|
const { directory, mode } = (payload || {}) as { directory?: string; mode?: 'light' };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
const status = await gitService.getGitStatus(directory!, mode === 'light' ? { mode } : undefined);
|
|
return { id, type, success: true, data: status };
|
|
}
|
|
|
|
case 'api:git/branches': {
|
|
const { directory, method, name, startPoint, force } = (payload || {}) as {
|
|
directory?: string;
|
|
method?: string;
|
|
name?: string;
|
|
startPoint?: string;
|
|
force?: boolean;
|
|
};
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
|
|
const normalizedMethod = typeof method === 'string' ? method.toUpperCase() : 'GET';
|
|
|
|
if (normalizedMethod === 'GET') {
|
|
const branches = await gitService.getGitBranches(directory!);
|
|
return { id, type, success: true, data: branches };
|
|
}
|
|
|
|
if (normalizedMethod === 'POST') {
|
|
if (!name) {
|
|
return { id, type, success: false, error: 'Branch name is required' };
|
|
}
|
|
const result = await gitService.createBranch(directory!, name, startPoint);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
if (normalizedMethod === 'DELETE') {
|
|
if (!name) {
|
|
return { id, type, success: false, error: 'Branch name is required' };
|
|
}
|
|
const result = await gitService.deleteGitBranch(directory!, name, force);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
return { id, type, success: false, error: `Unsupported method: ${normalizedMethod}` };
|
|
}
|
|
|
|
case 'api:git/remote-branches': {
|
|
const { directory, branch, remote } = (payload || {}) as {
|
|
directory?: string;
|
|
branch?: string;
|
|
remote?: string;
|
|
};
|
|
if (!directory || !branch) {
|
|
return { id, type, success: false, error: 'Directory and branch are required' };
|
|
}
|
|
const result = await gitService.deleteRemoteBranch(directory, branch, remote);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/checkout': {
|
|
const { directory, branch } = (payload || {}) as { directory?: string; branch?: string };
|
|
if (!directory || !branch) {
|
|
return { id, type, success: false, error: 'Directory and branch are required' };
|
|
}
|
|
const result = await gitService.checkoutBranch(directory, branch);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/worktrees': {
|
|
const { directory, method } = (payload || {}) as {
|
|
directory?: string;
|
|
method?: string;
|
|
body?: unknown;
|
|
directoryPath?: string;
|
|
deleteLocalBranch?: boolean;
|
|
};
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
|
|
const normalizedMethod = typeof method === 'string' ? method.toUpperCase() : 'GET';
|
|
|
|
if (normalizedMethod === 'GET') {
|
|
const worktrees = await gitService.listGitWorktrees(directory!);
|
|
return { id, type, success: true, data: worktrees };
|
|
}
|
|
|
|
if (normalizedMethod === 'POST') {
|
|
const created = await gitService.createWorktree(directory!, (payload || {}) as gitService.CreateGitWorktreePayload);
|
|
return { id, type, success: true, data: created };
|
|
}
|
|
|
|
if (normalizedMethod === 'DELETE') {
|
|
const removePayload = payload as {
|
|
body?: { directory?: string; deleteLocalBranch?: boolean };
|
|
directory?: string;
|
|
deleteLocalBranch?: boolean;
|
|
};
|
|
const bodyDirectory = typeof removePayload?.body?.directory === 'string'
|
|
? removePayload.body.directory
|
|
: '';
|
|
const legacyDirectory = typeof removePayload?.directory === 'string' ? removePayload.directory : '';
|
|
const worktreeDirectory = bodyDirectory || legacyDirectory || '';
|
|
|
|
if (!worktreeDirectory) {
|
|
return { id, type, success: false, error: 'Worktree directory is required' };
|
|
}
|
|
const removed = await gitService.removeWorktree(directory!, {
|
|
directory: worktreeDirectory,
|
|
deleteLocalBranch: removePayload?.body?.deleteLocalBranch === true || removePayload?.deleteLocalBranch === true,
|
|
});
|
|
return { id, type, success: true, data: { success: Boolean(removed) } };
|
|
}
|
|
|
|
return { id, type, success: false, error: `Unsupported method: ${normalizedMethod}` };
|
|
}
|
|
|
|
case 'api:git/worktrees/validate': {
|
|
const { directory } = (payload || {}) as { directory?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
const result = await gitService.validateWorktreeCreate(directory!, (payload || {}) as gitService.CreateGitWorktreePayload);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/worktrees/bootstrap-status': {
|
|
const { directory } = (payload || {}) as { directory?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
const result = await gitService.getWorktreeBootstrapStatus(directory!);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/worktrees/preview': {
|
|
const { directory } = (payload || {}) as { directory?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
const result = await gitService.previewWorktreeCreate(directory!, (payload || {}) as gitService.CreateGitWorktreePayload);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/validate-directory': {
|
|
const { directory, worktreeRoot } = (payload || {}) as { directory?: string; worktreeRoot?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
const result = await gitService.validateWorktreeDirectory(directory!, worktreeRoot!);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/canonicalize-worktree-state': {
|
|
const { directory } = (payload || {}) as { directory?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
const result = await gitService.canonicalizeWorktreeState(directory!);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/diff': {
|
|
const { directory, path: filePath, staged, contextLines } = (payload || {}) as {
|
|
directory?: string;
|
|
path?: string;
|
|
staged?: boolean;
|
|
contextLines?: number;
|
|
};
|
|
if (!directory || !filePath) {
|
|
return { id, type, success: false, error: 'Directory and path are required' };
|
|
}
|
|
const result = await gitService.getGitDiff(directory, filePath, staged, contextLines);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/file-diff': {
|
|
const { directory, path: filePath, staged } = (payload || {}) as {
|
|
directory?: string;
|
|
path?: string;
|
|
staged?: boolean;
|
|
};
|
|
if (!directory || !filePath) {
|
|
return { id, type, success: false, error: 'Directory and path are required' };
|
|
}
|
|
const result = await gitService.getGitFileDiff(directory, filePath, staged);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/revert': {
|
|
const { directory, path: filePath, scope } = (payload || {}) as { directory?: string; path?: string; scope?: 'all' | 'working' };
|
|
if (!directory || !filePath) {
|
|
return { id, type, success: false, error: 'Directory and path are required' };
|
|
}
|
|
await gitService.revertGitFile(directory, filePath, { scope });
|
|
return { id, type, success: true, data: { success: true } };
|
|
}
|
|
|
|
case 'api:git/stage': {
|
|
const { directory, path: filePath, paths } = (payload || {}) as { directory?: string; path?: string; paths?: string[] };
|
|
const filePaths = (Array.isArray(paths) ? paths : [filePath])
|
|
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0);
|
|
if (!directory || filePaths.length === 0) {
|
|
return { id, type, success: false, error: 'Directory and path are required' };
|
|
}
|
|
await gitService.stageGitFiles(directory, filePaths);
|
|
return { id, type, success: true, data: { success: true } };
|
|
}
|
|
|
|
case 'api:git/unstage': {
|
|
const { directory, path: filePath, paths } = (payload || {}) as { directory?: string; path?: string; paths?: string[] };
|
|
const filePaths = (Array.isArray(paths) ? paths : [filePath])
|
|
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0);
|
|
if (!directory || filePaths.length === 0) {
|
|
return { id, type, success: false, error: 'Directory and path are required' };
|
|
}
|
|
await gitService.unstageGitFiles(directory, filePaths);
|
|
return { id, type, success: true, data: { success: true } };
|
|
}
|
|
|
|
case 'api:git/apply-hunk': {
|
|
const { directory, path: filePath, patch, action } = (payload || {}) as {
|
|
directory?: string;
|
|
path?: string;
|
|
patch?: string;
|
|
action?: 'stage' | 'unstage' | 'discard';
|
|
};
|
|
if (!directory || !filePath || typeof patch !== 'string' || !patch.trim()) {
|
|
return { id, type, success: false, error: 'Directory, path, and patch are required' };
|
|
}
|
|
if (action !== 'stage' && action !== 'unstage' && action !== 'discard') {
|
|
return { id, type, success: false, error: 'action must be stage, unstage, or discard' };
|
|
}
|
|
await gitService.applyGitHunk(directory, filePath, patch, action);
|
|
return { id, type, success: true, data: { success: true } };
|
|
}
|
|
|
|
case 'api:git/commit': {
|
|
const { directory, message, addAll, files, stageFiles } = (payload || {}) as {
|
|
directory?: string;
|
|
message?: string;
|
|
addAll?: boolean;
|
|
files?: string[];
|
|
stageFiles?: string[];
|
|
};
|
|
if (!directory || !message) {
|
|
return { id, type, success: false, error: 'Directory and message are required' };
|
|
}
|
|
const result = await gitService.createGitCommit(directory, message, { addAll, files, stageFiles });
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/push': {
|
|
const { directory, remote, branch, options } = (payload || {}) as {
|
|
directory?: string;
|
|
remote?: string;
|
|
branch?: string;
|
|
options?: string[] | Record<string, unknown>;
|
|
};
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
const result = await gitService.gitPush(directory!, { remote, branch, options });
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/pull': {
|
|
const { directory, remote, branch, rebase } = (payload || {}) as {
|
|
directory?: string;
|
|
remote?: string;
|
|
branch?: string;
|
|
rebase?: boolean;
|
|
};
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
const result = await gitService.gitPull(directory!, { remote, branch, rebase });
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/fetch': {
|
|
const { directory, remote, branch } = (payload || {}) as {
|
|
directory?: string;
|
|
remote?: string;
|
|
branch?: string;
|
|
};
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
const result = await gitService.gitFetch(directory!, { remote, branch });
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/stashes': {
|
|
const { directory } = (payload || {}) as { directory?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
return { id, type, success: true, data: { stashes: await gitService.listGitStashes(directory!) } };
|
|
}
|
|
|
|
case 'api:git/stashes/file-counts': {
|
|
const { directory, refs } = (payload || {}) as { directory?: string; refs?: string[] };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
return { id, type, success: true, data: { counts: await gitService.countGitStashFiles(directory!, refs ?? []) } };
|
|
}
|
|
|
|
case 'api:git/stash': {
|
|
const { directory, message } = (payload || {}) as { directory?: string; message?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
return { id, type, success: true, data: await gitService.stashGitChanges(directory!, { message }) };
|
|
}
|
|
|
|
case 'api:git/stash/apply':
|
|
case 'api:git/stash/pop':
|
|
case 'api:git/stash/drop': {
|
|
const { directory, ref } = (payload || {}) as { directory?: string; ref?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
const stashRef = ref || 'stash@{0}';
|
|
const data = type === 'api:git/stash/apply'
|
|
? await gitService.applyGitStash(directory!, { ref: stashRef })
|
|
: type === 'api:git/stash/pop'
|
|
? await gitService.popGitStash(directory!, { ref: stashRef })
|
|
: await gitService.dropGitStash(directory!, { ref: stashRef });
|
|
return { id, type, success: true, data };
|
|
}
|
|
|
|
case 'api:git/remotes': {
|
|
const { directory, method, remote } = (payload || {}) as {
|
|
directory?: string;
|
|
method?: string;
|
|
remote?: string;
|
|
};
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
|
|
const normalizedMethod = typeof method === 'string' ? method.toUpperCase() : 'GET';
|
|
if (normalizedMethod === 'GET') {
|
|
const result = await gitService.getRemotes(directory!);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
if (normalizedMethod === 'DELETE') {
|
|
if (!remote) {
|
|
return { id, type, success: false, error: 'Remote name is required' };
|
|
}
|
|
const result = await gitService.removeRemote(directory!, remote);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
return { id, type, success: false, error: `Unsupported method: ${normalizedMethod}` };
|
|
}
|
|
|
|
case 'api:git/rebase': {
|
|
const { directory, onto } = (payload || {}) as { directory?: string; onto?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
if (!onto) {
|
|
return { id, type, success: false, error: 'onto is required' };
|
|
}
|
|
const result = await gitService.rebase(directory!, { onto });
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/rebase/abort': {
|
|
const { directory } = (payload || {}) as { directory?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
const result = await gitService.abortRebase(directory!);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/merge': {
|
|
const { directory, branch } = (payload || {}) as { directory?: string; branch?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
if (!branch) {
|
|
return { id, type, success: false, error: 'branch is required' };
|
|
}
|
|
const result = await gitService.merge(directory!, { branch });
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/merge/abort': {
|
|
const { directory } = (payload || {}) as { directory?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
const result = await gitService.abortMerge(directory!);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/rebase/continue': {
|
|
const { directory } = (payload || {}) as { directory?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
const result = await gitService.continueRebase(directory!);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/merge/continue': {
|
|
const { directory } = (payload || {}) as { directory?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
const result = await gitService.continueMerge(directory!);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/checkout-commit': {
|
|
const { directory, hash } = (payload || {}) as { directory?: string; hash?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
if (!isValidCommitHash(hash)) {
|
|
return { id, type, success: false, error: 'Invalid commit hash' };
|
|
}
|
|
const result = await gitService.checkoutCommit(directory!, hash);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/cherry-pick': {
|
|
const { directory, hash } = (payload || {}) as { directory?: string; hash?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
if (!isValidCommitHash(hash)) {
|
|
return { id, type, success: false, error: 'Invalid commit hash' };
|
|
}
|
|
const result = await gitService.cherryPick(directory!, hash);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/revert-commit': {
|
|
const { directory, hash } = (payload || {}) as { directory?: string; hash?: string };
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
if (!isValidCommitHash(hash)) {
|
|
return { id, type, success: false, error: 'Invalid commit hash' };
|
|
}
|
|
const result = await gitService.revertCommit(directory!, hash);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/reset-to-commit': {
|
|
const { directory, hash, mode, force } = (payload || {}) as {
|
|
directory?: string;
|
|
hash?: string;
|
|
mode?: 'soft' | 'mixed' | 'hard';
|
|
force?: boolean;
|
|
};
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
if (!isValidCommitHash(hash)) {
|
|
return { id, type, success: false, error: 'Invalid commit hash' };
|
|
}
|
|
if (!mode || !['soft', 'mixed', 'hard'].includes(mode)) {
|
|
return { id, type, success: false, error: 'mode must be soft, mixed, or hard' };
|
|
}
|
|
const result = await gitService.resetToCommit(directory!, hash, mode, force);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/log': {
|
|
const { directory, maxCount, from, to, file, all } = (payload || {}) as {
|
|
directory?: string;
|
|
maxCount?: number;
|
|
from?: string;
|
|
to?: string;
|
|
file?: string;
|
|
all?: boolean;
|
|
};
|
|
const dirError = requireDirectory(id, type, directory);
|
|
if (dirError) return dirError;
|
|
const result = await gitService.getGitLog(directory!, { maxCount, from, to, file, all });
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/commit-files': {
|
|
const { directory, hash } = (payload || {}) as { directory?: string; hash?: string };
|
|
if (!directory || !hash) {
|
|
return { id, type, success: false, error: 'Directory and hash are required' };
|
|
}
|
|
const result = await gitService.getCommitFiles(directory, hash);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/commit-file-diff': {
|
|
const { directory, hash, path: filePath, binary } = (payload || {}) as {
|
|
directory?: string;
|
|
hash?: string;
|
|
path?: string;
|
|
binary?: boolean;
|
|
};
|
|
if (!directory || !hash || !filePath) {
|
|
return { id, type, success: false, error: 'Directory, hash, and path are required' };
|
|
}
|
|
if (!/^[0-9a-fA-F]{7,40}$/.test(hash)) {
|
|
return { id, type, success: false, error: 'hash must be a valid commit SHA' };
|
|
}
|
|
const result = await gitService.getCommitFileDiff(directory, hash, filePath, Boolean(binary));
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
case 'api:git/identity': {
|
|
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;
|
|
|
|
const normalizedMethod = typeof method === 'string' ? method.toUpperCase() : 'GET';
|
|
|
|
if (normalizedMethod === 'GET') {
|
|
const identity = await gitService.getCurrentGitIdentity(directory!);
|
|
return { id, type, success: true, data: identity };
|
|
}
|
|
|
|
if (normalizedMethod === 'POST') {
|
|
if (!userName || !userEmail) {
|
|
return { id, type, success: false, error: 'userName and userEmail are required' };
|
|
}
|
|
const result = await gitService.setGitIdentity(
|
|
directory!,
|
|
userName,
|
|
userEmail,
|
|
sshKey,
|
|
signCommits === true,
|
|
signingKey ?? null
|
|
);
|
|
return { id, type, success: true, data: result };
|
|
}
|
|
|
|
return { id, type, success: false, error: `Unsupported method: ${normalizedMethod}` };
|
|
}
|
|
|
|
case 'api:git/ignore-openchamber': {
|
|
return { id, type, success: true, data: { success: true } };
|
|
}
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|