Files
xiaoli-cli/plugins/git_workflow.py
T
Xiaoli Dev b1b2097311 feat: 新增 3 个工程化自动化插件
- auto_engineer: lint/format/test/build/deps/info/init
- git_workflow: 规范提交/changelog/变更摘要/贡献者统计
- project_analyzer: 复杂度/依赖/重复/安全/质量指标/改进建议

工具总数: 17 个

本次更新由 mimo 自动化执行,与开发者无关。
2026-04-25 11:28:53 +08:00

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)