diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 331229c..72182f4 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -32,7 +32,7 @@ import { handleDiffFiles, handleReplaceInFiles, handleReadMultipleFiles, - handleGitStatus, + handleGit, handleCompress } from './tool-handlers.js'; import { getAllowedDirs, getBlockedDirs, setAllowedDirs } from './tool-security.js'; @@ -132,7 +132,7 @@ export function setupIPC(): void { case 'diff_files': return await handleDiffFiles(args as { file1: string; file2: string; context_lines?: number }); case 'replace_in_files': return await handleReplaceInFiles(args as { path: string; glob: string; old_text: string; new_text: string }); case 'read_multiple_files':return await handleReadMultipleFiles(args as { paths: string[]; max_chars_per_file?: number }); - case 'git_status': return await handleGitStatus(args as { path?: string }); + case 'git': return await handleGit(args as any); case 'compress': return await handleCompress(args as { action: string; path: string; destination?: string; format?: string }); default: return { success: false, error: `未知工具: ${toolName}` }; } diff --git a/src/main/tool-handlers.ts b/src/main/tool-handlers.ts index 6ffe958..f056710 100644 --- a/src/main/tool-handlers.ts +++ b/src/main/tool-handlers.ts @@ -799,31 +799,37 @@ export async function handleReadMultipleFiles(params: { paths: string[]; max_cha } } -export async function handleGitStatus(params: { path?: string }): Promise { +export async function handleGit(params: { action: string; path?: string; files?: string[]; message?: string; branch?: string; remote?: string; remote_url?: string; count?: number; all?: boolean; staged?: boolean; new_branch?: boolean; delete_branch?: boolean; force?: boolean; url?: string }): Promise { try { const cwd = params.path ? path.resolve(params.path) : getWorkspaceDir(); - return new Promise((resolve) => { - const git = spawn('git', ['status', '--porcelain=v1', '-b'], { cwd, stdio: ['pipe', 'pipe', 'pipe'] }); - let stdout = ''; - let stderr = ''; + async function runGit(args: string[]): Promise<{ stdout: string; stderr: string; code: number }> { + return new Promise((resolve) => { + const git = spawn('git', args, { cwd, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, LANG: 'en_US.UTF-8' } }); + let stdout = ''; + let stderr = ''; + git.stdout.on('data', (d: Buffer) => { stdout += d.toString(); }); + git.stderr.on('data', (d: Buffer) => { stderr += d.toString(); }); + git.on('close', (code) => resolve({ stdout, stderr, code: code ?? 1 })); + git.on('error', (err) => resolve({ stdout, stderr: err.message, code: 1 })); + }); + } - git.stdout.on('data', (d: Buffer) => { stdout += d.toString(); }); - git.stderr.on('data', (d: Buffer) => { stderr += d.toString(); }); + switch (params.action) { + case 'status': { + const r = await runGit(['status', '--porcelain=v1', '-b']); + if (r.code !== 0) return { success: false, error: r.stderr || '不是 git 仓库' }; - git.on('close', (code) => { - if (code !== 0) { - resolve({ success: false, error: stderr || '不是 git 仓库' }); - return; - } - - const lines = stdout.split('\n').filter(l => l.trim()); + const lines = r.stdout.split('\n').filter(l => l.trim()); const branchLine = lines.find(l => l.startsWith('##')) || ''; const branch = branchLine.replace('## ', '').split('...')[0] || 'unknown'; + const tracking = branchLine.includes('...') ? branchLine.split('...')[1] : ''; const modified: string[] = []; const staged: string[] = []; const untracked: string[] = []; + const deleted: string[] = []; + const renamed: string[] = []; for (const line of lines) { if (line.startsWith('##')) continue; @@ -831,25 +837,179 @@ export async function handleGitStatus(params: { path?: string }): Promise { - resolve({ success: false, error: err.message }); - }); - }); + case 'log': { + const count = params.count || 20; + const format = '--oneline'; + const r = await runGit(['log', format, `--max-count=${count}`, ...(params.all ? ['--all'] : [])]); + if (r.code !== 0) return { success: false, error: r.stderr }; + const commits = r.stdout.split('\n').filter(l => l.trim()).map(l => { + const spaceIdx = l.indexOf(' '); + return { hash: l.slice(0, spaceIdx), message: l.slice(spaceIdx + 1) }; + }); + return { success: true, commits, total: commits.length }; + } + + case 'diff': { + const args = ['diff']; + if (params.staged) args.push('--cached'); + if (params.files?.length) args.push('--', ...params.files); + const r = await runGit(args); + if (r.code !== 0) return { success: false, error: r.stderr }; + const hasChanges = r.stdout.trim().length > 0; + return { success: true, diff: r.stdout, hasChanges, staged: !!params.staged }; + } + + case 'add': { + if (!params.files?.length) return { success: false, error: '请指定要暂存的文件' }; + const r = await runGit(['add', ...params.files]); + if (r.code !== 0) return { success: false, error: r.stderr }; + return { success: true, action: 'add', files: params.files }; + } + + case 'commit': { + if (!params.message) return { success: false, error: '请提供提交信息' }; + const r = await runGit(['commit', '-m', params.message]); + if (r.code !== 0) return { success: false, error: r.stderr || '提交失败(可能没有暂存的更改)' }; + return { success: true, action: 'commit', message: params.message, output: r.stdout.trim() }; + } + + case 'push': { + const args = ['push']; + if (params.remote) args.push(params.remote); + if (params.branch) args.push(params.branch); + if (params.force) args.push('--force'); + const r = await runGit(args); + if (r.code !== 0) return { success: false, error: r.stderr }; + return { success: true, action: 'push', output: r.stderr.trim() || r.stdout.trim() }; + } + + case 'pull': { + const args = ['pull']; + if (params.remote) args.push(params.remote); + if (params.branch) args.push(params.branch); + const r = await runGit(args); + if (r.code !== 0) return { success: false, error: r.stderr }; + return { success: true, action: 'pull', output: r.stdout.trim() }; + } + + case 'branch': { + if (params.new_branch && params.branch) { + const r = await runGit(['checkout', '-b', params.branch]); + if (r.code !== 0) return { success: false, error: r.stderr }; + return { success: true, action: 'branch', created: params.branch }; + } + if (params.delete_branch && params.branch) { + const r = await runGit(['branch', '-d', params.branch]); + if (r.code !== 0) return { success: false, error: r.stderr }; + return { success: true, action: 'branch', deleted: params.branch }; + } + const r = await runGit(['branch', ...(params.all ? ['-a'] : [])]); + if (r.code !== 0) return { success: false, error: r.stderr }; + const branches = r.stdout.split('\n').filter(l => l.trim()).map(l => ({ + name: l.replace(/^\*?\s*/, '').trim(), + current: l.startsWith('*') + })); + return { success: true, action: 'branch', branches }; + } + + case 'checkout': { + if (!params.branch) return { success: false, error: '请指定分支名' }; + const args = ['checkout']; + if (params.force) args.push('-f'); + args.push(params.branch); + const r = await runGit(args); + if (r.code !== 0) return { success: false, error: r.stderr }; + return { success: true, action: 'checkout', branch: params.branch }; + } + + case 'merge': { + if (!params.branch) return { success: false, error: '请指定要合并的分支' }; + const r = await runGit(['merge', params.branch]); + if (r.code !== 0) return { success: false, error: r.stderr || '合并冲突' }; + return { success: true, action: 'merge', branch: params.branch, output: r.stdout.trim() }; + } + + case 'stash': { + const subAction = params.message || 'push'; + const args = ['stash', subAction]; + if (subAction === 'push' && params.message) args.push('-m', params.message); + const r = await runGit(args); + if (r.code !== 0) return { success: false, error: r.stderr }; + return { success: true, action: 'stash', subAction, output: r.stdout.trim() }; + } + + case 'remote': { + if (params.remote_url && params.remote) { + const r = await runGit(['remote', 'add', params.remote, params.remote_url]); + if (r.code !== 0) return { success: false, error: r.stderr }; + return { success: true, action: 'remote', added: params.remote, url: params.remote_url }; + } + const args = ['remote', '-v']; + const r = await runGit(args); + if (r.code !== 0) return { success: false, error: r.stderr }; + const remotes = r.stdout.split('\n').filter(l => l.trim()).map(l => { + const [name, url] = l.split(/\s+/); + return { name, url }; + }); + return { success: true, action: 'remote', remotes }; + } + + case 'clone': { + if (!params.url) return { success: false, error: '请提供仓库 URL' }; + const destDir = params.path ? path.resolve(params.path) : getWorkspaceDir(); + const r = await runGit(['clone', params.url, destDir]); + if (r.code !== 0) return { success: false, error: r.stderr }; + return { success: true, action: 'clone', url: params.url, destination: destDir }; + } + + case 'init': { + const r = await runGit(['init']); + if (r.code !== 0) return { success: false, error: r.stderr }; + return { success: true, action: 'init', output: r.stdout.trim() }; + } + + case 'reset': { + const args = ['reset']; + if (params.staged) args.push('--cached'); + if (params.force) args.push('--hard'); + if (params.files?.length) args.push('--', ...params.files); + const r = await runGit(args); + if (r.code !== 0) return { success: false, error: r.stderr }; + return { success: true, action: 'reset', output: r.stdout.trim() }; + } + + case 'tag': { + if (params.branch) { + // branch field reused as tag name + const args = ['tag', params.branch]; + if (params.message) args.push('-m', params.message); + const r = await runGit(args); + if (r.code !== 0) return { success: false, error: r.stderr }; + return { success: true, action: 'tag', created: params.branch }; + } + const r = await runGit(['tag', '-l']); + if (r.code !== 0) return { success: false, error: r.stderr }; + const tags = r.stdout.split('\n').filter(l => l.trim()); + return { success: true, action: 'tag', tags }; + } + + default: + return { success: false, error: `未知 git 操作: ${params.action}` }; + } } catch (err) { return { success: false, error: (err as Error).message }; } diff --git a/src/renderer/index.html b/src/renderer/index.html index 01e28fa..dc3a2cc 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -461,8 +461,8 @@
一次读取多个文件内容
-
🔖git_status自动
-
查看 Git 仓库状态:分支、修改、暂存
+
🔖git自动
+
Git 全操作:status/log/diff/add/commit/push/pull/branch/checkout/merge/stash/remote/clone/init/reset/tag
🗜️compress需确认
diff --git a/src/renderer/services/tool-registry.ts b/src/renderer/services/tool-registry.ts index 08a3a2b..b481439 100644 --- a/src/renderer/services/tool-registry.ts +++ b/src/renderer/services/tool-registry.ts @@ -295,13 +295,26 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ { type: 'function', function: { - name: 'git_status', - description: 'Show git repository status: branch, modified/staged/untracked files.', + name: 'git', + description: 'Git operations: status, log, diff, add, commit, push, pull, branch, checkout, merge, stash, remote, clone, init. All operations are automatic (no confirmation).', parameters: { type: 'object', - required: [], + required: ['action'], properties: { - path: { type: 'string', description: 'Repository path. Defaults to workspace.' } + action: { type: 'string', enum: ['status', 'log', 'diff', 'add', 'commit', 'push', 'pull', 'branch', 'checkout', 'merge', 'stash', 'remote', 'clone', 'init', 'reset', 'tag'], description: 'Git action to perform.' }, + path: { type: 'string', description: 'Repository path. Defaults to workspace.' }, + files: { type: 'array', items: { type: 'string' }, description: 'Files for add/diff/checkout.' }, + message: { type: 'string', description: 'Commit message for commit action.' }, + branch: { type: 'string', description: 'Branch name for branch/checkout/merge.' }, + remote: { type: 'string', description: 'Remote name for push/pull/remote.' }, + remote_url: { type: 'string', description: 'Remote URL for remote add.' }, + count: { type: 'integer', description: 'Number of log entries. Default: 20.' }, + all: { type: 'boolean', description: 'Show all (branches, stashes, etc). Default: false.' }, + staged: { type: 'boolean', description: 'Show staged changes in diff. Default: false.' }, + new_branch: { type: 'boolean', description: 'Create new branch in branch action. Default: false.' }, + delete_branch: { type: 'boolean', description: 'Delete branch in branch action. Default: false.' }, + force: { type: 'boolean', description: 'Force push/checkout. Default: false.' }, + url: { type: 'string', description: 'Repository URL for clone.' } } } } @@ -337,7 +350,7 @@ let enabledTools: Set = new Set([ 'run_command', 'move_file', 'copy_file', 'web_fetch', 'append_file', 'edit_file', 'get_file_info', 'tree', 'download_file', 'diff_files', 'replace_in_files', - 'read_multiple_files', 'git_status', 'compress' + 'read_multiple_files', 'git', 'compress' ]); export function setToolEnabled(toolName: string, enabled: boolean): void { @@ -422,7 +435,7 @@ export function getToolIcon(name: string): string { diff_files: '🔀', replace_in_files: '🔄', read_multiple_files: '📚', - git_status: '🔖', + git: '🔖', compress: '🗜️' }; return icons[name] || '🔧'; @@ -448,7 +461,7 @@ export function formatToolName(name: string): string { diff_files: '文件对比', replace_in_files: '批量替换', read_multiple_files: '批量读取', - git_status: 'Git 状态', + git: 'Git 操作', compress: '压缩/解压' }; return names[name] || name;