""" Git 工具插件 - 统一的版本控制能力 合并原 git_tools + git_workflow,消除重复 """ import os import re import subprocess from typing import Tuple, Optional class Plugin: """Git 版本控制 - 基础操作 + 工作流自动化""" def __init__(self): self.usage = """Git 版本控制工具 基础操作: status - 查看仓库状态 diff [文件路径] - 查看未暂存变更 diff --cached - 查看已暂存变更 log [数量] - 提交历史 (默认10条) log --oneline [数量] - 简洁提交历史 add <文件路径> - 暂存文件 add . - 暂存所有变更 commit <提交信息> - 提交变更 branch - 查看分支列表 branch <名称> - 创建新分支 checkout <分支名> - 切换分支 checkout -b <分支名> - 创建并切换分支 stash - 暂存当前变更 stash pop - 恢复暂存的变更 show - 查看指定提交 blame <文件路径> - 逐行修改记录 remote - 查看远程仓库 init - 初始化仓库 clone [目录] - 克隆仓库 工作流: smart-commit [消息] - 智能提交 (自动 add + 规范化消息) changelog [版本] - 生成 CHANGELOG summary - 变更摘要 (未提交改动) contributors - 贡献者统计 stale [天数] - 查找长期未更新文件 """ self.cli = None def set_cli(self, cli): self.cli = cli def get_tool_info(self): return { "name": "git_tools", "description": "Git 版本控制 - 状态查看、提交、分支管理、diff、历史、blame、工作流自动化", "keywords": ["git", "版本控制", "提交", "分支", "commit", "branch", "status", "diff", "changelog", "log", "blame", "stash", "checkout", "merge"], "usage": self.usage } def get_mcp_definition(self): return { "name": "git_tools", "description": "Git 版本控制工具", "inputSchema": { "type": "object", "properties": { "operation": {"type": "string", "description": "Git 操作"}, "args": {"type": "string", "description": "操作参数"} }, "required": ["operation"] } } def convert_mcp_args(self, arguments): op = arguments.get("operation", "") args = arguments.get("args", "") return f"{op} {args}".strip() # ── 执行 Git 命令 ── def _run_git(self, args: list, cwd: Optional[str] = None) -> Tuple[bool, str]: try: result = subprocess.run( ["git"] + args, capture_output=True, text=True, cwd=cwd, timeout=30 ) output = (result.stdout + result.stderr).strip() return result.returncode == 0, output except FileNotFoundError: return False, "git 未安装,请先安装 Git" except subprocess.TimeoutExpired: return False, "Git 命令执行超时" except Exception as e: return False, str(e) def _run_shell(self, cmd: str, cwd: Optional[str] = None) -> Tuple[bool, str]: try: result = subprocess.run( cmd, shell=True, capture_output=True, text=True, cwd=cwd or os.getcwd(), timeout=30 ) return result.returncode == 0, (result.stdout + result.stderr).strip() except Exception as e: return False, str(e) def _get_cwd(self): if self.cli and hasattr(self.cli, 'current_dir'): return self.cli.current_dir return None # ── 路由 ── def handle(self, args: str) -> str: try: parts = args.strip().split(maxsplit=1) if not parts: return self._status() op = parts[0].lower() rest = parts[1] if len(parts) > 1 else "" cwd = self._get_cwd() handlers = { # 基础 "status": lambda r: self._status(cwd), "diff": lambda r: self._diff(r, cwd), "log": lambda r: self._log(r, cwd), "add": lambda r: self._add(r, cwd), "commit": lambda r: self._commit(r, cwd), "branch": lambda r: self._branch(r, cwd), "checkout": lambda r: self._checkout(r, cwd), "stash": lambda r: self._stash(r, cwd), "show": lambda r: self._show(r, cwd), "blame": lambda r: self._blame(r, cwd), "remote": lambda r: self._remote(cwd), "init": lambda r: self._init(r, cwd), "clone": lambda r: self._clone(r, cwd), # 工作流 "smart-commit": lambda r: self._smart_commit(r, cwd), "changelog": lambda r: self._changelog(r, cwd), "summary": lambda r: self._summary(cwd), "contributors": lambda r: self._contributors(cwd), "stale": lambda r: self._stale(r, cwd), } handler = handlers.get(op) if handler: return handler(rest) # 回退:直接执行 git 命令 ok, output = self._run_git(args.split(), cwd) return output if ok else f"Git 错误: {output}" except Exception as e: return f"Git 工具错误: {str(e)}" # ── 基础操作 ── def _status(self, cwd=None) -> str: ok, output = self._run_git(["status", "--porcelain"], cwd) if not ok: return f"Git 错误: {output}" if not output: # 获取分支 _, branch = self._run_git(["branch", "--show-current"], cwd) return f" 工作区干净 (分支: {branch})" lines = output.split('\n') staged, unstaged, untracked = [], [], [] for line in lines: if len(line) < 4: continue s, filename = line[:2], line[3:] if s[0] in 'MADRC': staged.append(f" {s[0]} {filename}") if s[1] in 'MD': unstaged.append(f" ! {s[1]} {filename}") if s == '??': untracked.append(f" ? {filename}") _, branch = self._run_git(["branch", "--show-current"], cwd) result = [f" Git 状态 (分支: {branch})"] if staged: result.append(f"\n已暂存 ({len(staged)}):") result.extend(staged) if unstaged: result.append(f"\n未暂存 ({len(unstaged)}):") result.extend(unstaged) if untracked: result.append(f"\n未跟踪 ({len(untracked)}):") result.extend(untracked) return '\n'.join(result) def _diff(self, args: str, cwd=None) -> str: args = args.strip() if args == "--cached": git_args = ["diff", "--cached"] elif args: git_args = ["diff", args] else: git_args = ["diff"] ok, output = self._run_git(git_args, cwd) if not ok: return f"Git 错误: {output}" if not output: return "没有变更" if len(output) > 5000: output = output[:5000] + f"\n... (截断,共 {len(output)} 字符)" return output def _log(self, args: str, cwd=None) -> str: parts = args.split() oneline = "--oneline" in parts count = 10 for p in parts: if p.isdigit(): count = int(p) if oneline: ok, output = self._run_git( ["log", "--oneline", f"-{count}", "--graph", "--decorate"], cwd) else: ok, output = self._run_git( ["log", f"-{count}", "--format=%h %an %ad %s", "--date=short"], cwd) if not ok: return f"Git 错误: {output}" return output or "暂无提交历史" def _add(self, args: str, cwd=None) -> str: target = args.strip() if not target: return "错误:请提供文件路径(使用 . 暂存所有)" ok, output = self._run_git(["add", target], cwd) return f" 已暂存: {target}" if ok else f"Git add 失败: {output}" def _commit(self, args: str, cwd=None) -> str: message = args.strip() if not message: return "错误:请提供提交信息" ok, output = self._run_git(["commit", "-m", message], cwd) return f" 提交成功:\n{output}" if ok else f"Git commit 失败: {output}" def _branch(self, args: str, cwd=None) -> str: if not args.strip(): ok, output = self._run_git(["branch", "-a"], cwd) return f" 分支列表:\n{output}" if ok else f"Git 错误: {output}" else: ok, output = self._run_git(["branch", args.strip()], cwd) return f" 已创建分支: {args.strip()}" if ok else f"创建分支失败: {output}" def _checkout(self, args: str, cwd=None) -> str: args = args.strip() if args.startswith("-b "): branch = args[3:].strip() ok, output = self._run_git(["checkout", "-b", branch], cwd) else: branch = args ok, output = self._run_git(["checkout", branch], cwd) return f" 已切换到: {branch}" if ok else f"切换失败: {output}" def _stash(self, args: str, cwd=None) -> str: if args.strip() == "pop": ok, output = self._run_git(["stash", "pop"], cwd) else: ok, output = self._run_git(["stash"], cwd) return f" {output}" if ok else f"Git stash 失败: {output}" def _show(self, args: str, cwd=None) -> str: commit = args.strip() or "HEAD" ok, output = self._run_git(["show", "--stat", commit], cwd) if not ok: return f"Git show 失败: {output}" if len(output) > 3000: output = output[:3000] + "\n... (截断)" return output def _blame(self, args: str, cwd=None) -> str: path = args.strip() if not path: return "错误:请提供文件路径" ok, output = self._run_git(["blame", "--line-porcelain", path], cwd) if not ok: return f"Git blame 失败: {output}" lines = output.split('\n') result = [] author = "" for line in lines: if line.startswith('author '): author = line[7:] elif line.startswith('\t'): result.append(f" {author}: {line[1:]}") if len(result) > 100: result = result[:100] result.append(f" ... (共 {len(result)} 行)") return f" {path} 修改记录:\n" + '\n'.join(result) def _remote(self, cwd=None) -> str: ok, output = self._run_git(["remote", "-v"], cwd) if not ok: return f"Git 错误: {output}" return output or "没有配置远程仓库" def _init(self, args: str, cwd=None) -> str: path = args.strip() or "." ok, output = self._run_git(["init", path], cwd) return f" {output}" if ok else f"Git init 失败: {output}" def _clone(self, args: str, cwd=None) -> str: parts = args.split() if not parts: return "错误:请提供仓库 URL" cmd = ["clone", parts[0]] if len(parts) > 1: cmd.append(parts[1]) ok, output = self._run_git(cmd, cwd) return f" 克隆成功:\n{output}" if ok else f"Git clone 失败: {output}" # ── 工作流 ── def _smart_commit(self, message: str, cwd=None) -> str: """智能提交:自动 add + 规范化消息""" ok, status = self._run_git(["status", "--porcelain"], cwd) if not status: return " 工作区干净,没有需要提交的变更" self._run_git(["add", "-A"], cwd) if not message: message = self._auto_commit_message(status) # conventional commits 检查 if not re.match(r'^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?: .+', message): if 'test' in message.lower(): message = f"test: {message}" elif 'doc' in message.lower() or 'readme' in message.lower(): message = f"docs: {message}" else: message = f"feat: {message}" ok, output = self._run_git(["commit", "-m", message], cwd) if ok: _, hash_out = self._run_git(["log", "--oneline", "-1"], cwd) return f" 已提交: {message}\n {hash_out}" return f" 提交失败: {output}" def _auto_commit_message(self, status: str) -> str: lines = status.strip().split('\n') added = len([l for l in lines if l.startswith('A ') or l.startswith('A') and l[1] == ' ']) modified = len([l for l in lines if l.startswith('M ') or (len(l) > 1 and l[1] == 'M')]) deleted = len([l for l in lines if l.startswith('D ') or (len(l) > 1 and l[1] == 'D')]) parts = [] if added: parts.append(f"添加 {added} 个文件") if modified: parts.append(f"修改 {modified} 个文件") if deleted: parts.append(f"删除 {deleted} 个文件") return ", ".join(parts) if parts else "更新代码" def _changelog(self, version: str, cwd=None) -> str: ok, output = self._run_shell( 'git log --oneline --format="%s" | head -50', cwd) if not ok: return f" {output}" commits = output.strip().split('\n') categories = { 'feat': [], 'fix': [], 'docs': [], 'refactor': [], 'test': [], 'chore': [], 'other': [] } for commit in commits: matched = False for prefix in categories: if commit.startswith(prefix + ':') or commit.startswith(prefix + '('): categories[prefix].append(commit) matched = True break if not matched: categories['other'].append(commit) labels = { 'feat': ' 新增', 'fix': ' 修复', 'docs': ' 文档', 'refactor': ' 重构', 'test': ' 测试', 'chore': ' 杂项', 'other': ' 其他' } ver = version or "未发布" result = [f"## {ver}\n"] for prefix, label in labels.items(): items = categories[prefix] if items: result.append(f"\n### {label}") for item in items[:10]: result.append(f"- {item}") return '\n'.join(result) def _summary(self, cwd=None) -> str: ok, output = self._run_git(["diff", "--stat"], cwd) if not output: return " 没有未提交的变更" _, short = self._run_git(["diff", "--shortstat"], cwd) return f" 变更摘要:\n\n{short}\n\n{output[:2000]}" def _contributors(self, cwd=None) -> str: ok, output = self._run_git(["shortlog", "-sn", "--all", "--no-merges"], cwd) if not ok: return f" {output}" lines = output.strip().split('\n') result = [f" 贡献者 ({len(lines)} 人):\n"] for line in lines[:20]: result.append(f" {line.strip()}") return '\n'.join(result) def _stale(self, args: str, cwd=None) -> str: days = int(args) if args.strip().isdigit() else 90 ok, output = self._run_shell( f"git log --diff-filter=A --format='%aI %n' --since='{days} days ago' | head -20", cwd) return f" 最近 {days} 天新增的文件:\n{output[:2000]}" if ok else f" {output}"