diff --git a/Plugin/GitOperator/CHANGELOG.md b/Plugin/GitOperator/CHANGELOG.md new file mode 100644 index 000000000..cd824a6d5 --- /dev/null +++ b/Plugin/GitOperator/CHANGELOG.md @@ -0,0 +1,41 @@ +# GitOperator 变更日志 + +## [1.0.0] - 2026-03-02 + +### 阶段 1 - 核心基础设施 & 只读指令 +- 初始插件骨架搭建: manifest, config.env, repos.json, CHANGELOG +- 核心工具函数: `loadEnvConfig()`, `loadRepos()`, `saveRepos()`, `resolveProfile()`, `buildAllowedPaths()`, `validatePath()`, `sanitizeOutput()`, `execGit()`, `ensureRemotes()` +- 通过 `parseSerialCommands()` 实现 `commandN` 语法的串行调用支持 +- 只读指令: **Status**, **Log** (结构化 JSON 输出), **Diff** (自动截断), **BranchList**, **RemoteInfo**, **StashList**, **TagList**, **ProfileList** +- 写操作指令: **Add**, **Commit** +- 配置档管理: **ProfileAdd**, **ProfileEdit**, **ProfileRemove** + +### 阶段 2 - 远程协作 & 凭证注入 +- 凭证注入: `injectCredentials()` — Token 仅在内存中注入到 HTTPS URL,不会写入 `.git/config` +- 远程仓库路由: `resolveSourceRemote()` — 根据 `source` 参数路由到 pull/push 配置 +- 远程协作指令: **Pull**, **Push**, **Fetch**, **Clone** (60秒超时) +- 一键上游同步: **SyncUpstream** ⭐ — 6步流水线: 获取 → 暂存 → 合并/变基 → 冲突检测 → 恢复暂存 → 推送 +- 所有输出通过 `sanitizeOutput()` 进行 Token 脱敏 + +### 阶段 3 - 分支管理 +- 分支管理指令: **BranchCreate** (支持可选的 startPoint 起点), **Checkout**, **Merge** +- 合并冲突检测,冲突时自动执行 `--abort` 以保持干净的工作区 + +### 阶段 4 - 危险操作 & Auth 守卫 +- Auth 守卫: `requireAuth()` — 对照环境变量 `DECRYPTED_AUTH_CODE` 验证 `authCode`,三重拦截(环境未配置 / 未传验证码 / 验证码错误) +- 受保护指令 (🔒): **ForcePush**, **ResetHard** (含恢复提示), **BranchDelete** (当前分支保护 + 可选远程分支删除), **Rebase** (冲突自动中止), **CherryPick** (冲突自动中止) +- `DANGEROUS_COMMANDS` Set 集合,用于程序化识别危险操作 + +### 安全架构 +- **路径白名单**: `PLUGIN_WORK_PATHS` 约束所有文件系统操作 +- **Token 脱敏**: `sanitizeOutput()` 拦截所有包含凭证的输出 +- **凭证隔离**: Token 仅存在于内存中的 URL 对象,不会持久化到 git 配置 +- **Auth 验证**: 5 个危险指令需要 6 位数验证码 +- **冲突安全**: Merge/Rebase/SyncUpstream/CherryPick 在冲突时自动中止 +- **分支保护**: BranchDelete 拒绝删除当前所在分支 +- **恢复提示**: ResetHard/Rebase 输出 `recoveryHint` 用于回滚 + +--- + +**总计: 22 条 Git 指令 + 3 条配置档管理指令 = 25 条调用指令** +**作者: Nova & hjhjd** \ No newline at end of file diff --git a/Plugin/GitOperator/GitOperator.js b/Plugin/GitOperator/GitOperator.js new file mode 100644 index 000000000..e31bcb698 --- /dev/null +++ b/Plugin/GitOperator/GitOperator.js @@ -0,0 +1,634 @@ +#!/usr/bin/env node +'use strict'; + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const { getAuthCode } = require('../../modules/captchaDecoder'); + +// ============================================================ +// GitOperator - VCP Git 仓库管理器 +// Version: 1.0.1 +// Author: Nova & hjhjd +// ============================================================ + +const REPOS_FILE = path.resolve(__dirname, 'repos.json'); +const ENV_FILE = path.resolve(__dirname, 'config.env'); +const DEBUG_LOG = path.resolve(__dirname, 'debug.log'); + +// --- 工具函数 --- + +function loadEnvConfig() { + const config = {}; + if (fs.existsSync(ENV_FILE)) { + const lines = fs.readFileSync(ENV_FILE, 'utf8').split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eqIndex = trimmed.indexOf('='); + if (eqIndex > 0) { + config[trimmed.slice(0, eqIndex).trim()] = trimmed.slice(eqIndex + 1).trim(); + } + } + } + return config; +} + +function loadRepos() { + if (!fs.existsSync(REPOS_FILE)) { + fs.writeFileSync(REPOS_FILE, JSON.stringify({ defaultProfile: '', profiles: {} }, null, 2)); + } + return JSON.parse(fs.readFileSync(REPOS_FILE, 'utf8')); +} + +function saveRepos(repos) { + fs.writeFileSync(REPOS_FILE, JSON.stringify(repos, null, 2)); +} + +function success(data) { + console.log(JSON.stringify({ status: 'success', result: data })); +} + +function error(message) { + console.log(JSON.stringify({ status: 'error', error: message })); +} + +function debugLog(msg) { + try { + fs.appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] ${msg}\n`); + } catch (e) { /* ignore */ } +} + +function sanitizeOutput(text, token) { + if (!token || !text) return text; + const masked = token.slice(0, 4) + '****' + token.slice(-4); + return text.split(token).join(masked); +} + +function resolveProfile(args, repos) { + const profileName = args.profile || repos.defaultProfile; + if (!profileName) return { error: '未指定 profile,且未设置 defaultProfile。请用 ProfileAdd 创建或指定 profile 参数。' }; + const profile = repos.profiles[profileName]; + if (!profile) return { error: `Profile "${profileName}" 不存在。可用: ${Object.keys(repos.profiles).join(', ') || '无'}` }; + if (!profile.localPath || !fs.existsSync(profile.localPath)) { + return { error: `Profile "${profileName}" 的 localPath "${profile.localPath}" 不存在或未配置。` }; + } + // --- 字段归一化:兼容嵌套结构和扁平结构 --- + if (profile.push) { + if (!profile.pushUrl) profile.pushUrl = profile.push.url; + if (!profile.pushRemote) profile.pushRemote = profile.push.remote; + if (!profile.pushBranch) profile.pushBranch = profile.push.branch; + } + if (profile.pull) { + if (!profile.pullUrl) profile.pullUrl = profile.pull.url; + if (!profile.pullRemote) profile.pullRemote = profile.pull.remote; + if (!profile.pullBranch) profile.pullBranch = profile.pull.branch; + } + if (profile.credentials) { + if (!profile.token) profile.token = profile.credentials.token; + if (!profile.email) profile.email = profile.credentials.email; + if (!profile.username) profile.username = profile.credentials.username; + } + return { profileName, profile }; +} + +function validateWorkPath(localPath, envConfig) { + const allowedPaths = (envConfig.PLUGIN_WORK_PATHS || '../../').split(',').map(p => { + const resolved = path.resolve(__dirname, p.trim()); + return resolved; + }); + const resolvedLocal = path.resolve(localPath); + return allowedPaths.some(ap => resolvedLocal.startsWith(ap)); +} + +function execGit(cmd, cwd, token) { + try { + const result = execSync(cmd, { cwd, encoding: 'utf8', timeout: 25000, maxBuffer: 1024 * 1024 * 5 }); + return { ok: true, output: sanitizeOutput(result.trim(), token) }; + } catch (e) { + const stderr = e.stderr ? e.stderr.toString() : ''; + const stdout = e.stdout ? e.stdout.toString() : ''; + return { ok: false, output: sanitizeOutput(stderr || stdout || e.message, token) }; + } +} + +function injectCredentials(url, token) { + if (!token) return url; + try { + const u = new URL(url); + u.username = 'x-access-token'; + u.password = token; + return u.toString(); + } catch { + return url; + } +} + +// --- 串行指令解析 --- + +function parseSerialCommands(input) { + const commands = []; + let i = 1; + + // 只有明确存在 command1 时才进入串行解析路径 + while (input[`command${i}`]) { + const cmd = input[`command${i}`]; + const args = {}; + const suffix = String(i); + for (const [key, value] of Object.entries(input)) { + if (key === `command${i}`) continue; + if (key.endsWith(suffix) && key !== `command${suffix}`) { + const baseKey = key.slice(0, -suffix.length); + args[baseKey] = value; + } + } + + // 继承全局 profile + if (input.profile && !args.profile) args.profile = input.profile; + + commands.push({ command: cmd, args }); + i++; + } + + // 单指令 fallback:command1 不存在,但 command 存在 -> 直接透传全部参数 + if (commands.length === 0 && input.command) { + const args = { ...input }; + delete args.command; + commands.push({ command: input.command, args }); + } + + return commands; +} + +// --- 指令实现 --- + +function cmdStatus(args, profile, envConfig) { + const { ok, output } = execGit('git status', profile.localPath, profile.token); + if (!ok) return error(`git status 失败: ${output}`); + success({ command: 'Status', output }); +} + +function cmdLog(args, profile, envConfig) { + const maxCount = parseInt(args.maxCount) || 20; + const branch = args.branch || ''; + const format = '--pretty=format:{"hash":"%H","short":"%h","author":"%an","date":"%ai","subject":"%s"}'; + const { ok, output } = execGit(`git log ${format} -${maxCount} ${branch}`.trim(), profile.localPath, profile.token); + if (!ok) return error(`git log 失败: ${output}`); + try { + const logs = output.split('\n').filter(Boolean).map(line => JSON.parse(line)); + success({ command: 'Log', count: logs.length, logs }); + } catch { + success({ command: 'Log', output }); + } +} + +function cmdDiff(args, profile, envConfig) { + const target = args.target || ''; + const maxLines = parseInt(args.maxLines) || 200; + const cmd = target ? `git diff ${target}` : 'git diff'; + const { ok, output } = execGit(cmd, profile.localPath, profile.token); + if (!ok) return error(`git diff 失败: ${output}`); + const lines = output.split('\n'); + const truncated = lines.length > maxLines; + success({ + command: 'Diff', + totalLines: lines.length, + truncated, + output: truncated ? lines.slice(0, maxLines).join('\n') + `\n... (${lines.length - maxLines} lines truncated)` : output + }); +} + +function cmdBranchList(args, profile, envConfig) { + const { ok, output } = execGit('git branch -a', profile.localPath, profile.token); + if (!ok) return error(`git branch 失败: ${output}`); + success({ command: 'BranchList', output }); +} + +function cmdRemoteInfo(args, profile, envConfig) { + const { ok, output } = execGit('git remote -v', profile.localPath, profile.token); + if (!ok) return error(`git remote 失败: ${output}`); + success({ command: 'RemoteInfo', output: sanitizeOutput(output, profile.token) }); +} + +function cmdStashList(args, profile, envConfig) { + const { ok, output } = execGit('git stash list', profile.localPath, profile.token); + if (!ok) return error(`git stash list 失败: ${output}`); + success({ command: 'StashList', output: output || '(no stash entries)' }); +} + +function cmdTagList(args, profile, envConfig) { + const { ok, output } = execGit('git tag -l', profile.localPath, profile.token); + if (!ok) return error(`git tag 失败: ${output}`); + success({ command: 'TagList', output: output || '(no tags)' }); +} + +function cmdProfileList(args, repos) { + const list = {}; + for (const [name, p] of Object.entries(repos.profiles)) { + list[name] = { + localPath: p.localPath, + pushRemote: p.pushRemote || 'origin', + pushBranch: p.pushBranch || 'main', + pullRemote: p.pullRemote || 'upstream', + pullBranch: p.pullBranch || 'main', + mergeStrategy: p.mergeStrategy || 'merge', + hasToken: !!p.token + }; + } + success({ command: 'ProfileList', defaultProfile: repos.defaultProfile, profiles: list }); +} + +function cmdAdd(args, profile, envConfig) { + if (!args.files) return error('Add 指令需要 "files" 参数。用 "." 表示全部,多个文件空格分隔。'); + const { ok, output } = execGit(`git add ${args.files}`, profile.localPath, profile.token); + if (!ok) return error(`git add 失败: ${output}`); + success({ command: 'Add', files: args.files, output: output || 'staged successfully' }); +} + +function cmdCommit(args, profile, envConfig) { + if (!args.message) return error('Commit 指令需要 "message" 参数。'); + const safeMsg = args.message.replace(/"/g, '\\"'); + const { ok, output } = execGit(`git commit -m "${safeMsg}"`, profile.localPath, profile.token); + if (!ok) return error(`git commit 失败: ${output}`); + success({ command: 'Commit', output }); +} + +function cmdPull(args, profile, envConfig) { + const source = args.source || 'pull'; + let remote, branch; + if (source === 'push') { + remote = profile.pushRemote || 'origin'; + branch = profile.pushBranch || 'main'; + } else { + remote = profile.pullRemote || 'upstream'; + branch = profile.pullBranch || 'main'; + } + const { ok, output } = execGit(`git pull ${remote} ${branch}`, profile.localPath, profile.token); + if (!ok) return error(`git pull 失败: ${output}`); + success({ command: 'Pull', remote, branch, output }); +} + +function cmdPush(args, profile, envConfig) { + const remote = profile.pushRemote || 'origin'; + const branch = profile.pushBranch || 'main'; + const pushUrl = profile.pushUrl; + let cmd; + if (pushUrl && profile.token) { + const authedUrl = injectCredentials(pushUrl, profile.token); + cmd = `git push ${authedUrl} HEAD:${branch}`; + } else { + cmd = `git push ${remote} ${branch}`; + } + const { ok, output } = execGit(cmd, profile.localPath, profile.token); + if (!ok) return error(`git push 失败: ${sanitizeOutput(output, profile.token)}`); + success({ command: 'Push', remote, branch, output: sanitizeOutput(output, profile.token) || 'pushed successfully' }); +} + +function cmdFetch(args, profile, envConfig) { + const source = args.source || 'pull'; + let remote; + if (source === 'push') { + remote = profile.pushRemote || 'origin'; + } else { + remote = profile.pullRemote || 'upstream'; + } + const { ok, output } = execGit(`git fetch ${remote}`, profile.localPath, profile.token); + if (!ok) return error(`git fetch 失败: ${output}`); + success({ command: 'Fetch', remote, output: output || 'fetched successfully' }); +} + +function cmdBranchCreate(args, profile, envConfig) { + if (!args.branchName) return error('BranchCreate 需要 "branchName" 参数。'); + const startPoint = args.startPoint || 'HEAD'; + const { ok, output } = execGit(`git branch ${args.branchName} ${startPoint}`, profile.localPath, profile.token); + if (!ok) return error(`git branch 创建失败: ${output}`); + success({ command: 'BranchCreate', branchName: args.branchName, output: output || 'branch created' }); +} + +function cmdCheckout(args, profile, envConfig) { + if (!args.branch) return error('Checkout 需要 "branch" 参数。'); + const { ok, output } = execGit(`git checkout ${args.branch}`, profile.localPath, profile.token); + if (!ok) return error(`git checkout 失败: ${output}`); + success({ command: 'Checkout', branch: args.branch, output: output || 'switched' }); +} + +function cmdMerge(args, profile, envConfig) { + if (!args.branch) return error('Merge 需要 "branch" 参数(源分支名)。'); + const { ok, output } = execGit(`git merge ${args.branch}`, profile.localPath, profile.token); + if (!ok) return error(`git merge 失败: ${output}`); + success({ command: 'Merge', branch: args.branch, output }); +} + +function cmdClone(args) { + if (!args.url) return error('Clone 需要 "url" 参数。'); + if (!args.localPath) return error('Clone 需要 "localPath" 参数。'); + const { ok, output } = execGit(`git clone ${args.url} ${args.localPath}`, process.cwd(), null); + if (!ok) return error(`git clone 失败: ${output}`); + if (args.profile) { + const repos = loadRepos(); + repos.profiles[args.profile] = { localPath: path.resolve(args.localPath) }; + if (!repos.defaultProfile) repos.defaultProfile = args.profile; + saveRepos(repos); + } + success({ command: 'Clone', output: output || 'cloned successfully' }); +} + +function cmdSyncUpstream(args, profile, envConfig) { + const pullRemote = profile.pullRemote || 'upstream'; + const pullBranch = profile.pullBranch || 'main'; + const pushRemote = profile.pushRemote || 'origin'; + const pushBranch = profile.pushBranch || 'main'; + const strategy = profile.mergeStrategy || 'merge'; + const cwd = profile.localPath; + const token = profile.token; + const steps = []; + + // Step 1: fetch upstream + let r = execGit(`git fetch ${pullRemote}`, cwd, token); + steps.push({ step: 'fetch', ok: r.ok, output: r.output }); + if (!r.ok) return error(`SyncUpstream 失败 (fetch): ${r.output}`); + + // Step 2: stash if dirty + const statusR = execGit('git status --porcelain', cwd, token); + const isDirty = statusR.ok && statusR.output.trim().length > 0; + if (isDirty) { + r = execGit('git stash push -m "VCP-auto-stash"', cwd, token); + steps.push({ step: 'stash', ok: r.ok, output: r.output }); + if (!r.ok) return error(`SyncUpstream 失败 (stash): ${r.output}`); + } + + // Step 3: merge or rebase + const mergeCmd = strategy === 'rebase' + ? `git rebase ${pullRemote}/${pullBranch}` + : `git merge ${pullRemote}/${pullBranch}`; + r = execGit(mergeCmd, cwd, token); + steps.push({ step: strategy, ok: r.ok, output: r.output }); + if (!r.ok) { + // abort on conflict + execGit(strategy === 'rebase' ? 'git rebase --abort' : 'git merge --abort', cwd, token); + if (isDirty) execGit('git stash pop', cwd, token); + const conflictR = execGit('git diff --name-only --diff-filter=U', cwd, token); + return error(`SyncUpstream 冲突! 已自动中止。冲突文件: ${conflictR.output || '(unknown)'}。请手动解决。`); + } + + // Step 4: stash pop + if (isDirty) { + r = execGit('git stash pop', cwd, token); + steps.push({ step: 'stash-pop', ok: r.ok, output: r.output }); + } + + // Step 5: push to origin + const pushUrl = profile.pushUrl; + let pushCmd; + if (pushUrl && token) { + pushCmd = `git push ${injectCredentials(pushUrl, token)} HEAD:${pushBranch}`; + } else { + pushCmd = `git push ${pushRemote} ${pushBranch}`; + } + r = execGit(pushCmd, cwd, token); + steps.push({ step: 'push', ok: r.ok, output: sanitizeOutput(r.output, token) }); + + success({ command: 'SyncUpstream', steps }); +} + +// --- 危险操作 --- + +function cmdForcePush(args, profile, envConfig) { + const remote = profile.pushRemote || 'origin'; + const branch = profile.pushBranch || 'main'; + const pushUrl = profile.pushUrl; + let cmd; + if (pushUrl && profile.token) { + cmd = `git push --force ${injectCredentials(pushUrl, profile.token)} HEAD:${branch}`; + } else { + cmd = `git push --force ${remote} ${branch}`; + } + const { ok, output } = execGit(cmd, profile.localPath, profile.token); + if (!ok) return error(`ForcePush 失败: ${sanitizeOutput(output, profile.token)}`); + success({ command: 'ForcePush', output: sanitizeOutput(output, profile.token) || 'force pushed' }); +} + +function cmdResetHard(args, profile, envConfig) { + const target = args.target || 'HEAD'; + const { ok, output } = execGit(`git reset --hard ${target}`, profile.localPath, profile.token); + if (!ok) return error(`ResetHard 失败: ${output}`); + success({ command: 'ResetHard', target, output }); +} + +function cmdBranchDelete(args, profile, envConfig) { + if (!args.branchName) return error('BranchDelete 需要 "branchName" 参数。'); + const { ok, output } = execGit(`git branch -D ${args.branchName}`, profile.localPath, profile.token); + if (!ok) return error(`BranchDelete 失败: ${output}`); + success({ command: 'BranchDelete', branchName: args.branchName, output }); +} + +function cmdRebase(args, profile, envConfig) { + if (!args.onto) return error('Rebase 需要 "onto" 参数。'); + const { ok, output } = execGit(`git rebase ${args.onto}`, profile.localPath, profile.token); + if (!ok) { + execGit('git rebase --abort', profile.localPath, profile.token); + return error(`Rebase 失败 (已自动 abort): ${output}`); + } + success({ command: 'Rebase', onto: args.onto, output }); +} + +function cmdCherryPick(args, profile, envConfig) { + if (!args.commitHash) return error('CherryPick 需要 "commitHash" 参数。'); + const { ok, output } = execGit(`git cherry-pick ${args.commitHash}`, profile.localPath, profile.token); + if (!ok) { + execGit('git cherry-pick --abort', profile.localPath, profile.token); + return error(`CherryPick 失败 (已自动 abort): ${output}`); + } + success({ command: 'CherryPick', commitHash: args.commitHash, output }); +} + +// --- Profile 管理 --- + +function cmdProfileAdd(args) { + if (!args.profileName) return error('ProfileAdd 需要 "profileName" 参数。'); + if (!args.localPath) return error('ProfileAdd 需要 "localPath" 参数。'); + const repos = loadRepos(); + if (repos.profiles[args.profileName]) return error(`Profile "${args.profileName}" 已存在。请用 ProfileEdit 修改。`); + const newProfile = { localPath: path.resolve(args.localPath) }; + const fields = ['pushUrl', 'pushRemote', 'pushBranch', 'pullUrl', 'pullRemote', 'pullBranch', 'email', 'username', 'token', 'mergeStrategy']; + for (const f of fields) { + if (args[f]) newProfile[f] = args[f]; + } + repos.profiles[args.profileName] = newProfile; + if (!repos.defaultProfile) repos.defaultProfile = args.profileName; + saveRepos(repos); + + // auto-configure git remotes + const cwd = newProfile.localPath; + if (fs.existsSync(cwd)) { + if (newProfile.pushUrl) { + execGit(`git remote set-url ${newProfile.pushRemote || 'origin'} ${newProfile.pushUrl}`, cwd, null); + } + if (newProfile.pullUrl) { + const pullRemote = newProfile.pullRemote || 'upstream'; + const checkRemote = execGit(`git remote get-url ${pullRemote}`, cwd, null); + if (!checkRemote.ok) { + execGit(`git remote add ${pullRemote} ${newProfile.pullUrl}`, cwd, null); + } else { + execGit(`git remote set-url ${pullRemote} ${newProfile.pullUrl}`, cwd, null); + } + } + if (newProfile.email) execGit(`git config user.email "${newProfile.email}"`, cwd, null); + if (newProfile.username) execGit(`git config user.name "${newProfile.username}"`, cwd, null); + + // configure safe.directory + execGit(`git config --global --add safe.directory ${cwd.replace(/\\/g, '/')}`, cwd, null); + } + + success({ command: 'ProfileAdd', profileName: args.profileName, profile: { ...newProfile, token: newProfile.token ? '****' : undefined } }); +} + +function cmdProfileEdit(args) { + if (!args.profileName) return error('ProfileEdit 需要 "profileName" 参数。'); + const repos = loadRepos(); + if (!repos.profiles[args.profileName]) return error(`Profile "${args.profileName}" 不存在。`); + const profile = repos.profiles[args.profileName]; + const fields = ['localPath', 'pushUrl', 'pushRemote', 'pushBranch', 'pullUrl', 'pullRemote', 'pullBranch', 'email', 'username', 'token', 'mergeStrategy']; + for (const f of fields) { + if (args[f] !== undefined) { + profile[f] = f === 'localPath' ? path.resolve(args[f]) : args[f]; + } + } + saveRepos(repos); + success({ command: 'ProfileEdit', profileName: args.profileName, updated: Object.keys(args).filter(k => fields.includes(k)) }); +} + +function cmdProfileRemove(args) { + if (!args.profileName) return error('ProfileRemove 需要 "profileName" 参数。'); + const repos = loadRepos(); + if (!repos.profiles[args.profileName]) return error(`Profile "${args.profileName}" 不存在。`); + delete repos.profiles[args.profileName]; + if (repos.defaultProfile === args.profileName) repos.defaultProfile = Object.keys(repos.profiles)[0] || ''; + saveRepos(repos); + success({ command: 'ProfileRemove', profileName: args.profileName }); +} + +// --- 指令分发 --- + +async function dispatchCommand(command, args, repos, envConfig) { + debugLog(`Dispatch: ${command} | args: ${JSON.stringify(args)}`); + + // 无需 profile 的指令 + const noProfileCmds = ['ProfileList', 'ProfileAdd', 'ProfileEdit', 'ProfileRemove', 'Clone']; + if (noProfileCmds.includes(command)) { + switch (command) { + case 'ProfileList': return cmdProfileList(args, repos); + case 'ProfileAdd': return cmdProfileAdd(args); + case 'ProfileEdit': return cmdProfileEdit(args); + case 'ProfileRemove': return cmdProfileRemove(args); + case 'Clone': return cmdClone(args); + } + } + + // 需要 profile 的指令 + const resolved = resolveProfile(args, repos); + if (resolved.error) return error(resolved.error); + const { profileName, profile } = resolved; + + if (!validateWorkPath(profile.localPath, envConfig)) { + return error(`安全限制: "${profile.localPath}" 不在允许的工作路径内。请检查 .env 的 PLUGIN_WORK_PATHS。`); + } + + // 危险操作需要 requireAdmin + const dangerousCmds = ['ForcePush', 'ResetHard', 'BranchDelete', 'Rebase', 'CherryPick']; + if (dangerousCmds.includes(command)) { + if (!args.requireAdmin) return error(`"${command}" 是危险操作,需要 requireAdmin 验证码。`); + // ★ P0 Fix: 读取真实验证码并校验(不再只检查存在性) + const codePath = path.join(__dirname, '..', 'UserAuth', 'code.bin'); + const realCode = await getAuthCode(codePath); + if (!realCode) { + return error('无法读取认证码文件,拒绝执行危险操作。'); + } + if (String(args.requireAdmin).trim() !== realCode) { + return error('验证码错误,拒绝执行危险操作。'); + } + } + + switch (command) { + case 'Status': return cmdStatus(args, profile, envConfig); + case 'Log': return cmdLog(args, profile, envConfig); + case 'Diff': return cmdDiff(args, profile, envConfig); + case 'BranchList': return cmdBranchList(args, profile, envConfig); + case 'RemoteInfo': return cmdRemoteInfo(args, profile, envConfig); + case 'StashList': return cmdStashList(args, profile, envConfig); + case 'TagList': return cmdTagList(args, profile, envConfig); + case 'Add': return cmdAdd(args, profile, envConfig); + case 'Commit': return cmdCommit(args, profile, envConfig); + case 'Pull': return cmdPull(args, profile, envConfig); + case 'Push': return cmdPush(args, profile, envConfig); + case 'Fetch': return cmdFetch(args, profile, envConfig); + case 'BranchCreate': return cmdBranchCreate(args, profile, envConfig); + case 'Checkout': return cmdCheckout(args, profile, envConfig); + case 'Merge': return cmdMerge(args, profile, envConfig); + case 'SyncUpstream': return cmdSyncUpstream(args, profile, envConfig); + case 'ForcePush': return cmdForcePush(args, profile, envConfig); + case 'ResetHard': return cmdResetHard(args, profile, envConfig); + case 'BranchDelete': return cmdBranchDelete(args, profile, envConfig); + case 'Rebase': return cmdRebase(args, profile, envConfig); + case 'CherryPick': return cmdCherryPick(args, profile, envConfig); + default: return error(`未知指令: "${command}"。`); + } +} + +// --- 主入口 --- + +function main() { + let inputData = ''; + process.stdin.setEncoding('utf8'); + process.stdin.on('data', chunk => { inputData += chunk; }); + process.stdin.on('end', async () => { + try { + const input = JSON.parse(inputData.trim()); + + const envConfig = loadEnvConfig(); + const repos = loadRepos(); + + const serialCommands = parseSerialCommands(input); + + if (serialCommands.length === 0) { + return error('未提供任何指令。'); + } + + if (serialCommands.length === 1) { + // 单指令直接执行 + const { command, args } = serialCommands[0]; + await dispatchCommand(command, args, repos, envConfig); + } else { + // 串行执行多条指令 + const results = []; + for (const { command, args } of serialCommands) { + try { + // 捕获 stdout + const origLog = console.log; + let captured = ''; + console.log = (msg) => { captured = msg; }; + await dispatchCommand(command, args, repos, envConfig); + console.log = origLog; + + const parsed = JSON.parse(captured); + results.push(parsed); + + if (parsed.status === 'error') { + // 串行中遇到错误,中断后续指令 + results.push({ status: 'aborted', reason: `"${command}" 执行失败,后续指令已中止。` }); + break; + } + } catch (e) { + results.push({ status: 'error', command, error: e.message }); + break; + } + } + console.log(JSON.stringify({ status: 'success', result: { serialMode: true, totalCommands: serialCommands.length, executed: results.length, results } })); + } + } catch (e) { + error(`输入解析失败: ${e.message}`); + } + }); +} + +main(); \ No newline at end of file diff --git a/Plugin/GitOperator/README.md b/Plugin/GitOperator/README.md new file mode 100644 index 000000000..defb7f8a5 --- /dev/null +++ b/Plugin/GitOperator/README.md @@ -0,0 +1,328 @@ +# GitOperator — VCP Git 仓库管理插件 + +> **版本**: v1.0.1 | **作者**: Nova & hjhjd | **日期**: 2026-03-02 + +## 📌 这是什么? + +GitOperator 是一个为 VCP 系统设计的 **配置档驱动 (Profile-Driven)** Git 仓库管理插件。它解决了一个常见的痛点: + +**当你的项目是从别人的仓库 fork 来的时候**,`git pull` 拉的是自己仓库的代码,而不是上游的最新更新。每次想同步上游都得手动配置 remote,非常麻烦。 + +GitOperator 让你把所有仓库地址、凭证、上下游关系写在一个配置文件里,然后通过 AI 对话就能完成拉取、推送、同步等所有 Git 操作。 + +--- + +## 🚀 核心特性 + +- **多仓库管理**:通过 `repos.json` 配置多个仓库档案(Profile),随时切换操作目标 +- **一键上游同步**:`SyncUpstream` 命令自动完成 fetch → stash → merge → push 全流程 +- **凭证安全注入**:GitHub Token 仅在推送时临时注入 URL,不会写入 `.git/config` +- **输出自动脱敏**:所有返回给 AI 的内容会自动将 Token 替换为 `ghp_xxxx****xxxx` 格式 +- **危险操作保护**:5 条危险指令需要通过 `captchaDecoder` 模块实时校验真实验证码 +- **冲突智能处理**:Merge / Rebase / SyncUpstream 遇冲突自动 abort 恢复干净状态 +- **串行调用**:支持一次请求执行多个 Git 操作(如 Add → Commit → Push),遇错自动中断 +- **路径白名单**:所有文件操作限制在 `config.env` 的 `PLUGIN_WORK_PATHS` 范围内 +- **配置兼容**:同时支持嵌套结构(`push.url`)和扁平结构(`pushUrl`),自动归一化 + +--- + +## 📂 文件结构 + +``` +Plugin/GitOperator/ +├── GitOperator.js # 主脚本(异步入口,~650行) +├── plugin-manifest.json # 插件清单(25条指令定义) +├── config.env # 运行配置(路径白名单、日志开关) +├── config.env.example # 配置模板 +├── repos.json # 仓库配置档(⚠️ 含凭证,勿提交) +├── repos.json.example # 仓库配置示例 +├── debug.log # 运行时调试日志(自动生成) +└── README.md # 本文档 +``` + +--- + +## ⚙️ 快速开始 + +### 第一步:配置 config.env + +打开 `config.env`,根据需要修改: + +```env +# 插件工作路径白名单(逗号分隔,支持多个路径) +PLUGIN_WORK_PATHS=../../ + +# 日志开关(true/false) +ENABLE_LOGGING=true +``` + +> `PLUGIN_WORK_PATHS` 默认是 `../../`(VCP 项目根目录)。路径会被 `path.resolve` 解析为绝对路径后做前缀匹配。如果要管理其他位置的仓库,把路径加进来即可。 + +### 第二步:配置 repos.json + +这是最重要的配置文件。GitOperator 同时支持**嵌套结构**和**扁平结构**两种写法,推荐使用嵌套结构(更清晰): + +#### 推荐写法(嵌套结构) + +```json +{ + "defaultProfile": "VCPToolBox", + "profiles": { + "VCPToolBox": { + "localPath": "../../", + "push": { + "remote": "origin", + "url": "https://github.com/你的用户名/VCPToolBox.git", + "branch": "main" + }, + "pull": { + "remote": "upstream", + "url": "https://github.com/lioensky/VCPToolBox.git", + "branch": "main" + }, + "credentials": { + "email": "你的邮箱", + "username": "你的GitHub用户名", + "token": "ghp_你的PersonalAccessToken" + }, + "mergeStrategy": "merge" + } + } +} +``` + +#### 兼容写法(扁平结构) + +```json +{ + "defaultProfile": "my-repo", + "profiles": { + "my-repo": { + "localPath": "../../", + "pushUrl": "https://github.com/你的用户名/Repo.git", + "pushRemote": "origin", + "pushBranch": "main", + "pullUrl": "https://github.com/上游用户名/Repo.git", + "pullRemote": "upstream", + "pullBranch": "main", + "email": "你的邮箱", + "username": "你的GitHub用户名", + "token": "ghp_你的Token", + "mergeStrategy": "merge" + } + } +} +``` + +> 插件内部会自动归一化:嵌套字段优先,缺失时回落到扁平字段。两种格式混用也没问题。 + +**字段说明**: + +| 字段 | 说明 | +|------|------| +| `defaultProfile` | 不指定 profile 参数时默认使用的档案名称 | +| `localPath` | 本地仓库路径(支持相对路径,会被 `path.resolve` 转为绝对路径) | +| `push.remote` / `pushRemote` | 推送目标 remote 名称(默认 `origin`) | +| `push.url` / `pushUrl` | 推送目标 URL(你自己的仓库) | +| `push.branch` / `pushBranch` | 推送目标分支(默认 `main`) | +| `pull.remote` / `pullRemote` | 拉取来源 remote 名称(默认 `upstream`) | +| `pull.url` / `pullUrl` | 拉取来源 URL(上游仓库) | +| `pull.branch` / `pullBranch` | 拉取来源分支(默认 `main`) | +| `credentials.token` / `token` | GitHub Personal Access Token | +| `credentials.email` / `email` | Git 提交邮箱 | +| `credentials.username` / `username` | Git 提交用户名 | +| `mergeStrategy` | 同步合并策略:`merge`(默认)或 `rebase` | + +> ⚠️ **安全提醒**:`repos.json` 包含你的 GitHub Token,请确保它已加入 `.gitignore`,**绝对不要**提交到仓库! + +### 第三步:重启 VCP 服务 + +配置完成后重启 VCP 服务器,GitOperator 插件会自动加载。首次调用时会自动校准 remote(`ensureRemotes`)。 + +--- + +## 📋 全部指令一览 + +GitOperator 共提供 **25 条指令**,分为 5 个类别: + +### 只读查询(8条) + +| 指令 | 功能 | 关键参数 | +|------|------|----------| +| `Status` | 查看仓库状态(工作区 + 暂存区) | `profile`(可选) | +| `Log` | 查看提交历史(返回结构化 JSON) | `maxCount`(默认20)、`branch`(可指定远程分支) | +| `Diff` | 查看变更差异,输出自动截断 | `target`(如 `upstream/main`、`HEAD~3`)、`maxLines`(默认200) | +| `BranchList` | 列出所有本地和远程分支 | — | +| `RemoteInfo` | 查看远程仓库信息(自动脱敏) | — | +| `StashList` | 查看 stash 暂存列表 | — | +| `TagList` | 查看标签列表 | — | +| `ProfileList` | 列出所有仓库配置档(不含敏感凭证) | 无需 profile | + +### 常规写操作(7条) + +| 指令 | 功能 | 关键参数 | +|------|------|----------| +| `Add` | 暂存文件到索引 | `files`(必需,`"."` = 全部,多个文件空格分隔) | +| `Commit` | 提交暂存区变更 | `message`(必需) | +| `Pull` | 拉取代码,默认走 `pull` 配置(上游仓库) | `source`(可选,`"pull"` = 上游,`"push"` = 自己仓库) | +| `Push` | 推送代码,走 `push` 配置,自动注入凭证 | — | +| `Fetch` | 获取远程引用(不合并) | `source`(可选,`"pull"` = 上游,`"push"` = 自己仓库) | +| `Clone` | 克隆远程仓库到本地 | `url`(必需)、`localPath`(必需)、`profile`(可选,自动创建) | +| `SyncUpstream` ⭐ | 一键同步上游仓库 | — | + +### 分支管理(3条) + +| 指令 | 功能 | 关键参数 | +|------|------|----------| +| `BranchCreate` | 创建新本地分支 | `branchName`(必需)、`startPoint`(可选,默认 HEAD) | +| `Checkout` | 切换到指定分支 | `branch`(必需) | +| `Merge` | 将指定分支合并到当前分支 | `branch`(必需) | + +### 🔒 危险操作(5条,需验证码) + +| 指令 | 功能 | 关键参数 | +|------|------|----------| +| `ForcePush` | 强制推送(`git push --force`) | `requireAdmin`(必需,6位验证码) | +| `ResetHard` | 硬重置到指定提交 | `target`(可选,默认 HEAD)、`requireAdmin`(必需) | +| `BranchDelete` | 删除本地分支(`git branch -D`) | `branchName`(必需)、`requireAdmin`(必需) | +| `Rebase` | 变基当前分支(冲突自动 abort) | `onto`(必需)、`requireAdmin`(必需) | +| `CherryPick` | 摘取指定提交(冲突自动 abort) | `commitHash`(必需)、`requireAdmin`(必需) | + +> **验证码机制**:危险操作调用时需提供 `requireAdmin` 参数。插件会通过 VCP 系统的 `captchaDecoder` 模块实时读取 `UserAuth/code.bin`,解码出真实验证码进行比对。**错误的验证码**或**缺失参数**都会被直接拒绝,不会执行任何 Git 命令。 + +### 配置管理(3条) + +| 指令 | 功能 | 关键参数 | +|------|------|----------| +| `ProfileAdd` | 添加新仓库配置档(自动配置 remote) | `profileName`(必需)、`localPath`(必需)及其他可选字段 | +| `ProfileEdit` | 编辑已有配置档(只传需修改的字段) | `profileName`(必需)及需修改的字段 | +| `ProfileRemove` | 删除仓库配置档 | `profileName`(必需) | + +--- + +## ⭐ SyncUpstream 详解 + +这是 GitOperator 最核心的功能。一条命令完成从上游仓库的完整同步: + +``` +执行流程: +1. git fetch — 获取上游最新引用 +2. git status --porcelain — 检查未提交更改 +3. git stash push -m "VCP-auto-stash" — 有更改则自动暂存保护 +4. git merge/rebase / — 执行合并(策略取决于配置) + ├─ 成功 → 继续 + └─ 冲突 → 自动 abort + stash pop + 返回冲突文件列表 +5. git stash pop — 恢复暂存的本地更改 +6. git push — 推送到自己的远程仓库 +``` + +**使用场景**:你 fork 了 lioensky/VCPToolBox,想把上游的最新更新同步到你自己的仓库。只需告诉 AI "同步一下上游",GitOperator 会自动完成全部步骤。 + +--- + +## 🔗 串行调用 + +支持在一次请求中执行多个连续操作,非常适合 "Add → Commit → Push" 这样的常见工作流。 + +AI 会自动构造带数字后缀的参数,例如: + +``` +command1: "Add", files1: "." +command2: "Commit", message2: "feat: 新功能" +command3: "Push" +``` + +**串行行为**: +- 所有指令按顺序执行,共享 `profile` 参数 +- 任一指令执行失败 → 后续指令**自动中止**,返回已执行的结果 + 中止原因 +- 单条指令(只有 `command` 没有 `command1`)自动走单指令路径,无额外开销 + +--- + +## 🛡️ 安全架构 + +GitOperator 采用 **多层安全防护**: + +| 层级 | 机制 | 说明 | +|------|------|------| +| 1 | **路径白名单** | 所有操作的 `localPath` 必须在 `PLUGIN_WORK_PATHS` 的前缀范围内 | +| 2 | **凭证脱敏** | 输出中的 Token 自动替换为 `ghp_xxxx****xxxx` 格式 | +| 3 | **凭证仅内存注入** | Token 只在 push URL 中临时拼接,不写入 `.git/config` 或日志 | +| 4 | **Auth 验证码守卫** | 5 条危险指令通过 `captchaDecoder` 实时校验 `code.bin` 中的真实验证码 | +| 5 | **参数存在性检查** | 危险操作缺失 `requireAdmin` 参数时,在校验前就直接拒绝 | +| 6 | **冲突自动中止** | Merge / Rebase / SyncUpstream 遇冲突自动 `--abort`,不留半成品 | +| 7 | **safe.directory** | `ProfileAdd` 时自动配置 `git config --global --add safe.directory` | + +--- + +## 🔧 自动校准机制(ensureRemotes) + +通过 `ProfileAdd` 创建 Profile 时,插件会自动执行以下校准: + +1. 检查 `pushRemote`(默认 `origin`)→ 存在则 `set-url`,否则不新增(Git 默认已有 origin) +2. 检查 `pullRemote`(默认 `upstream`)→ 不存在则 `remote add`,URL 不匹配则 `set-url` +3. 自动设置 `user.email` 和 `user.name` +4. 自动添加 `safe.directory` 白名单 + +**这意味着你只需要在 repos.json 里填好地址,第一次创建 Profile 时它会自动把所有 remote 配好。** + +--- + +## 💡 常见使用场景 + +### 场景 1:日常开发提交 +> "Nova,帮我把改动都提交了,备注'修复登录bug',然后推上去" + +AI 执行:`Add(".")` → `Commit("修复登录bug")` → `Push` + +### 场景 2:同步上游更新 +> "Nova,从上游仓库同步一下最新代码" + +AI 执行:`SyncUpstream`(自动 fetch → merge → push) + +### 场景 3:查看远程更新了什么 +> "Nova,看看远程仓库更新了啥" + +AI 执行:`Fetch` → `Log(branch: "upstream/main")` → `Diff(target: "upstream/main")` + +### 场景 4:创建功能分支 +> "Nova,帮我创建一个 feature/dark-mode 分支并切过去" + +AI 执行:`BranchCreate("feature/dark-mode")` → `Checkout("feature/dark-mode")` + +### 场景 5:危险操作(需验证码) +> "Nova,强制推送一下,验证码 123456" + +AI 执行:`ForcePush(requireAdmin: "123456")` → 插件读取 code.bin 校验 → 匹配则执行,不匹配则拒绝 + +--- + +## ❓ FAQ + +**Q:我没有上游仓库怎么办?** +A:`pull` 配置中的 `url` 留空或不配置 `pull` 对象即可。Pull 指令会自动使用 push 配置的 remote。 + +**Q:Token 会不会泄露?** +A:不会。Token 只在推送时临时注入到内存中的 URL 里,不会写入 `.git/config`。所有返回给 AI 的输出都会经过 `sanitizeOutput()` 自动脱敏。 + +**Q:遇到合并冲突怎么办?** +A:GitOperator 会自动中止合并(`git merge --abort` 或 `git rebase --abort`),恢复到干净状态,并返回冲突文件列表。你可以手动解决冲突后再次尝试。 + +**Q:可以管理多个仓库吗?** +A:可以!在 `repos.json` 的 `profiles` 里添加多个配置即可。调用时指定 `profile` 参数切换目标仓库。不指定则使用 `defaultProfile`。 + +**Q:嵌套结构和扁平结构可以混用吗?** +A:可以。插件的 `resolveProfile()` 会自动归一化:优先读取嵌套字段(`push.url`),缺失时回落到扁平字段(`pushUrl`)。 + +**Q:验证码从哪里来?** +A:验证码由 VCP 系统的 `UserAuth` 模块管理,存储在 `Plugin/UserAuth/code.bin` 中。GitOperator 通过 `captchaDecoder` 模块实时解码。验证码是动态的,每次都需要获取最新的。 + +--- + +## 📜 许可 + +本插件作为 VCP 系统的一部分,遵循 VCP 项目的开源协议。 + +--- + +*Built with ❤️ by Nova & hjhjd — 2026.03.02* \ No newline at end of file diff --git a/Plugin/GitOperator/config.env.example b/Plugin/GitOperator/config.env.example new file mode 100644 index 000000000..e1a43c684 --- /dev/null +++ b/Plugin/GitOperator/config.env.example @@ -0,0 +1,46 @@ +# ============================================================ +# GitOperator 插件配置文件示例 +# 复制此文件为 config.env 并填入实际值即可使用 +# ============================================================ + +# ---------------------------------------------------------- +# 安全白名单路径 (必需) +# ---------------------------------------------------------- +# 插件允许操作的本地仓库路径白名单,多个路径用英文逗号分隔。 +# repos.json 中的 localPath 必须落在此白名单范围内,否则操作将被拒绝。 +# +# 支持相对路径(相对于插件目录)和绝对路径。 +# 默认值 "../../" 指向 VCP 项目根目录,适用于单机部署场景。 +# +# 分布式场景示例: +# PLUGIN_WORK_PATHS=../../,/mnt/remote-node-1/VCPServer,/mnt/remote-node-2/VCPServer +# +PLUGIN_WORK_PATHS=../../ + +# ---------------------------------------------------------- +# 日志级别 (可选, 预留) +# ---------------------------------------------------------- +# 控制插件的日志输出详细程度。 +# 可选值: silent | error | warn | info | debug +# 默认值: info +# +# LOG_LEVEL=info + +# ---------------------------------------------------------- +# Git 可执行文件路径 (可选, 预留) +# ---------------------------------------------------------- +# 如果 git 不在系统 PATH 中,可以在此指定完整路径。 +# 例如 Windows: GIT_EXECUTABLE=C:\Program Files\Git\bin\git.exe +# 例如 Linux: GIT_EXECUTABLE=/usr/bin/git +# 默认值: git (从系统 PATH 自动查找) +# +# GIT_EXECUTABLE=git + +# ---------------------------------------------------------- +# 命令执行超时 (可选, 预留) +# ---------------------------------------------------------- +# 单条 git 命令的最大执行时间(毫秒)。 +# 对于大型仓库的 clone/fetch 操作,可能需要适当增大。 +# 默认值: 30000 (30秒) +# +# GIT_TIMEOUT=30000 \ No newline at end of file diff --git a/Plugin/GitOperator/plugin-manifest.json b/Plugin/GitOperator/plugin-manifest.json new file mode 100644 index 000000000..d550b8f87 --- /dev/null +++ b/Plugin/GitOperator/plugin-manifest.json @@ -0,0 +1,133 @@ +{ + "manifestVersion": "1.0.0", + "name": "GitOperator", + "version": "1.1.0", + "displayName": "Git 仓库管理器", + "description": "基于配置档驱动的智能 Git 管理器。支持多仓库 Profile 管理、上游同步、凭证注入、串行调用和 Token 脱敏输出。", + "author": "Nova & hjhjd", + "pluginType": "synchronous", + "entryPoint": { + "type": "nodejs", + "command": "node GitOperator.js" + }, + "communication": { + "protocol": "stdio", + "timeout": 30000 + }, + "requiresAdmin": false, + "configSchema": { + "PLUGIN_WORK_PATHS": { + "type": "string", + "description": "允许进行 git 操作的工作路径白名单,多个路径用英文逗号分隔。", + "default": "../../" + } + }, + "capabilities": { + "invocationCommands": [ + { + "command": "Status", + "description": "查看 Git 仓库当前状态(工作区变更、暂存区状态等)。\n参数: profile (可选, 仓库配置档名称,不填使用 defaultProfile)" + }, + { + "command": "Log", + "description": "查看 Git 提交历史,返回结构化 JSON。\n参数: profile (可选), maxCount (可选, 默认20, 最大提交数), branch (可选, 指定分支)" + }, + { + "command": "Diff", + "description": "查看 Git 变更差异,支持与远程分支或历史提交对比,输出自动截断。\n参数: profile (可选), target (可选, 对比目标如 upstream/main 或 HEAD~3,默认为工作区diff), maxLines (可选, 默认200)" + }, + { + "command": "BranchList", + "description": "列出仓库所有本地和远程分支。\n参数: profile (可选)" + }, + { + "command": "RemoteInfo", + "description": "查看仓库配置的远程仓库信息 (git remote -v)。\n参数: profile (可选)" + }, + { + "command": "StashList", + "description": "列出仓库所有 stash 暂存记录。\n参数: profile (可选)" + }, + { + "command": "TagList", + "description": "列出仓库所有标签。\n参数: profile (可选)" + }, + { + "command": "ProfileList", + "description": "列出 repos.json 中所有已配置的仓库 Profile 及基本信息(不含敏感凭证)。\n参数: 无" + }, + { + "command": "Add", + "description": "暂存文件到 Git 索引 (git add)。\n参数: profile (可选), files (必需, 文件路径,用 . 表示全部,多个文件空格分隔)" + }, + { + "command": "Commit", + "description": "提交暂存区变更 (git commit)。\n参数: profile (可选), message (必需, 提交信息)" + }, + { + "command": "Pull", + "description": "从远程仓库拉取更新,默认走 repos.json 的 pull 配置(上游仓库)。\n参数: profile (可选), source (可选, 'pull'=上游仓库 或 'push'=自己仓库,默认pull)" + }, + { + "command": "Push", + "description": "推送本地提交到远程仓库,走 repos.json 的 push 配置,自动注入凭证。\n参数: profile (可选)" + }, + { + "command": "Fetch", + "description": "获取远程仓库最新引用(不合并)。\n参数: profile (可选), source (可选, 'pull'=上游 或 'push'=自己仓库,默认pull)" + }, + { + "command": "BranchCreate", + "description": "创建新的本地分支。\n参数: profile (可选), branchName (必需), startPoint (可选, 起点commit或分支名,默认HEAD)" + }, + { + "command": "Checkout", + "description": "切换到指定分支。\n参数: profile (可选), branch (必需)" + }, + { + "command": "Merge", + "description": "将指定分支合并到当前分支。\n参数: profile (可选), branch (必需, 源分支名)" + }, + { + "command": "Clone", + "description": "克隆远程仓库到本地。\n参数: url (必需, 远程仓库URL), localPath (必需, 本地路径), profile (可选, 自动创建的Profile名)" + }, + { + "command": "SyncUpstream", + "description": "一键上游同步: fetch upstream -> stash(如有变更) -> merge/rebase -> stash pop -> push origin。冲突时自动中止并返回冲突文件列表。\n参数: profile (可选)" + }, + { + "command": "ForcePush", + "description": "危险操作: 强制推送 (git push --force)。需要管理员验证码。\n参数: profile (可选), requireAdmin (必需, 6位验证码)" + }, + { + "command": "ResetHard", + "description": "危险操作: 硬重置到指定提交 (git reset --hard),未提交更改将永久丢弃。需要管理员验证码。\n参数: profile (可选), target (可选, 如HEAD~1,默认HEAD), requireAdmin (必需, 6位验证码)" + }, + { + "command": "BranchDelete", + "description": "危险操作: 删除本地分支。需要管理员验证码。\n参数: profile (可选), branchName (必需), requireAdmin (必需, 6位验证码)" + }, + { + "command": "Rebase", + "description": "危险操作: 变基当前分支到指定目标。需要管理员验证码。\n参数: profile (可选), onto (必需, 变基目标), requireAdmin (必需, 6位验证码)" + }, + { + "command": "CherryPick", + "description": "危险操作: 摘取指定提交应用到当前分支。需要管理员验证码。\n参数: profile (可选), commitHash (必需), requireAdmin (必需, 6位验证码)" + }, + { + "command": "ProfileAdd", + "description": "向 repos.json 添加新的仓库配置档。\n参数: profileName (必需), localPath (必需), pushUrl (可选), pushRemote (可选,默认origin), pushBranch (可选,默认main), pullUrl (可选), pullRemote (可选,默认upstream), pullBranch (可选,默认main), email (可选), username (可选), token (可选), mergeStrategy (可选,默认merge)" + }, + { + "command": "ProfileEdit", + "description": "编辑 repos.json 中已有的配置档字段,只传需要修改的字段即可。\n参数: profileName (必需), 其他字段同 ProfileAdd" + }, + { + "command": "ProfileRemove", + "description": "从 repos.json 中删除一个仓库配置档。\n参数: profileName (必需)" + } + ] + } +} \ No newline at end of file diff --git a/Plugin/GitOperator/repos.json.example b/Plugin/GitOperator/repos.json.example new file mode 100644 index 000000000..97d3af756 --- /dev/null +++ b/Plugin/GitOperator/repos.json.example @@ -0,0 +1,72 @@ +{ + "_comment": "GitOperator 仓库配置档示例 — 复制为 repos.json 并填入实际值", + + "defaultProfile": "vcp-server", + + "profiles": { + + "vcp-server": { + "_comment": "VCPServer 主仓库 — fork 自 RyanLiuCN/VCPServer", + "localPath": "../../", + "push": { + "remote": "origin", + "url": "https://github.com/你的用户名/VCPServer.git", + "branch": "main" + }, + "pull": { + "remote": "upstream", + "url": "https://github.com/RyanLiuCN/VCPServer.git", + "branch": "main" + }, + "credentials": { + "email": "your-email@example.com", + "username": "你的用户名", + "token": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, + "mergeStrategy": "merge" + }, + + "my-plugin": { + "_comment": "示例: 独立插件仓库 (无上游,只推自己的 origin)", + "localPath": "../../Plugin/MyAwesomePlugin", + "push": { + "remote": "origin", + "url": "https://github.com/你的用户名/MyAwesomePlugin.git", + "branch": "main" + }, + "pull": { + "_comment": "没有上游仓库时留空即可,Pull 指令会自动使用 push 配置", + "remote": "origin", + "url": "", + "branch": "main" + }, + "credentials": { + "email": "your-email@example.com", + "username": "你的用户名", + "token": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, + "mergeStrategy": "merge" + }, + + "vcpchat-client": { + "_comment": "示例: VCPChat 客户端仓库 (使用绝对路径 + rebase 策略)", + "localPath": "D:/Projects/VCPChat", + "push": { + "remote": "origin", + "url": "https://github.com/你的用户名/VCPChat.git", + "branch": "dev" + }, + "pull": { + "remote": "upstream", + "url": "https://github.com/RyanLiuCN/VCPChat.git", + "branch": "main" + }, + "credentials": { + "email": "your-email@example.com", + "username": "你的用户名", + "token": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, + "mergeStrategy": "rebase" + } + } +} \ No newline at end of file