feat: git 工具升级为全操作支持(16 个子操作)

git 工具支持的操作:
- status: 查看状态(分支/修改/暂存/未追踪/删除/重命名)
- log: 提交历史
- diff: 差异查看(支持 staged)
- add: 暂存文件
- commit: 提交
- push/pull: 推送/拉取
- branch: 列出/创建/删除分支
- checkout: 切换分支
- merge: 合并分支
- stash: 暂存更改
- remote: 查看/添加远程仓库
- clone: 克隆仓库
- init: 初始化仓库
- reset: 重置
- tag: 标签管理

全部自动执行,无需用户确认
This commit is contained in:
OpenClaw Agent
2026-04-16 09:55:33 +08:00
parent 396100335a
commit 80158cb5bf
4 changed files with 213 additions and 40 deletions
+189 -29
View File
@@ -799,31 +799,37 @@ export async function handleReadMultipleFiles(params: { paths: string[]; max_cha
}
}
export async function handleGitStatus(params: { path?: string }): Promise<ToolResult> {
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<ToolResult> {
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<ToolRe
const file = line.slice(3);
if (status === '??') untracked.push(file);
else {
if (status[0] !== ' ' && status[0] !== '?') staged.push(file);
if (status[1] !== ' ' && status[1] !== '?') modified.push(file);
if (status[0] === 'D') deleted.push(file);
else if (status[0] === 'R') renamed.push(file);
else if (status[0] !== ' ' && status[0] !== '?') staged.push(file);
if (status[1] === 'M') modified.push(file);
else if (status[1] === 'D') deleted.push(file);
}
}
resolve({
success: true,
branch,
modified,
staged,
untracked,
isClean: modified.length === 0 && staged.length === 0 && untracked.length === 0
});
});
return {
success: true, branch, tracking, modified, staged, untracked, deleted, renamed,
isClean: modified.length === 0 && staged.length === 0 && untracked.length === 0 && deleted.length === 0
};
}
git.on('error', (err) => {
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 };
}