- auto_engineer: lint/format/test/build/deps/info/init - git_workflow: 规范提交/changelog/变更摘要/贡献者统计 - project_analyzer: 复杂度/依赖/重复/安全/质量指标/改进建议 工具总数: 17 个 本次更新由 mimo 自动化执行,与开发者无关。
285 lines
9.5 KiB
Python
285 lines
9.5 KiB
Python
"""
|
|
Git 工作流自动化 - commit 规范、分支管理、PR 生成
|
|
"""
|
|
import os
|
|
import subprocess
|
|
import re
|
|
from typing import Tuple
|
|
|
|
|
|
class Plugin:
|
|
"""Git 工作流自动化 - 规范提交、分支管理、变更摘要"""
|
|
|
|
def __init__(self):
|
|
self.usage = """Git 工作流自动化
|
|
|
|
操作类型:
|
|
commit [消息] - 智能提交 (自动 add + 规范化消息)
|
|
log [数量] - 格式化提交历史
|
|
changelog [版本] - 生成 CHANGELOG
|
|
summary - 变更摘要 (本次未提交的改动)
|
|
blame [文件] - 逐行追溯
|
|
contributors - 贡献者统计
|
|
stale [天数] - 查找长期未更新的文件
|
|
hooks - 管理 git hooks
|
|
"""
|
|
self.cli = None
|
|
|
|
def set_cli(self, cli):
|
|
self.cli = cli
|
|
|
|
def get_tool_info(self):
|
|
return {
|
|
"name": "git_workflow",
|
|
"description": "Git 工作流自动化 - 规范提交、变更摘要、贡献者统计",
|
|
"keywords": ["git", "commit", "changelog", "提交", "变更", "工作流"],
|
|
"usage": self.usage
|
|
}
|
|
|
|
def get_mcp_definition(self):
|
|
return {
|
|
"name": "git_workflow",
|
|
"description": "Git 工作流自动化工具",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"operation": {
|
|
"type": "string",
|
|
"enum": ["commit", "log", "changelog", "summary", "blame", "contributors", "stale", "hooks"],
|
|
"description": "操作类型"
|
|
},
|
|
"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()
|
|
|
|
def _run(self, cmd: str, cwd: 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 handle(self, args: str) -> str:
|
|
parts = args.strip().split(maxsplit=1)
|
|
if not parts:
|
|
return self.usage
|
|
|
|
op = parts[0].lower()
|
|
rest = parts[1] if len(parts) > 1 else ""
|
|
|
|
handlers = {
|
|
"commit": self._commit,
|
|
"log": self._log,
|
|
"changelog": self._changelog,
|
|
"summary": self._summary,
|
|
"blame": self._blame,
|
|
"contributors": self._contributors,
|
|
"stale": self._stale,
|
|
"hooks": self._hooks,
|
|
}
|
|
|
|
handler = handlers.get(op)
|
|
if not handler:
|
|
return f"错误: 不支持的操作 '{op}'"
|
|
|
|
try:
|
|
return handler(rest)
|
|
except Exception as e:
|
|
return f"执行错误: {e}"
|
|
|
|
def _commit(self, message: str) -> str:
|
|
"""智能提交"""
|
|
# 检查是否有变更
|
|
ok, status = self._run("git status --porcelain")
|
|
if not status:
|
|
return "✅ 工作区干净,没有需要提交的变更"
|
|
|
|
# 自动 add
|
|
self._run("git add -A")
|
|
|
|
# 规范化提交消息
|
|
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 any('.py' in l for l in status.split('\n')):
|
|
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(f'git commit -m "{message}"')
|
|
if ok:
|
|
# 获取 commit hash
|
|
_, hash_out = self._run("git log --oneline -1")
|
|
return f"✅ 已提交: {message}\n {hash_out}"
|
|
return f"❌ 提交失败: {output}"
|
|
|
|
def _auto_commit_message(self, status: str) -> str:
|
|
"""根据变更自动生成提交消息"""
|
|
lines = status.strip().split('\n')
|
|
added = [l[3:] for l in lines if l.startswith('A')]
|
|
modified = [l[3:] for l in lines if l.startswith('M')]
|
|
deleted = [l[3:] for l in lines if l.startswith('D')]
|
|
|
|
parts = []
|
|
if added:
|
|
parts.append(f"添加 {len(added)} 个文件")
|
|
if modified:
|
|
parts.append(f"修改 {len(modified)} 个文件")
|
|
if deleted:
|
|
parts.append(f"删除 {len(deleted)} 个文件")
|
|
|
|
return ", ".join(parts) if parts else "更新代码"
|
|
|
|
def _log(self, args: str) -> str:
|
|
"""格式化提交历史"""
|
|
count = int(args) if args.strip().isdigit() else 10
|
|
ok, output = self._run(f'git log --oneline -{count} --format="%h %s (%ar, %an)"')
|
|
if not ok:
|
|
return f"❌ {output}"
|
|
|
|
lines = output.strip().split('\n')
|
|
result = [f"📜 最近 {len(lines)} 次提交:\n"]
|
|
for line in lines:
|
|
result.append(f" {line}")
|
|
|
|
return '\n'.join(result)
|
|
|
|
def _changelog(self, version: str) -> str:
|
|
"""生成 CHANGELOG"""
|
|
# 获取所有提交
|
|
ok, output = self._run('git log --oneline --format="%s" | head -50')
|
|
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)
|
|
|
|
# 生成
|
|
ver = version or "未发布"
|
|
result = [f"## {ver}\n"]
|
|
|
|
labels = {
|
|
'feat': '🚀 新增', 'fix': '🐛 修复', 'docs': '📝 文档',
|
|
'refactor': '♻️ 重构', 'test': '🧪 测试', 'chore': '🔧 杂项',
|
|
'other': '📦 其他'
|
|
}
|
|
|
|
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, args: str) -> str:
|
|
"""变更摘要"""
|
|
ok, output = self._run("git diff --stat")
|
|
if not output:
|
|
return "✅ 没有未提交的变更"
|
|
|
|
ok2, short = self._run("git diff --shortstat")
|
|
return f"📊 变更摘要:\n\n{short}\n\n{output[:2000]}"
|
|
|
|
def _blame(self, args: str) -> str:
|
|
"""逐行追溯"""
|
|
if not args.strip():
|
|
return "错误: 请提供文件路径"
|
|
|
|
ok, output = self._run(f"git blame --line-porcelain {args} | grep -E '^(author | )'")
|
|
if not ok:
|
|
return f"❌ {output}"
|
|
|
|
# 简化输出
|
|
lines = output.strip().split('\n')
|
|
result = [f"📝 {args} 修改记录:\n"]
|
|
author = ""
|
|
for line in lines:
|
|
if line.startswith('author '):
|
|
author = line[7:]
|
|
elif line.startswith('\t'):
|
|
result.append(f" {author:20} {line[1:]}")
|
|
|
|
return '\n'.join(result[:50])
|
|
|
|
def _contributors(self, args: str) -> str:
|
|
"""贡献者统计"""
|
|
ok, output = self._run("git shortlog -sn --all --no-merges")
|
|
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) -> str:
|
|
"""查找长期未更新的文件"""
|
|
days = int(args) if args.strip().isdigit() else 90
|
|
ok, output = self._run(f"git log --diff-filter=A --format='%aI %n' --since='{days} days ago' | head -20")
|
|
if not ok:
|
|
return f"❌ {output}"
|
|
|
|
return f"📁 最近 {days} 天新增的文件:\n{output[:2000]}"
|
|
|
|
def _hooks(self, args: str) -> str:
|
|
"""管理 git hooks"""
|
|
hooks_dir = ".git/hooks"
|
|
if not os.path.exists(hooks_dir):
|
|
return "❌ 不是 git 仓库"
|
|
|
|
hooks = os.listdir(hooks_dir)
|
|
active = [h for h in hooks if not h.endswith('.sample') and os.path.isfile(os.path.join(hooks_dir, h))]
|
|
|
|
result = ["🪝 Git Hooks:\n"]
|
|
for h in hooks:
|
|
path = os.path.join(hooks_dir, h)
|
|
if h.endswith('.sample'):
|
|
result.append(f" ⭕ {h.replace('.sample', '')} (未激活)")
|
|
elif os.path.isfile(path):
|
|
result.append(f" ✅ {h} (活跃)")
|
|
|
|
if not active:
|
|
result.append("\n 没有活跃的 hooks。可以添加 pre-commit 等钩子。")
|
|
|
|
return '\n'.join(result)
|