From 0e451a4aeeee9ec944e80f2a80aa00a8e1d678c9 Mon Sep 17 00:00:00 2001 From: Li Ping <1477412247@qq.com> Date: Tue, 2 Sep 2025 05:16:34 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9Epatchkit=5Fmcp=20MCP?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加完整的patchkit MCP服务器实现,包含配置、核心功能模块、文档和依赖文件。 --- servers/patchkit_mcp/mcp-rpm.yaml | 20 + servers/patchkit_mcp/mcp_config.json | 16 + servers/patchkit_mcp/src/README.md | 142 ++++ servers/patchkit_mcp/src/icon.png | Bin 0 -> 10005 bytes .../patchkit_mcp/src/mcp-patch-resolver.py | 274 ++++++++ servers/patchkit_mcp/src/patchkit.py | 637 ++++++++++++++++++ servers/patchkit_mcp/src/requirements.txt | 3 + 7 files changed, 1092 insertions(+) create mode 100644 servers/patchkit_mcp/mcp-rpm.yaml create mode 100644 servers/patchkit_mcp/mcp_config.json create mode 100644 servers/patchkit_mcp/src/README.md create mode 100644 servers/patchkit_mcp/src/icon.png create mode 100644 servers/patchkit_mcp/src/mcp-patch-resolver.py create mode 100644 servers/patchkit_mcp/src/patchkit.py create mode 100644 servers/patchkit_mcp/src/requirements.txt diff --git a/servers/patchkit_mcp/mcp-rpm.yaml b/servers/patchkit_mcp/mcp-rpm.yaml new file mode 100644 index 0000000..9526587 --- /dev/null +++ b/servers/patchkit_mcp/mcp-rpm.yaml @@ -0,0 +1,20 @@ +name: "patchkit_mcp" +summary: "OpenEuler 补丁冲突解决服务" +description: | + 自动化处理OpenEuler 补丁冲突解决的服务 + +dependencies: + system: + - python3 + - git + - patch + python: + - requests + - PyGithub + +files: + required: + - src/server.py + - mcp_config.json + optional: + - src/requirements.txt diff --git a/servers/patchkit_mcp/mcp_config.json b/servers/patchkit_mcp/mcp_config.json new file mode 100644 index 0000000..460caa3 --- /dev/null +++ b/servers/patchkit_mcp/mcp_config.json @@ -0,0 +1,16 @@ +{ + "mcpServers": { + "patchkit_mcp": { + "command": "uv", + "args": [ + "--directory", "/opt/mcp-servers/servers/patchkit_mcp/src", + "run", "mcp-patch-resolver.py", + "--model_url", "https://api.deepseek.com", + "--api_key", "", + "--model_name", "deepseek-chat" + ], + "disabled": false, + "timeout": 1800 + } + } +} diff --git a/servers/patchkit_mcp/src/README.md b/servers/patchkit_mcp/src/README.md new file mode 100644 index 0000000..b758627 --- /dev/null +++ b/servers/patchkit_mcp/src/README.md @@ -0,0 +1,142 @@ +# PatchKit - AI驱动的Patch冲突解决工具 + +PatchKit 是一个专门用于解决 Git patch 冲突的工具,支持分步处理和 AI 辅助冲突解决。 + +## 架构设计 + +### 工具结构 +- **patchkit.py**: 核心工具,提供分步处理功能 +- **mcp-patch-resolver.py**: MCP 服务器,调用 patchkit 工具 + +### 工作目录 +- 所有数据保存在 `~/.patchkit` 目录下 +- 包含源仓库、目标仓库、patch 文件和映射关系 + +## 功能特性 + +### 第一步:克隆仓库 +- 根据源仓库和目标仓库地址克隆代码 +- 智能检测已存在的仓库,避免重复下载 +- 支持指定分支 + +### 第二步:生成 Patch 和映射 +- 根据 commit 列表生成 patch 文件 +- 建立源 commit 到目标 commit 的映射关系 +- 保存映射关系到 JSON 文件 + +### 第三步:AI 冲突解决 +- 如果目标仓库已有解决的 commit,先回退 +- 使用 AI 模型解决 patch 冲突 +- 自动提交解决结果 + +## 使用方法 + +### 直接使用 patchkit 工具 + +#### 1. 克隆仓库 +```bash +python3 patchkit.py --action clone \ + --source_repo "https://github.com/user/source-repo" \ + --source_branch "main" \ + --target_repo "https://github.com/user/target-repo" \ + --target_branch "develop" +``` + +#### 2. 生成 Patch 和映射 +```bash +python3 patchkit.py --action generate \ + --commit_list '[ + { + "commit_message": "修复bug", + "source_commit": "abc123", + "target_commit": "def456" + } + ]' +``` + +#### 3. 解决冲突 +```bash +python3 patchkit.py --action resolve \ + --source_commit "abc123" +``` + +#### 4. 查看状态 +```bash +python3 patchkit.py --action status +``` + +### 使用 MCP 服务器 + +#### 启动服务器 +```bash +python3 mcp-patch-resolver.py \ + --model_url "https://api.openai.com/v1" \ + --api_key "your-api-key" \ + --model_name "gpt-4" +``` + +#### 可用的 MCP 工具 + +1. **clone_repositories**: 克隆源仓库和目标仓库 +2. **generate_patches_and_mapping**: 生成 patch 文件并建立映射关系 +3. **resolve_single_commit**: 解决指定 commit 的冲突 +4. **get_patchkit_status**: 获取当前状态 +5. **resolve_patch_conflicts_batch**: 批量解决冲突(完整流程) + +## 工作流程示例 + +### 分步处理 +1. 使用 `clone_repositories` 克隆仓库 +2. 使用 `generate_patches_and_mapping` 生成 patch 和映射 +3. 使用 `resolve_single_commit` 逐个解决冲突 +4. 使用 `get_patchkit_status` 查看进度 + +### 批量处理 +使用 `resolve_patch_conflicts_batch` 一次性完成所有步骤 + +## 目录结构 + +``` +~/.patchkit/ +├── source/ # 源仓库 +├── target/ # 目标仓库 +├── patches/ # 生成的 patch 文件 +├── commit_mapping.json # commit 映射关系 +└── temp_patch_creator/ # 临时目录(仅在需要时创建) +``` + +## 映射文件格式 + +```json +{ + "abc123": { + "commit_message": "修复bug", + "source_commit": "abc123", + "target_commit": "def456", + "patch_file": "/home/user/.patchkit/patches/abc123.patch", + "status": "resolved", + "resolution_time": "2024-01-01T12:00:00", + "resolution_details": "AI成功解决冲突" + } +} +``` + +## 状态说明 + +- **pending**: 待处理 +- **resolved**: 已解决 +- **failed**: 解决失败 + +## 注意事项 + +1. 确保有足够的磁盘空间存储仓库 +2. AI 模型需要有效的 API 密钥 +3. 网络连接稳定,避免克隆失败 +4. 建议在解决冲突前备份重要数据 + +## 错误处理 + +- 工具会自动检测和跳过已存在的有效仓库 +- 失败的冲突会记录在映射文件中 +- 可以重复执行失败的步骤 +- 详细的错误信息会记录在日志中 \ No newline at end of file diff --git a/servers/patchkit_mcp/src/icon.png b/servers/patchkit_mcp/src/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5a09d241c68d641054b035e7e3f11004b1d1e6b0 GIT binary patch literal 10005 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%0Aiv2)L4>`D+x!J7g`4pMIYJdS_tT8R+j2yGEDD zL&4_K$+r>ej|uvpI%BXBO^vD9F?-9a>Ab&kXCYi|RS7{J3S`FGCzk;Acw}GT!x&Qx z&x^$~imj!-&6hUmrFK;vQanph4UbHPFj^7Z3a4cEDPH3c5oNfAt-Mt4vd7Y5YluQ9 zkAB%SJAF~6iD&cRzRoS}QI^Ve-Q%96*a+lG(eJIMo@UsSCZQf1!+duM>=A7k;pZl0 znHr4q&V``j-XyZ?DN`3&4Pnb!9Id@Y7#JV2hZZ!m-=~C0DjS%yClknuqWjoVx%&N26>lvZ_)INnVn${L{k7c9phf$`kAM|i(EnZNFoBZ|JGE2{#$N!(4$t-Q zWh;^xER{Y9F-^0i!KnJpt`$8>JWyp%B~*~sh>Xf&TZ#Q<(vjhXN@cXdGd{`cm_rjD zvu+%o$T{ssWB+3b9tt#HrWa?6W~q#^{BC(mE;6l!an>%y6lW>Y*H=wnZOFF{OR#Jt zllXelC$r*#NlKHhKmu;6d#rav&Xo3AH`*~5Nxqk5wOc>F;01P(fPu;Wc&PCxhbv_nANP$IT}jA73nnU=GV4{s!%}uM|T{&pvn=;^=U#%?twT&kXom zTpn-kjP6xG-&3}x)T41;dfOqQy!fFGjapwKfktOj_M|KsZ}i`XrFHK<9xf#Ud;Ve+ zl%wHG9jK)bZCRIvKgaE<5(JgwUQXlibDuHTF0T7M$lCDruq2QI?WeZPngypLe~V*y zQgc>TV>|sH4k!D&hlKktdt+5i=tg4!1-5Ai@wgJXh7pVBE|z!dN4f4ufyKQLPFPRp zZoU4JkVn*c7SSSc4@K-ts%1*rC;{5Jg4>P3hg&znvFFK2h~kS|Xt3dA12&1SJzURS zaI@%EBu#a=DM4mzxtxfz`pe3fn{Q<4zdo6CJ)1jxIP^NxfT& z)|Apo*Qu?jaBcLFBQt0Z;DWoSrbTsPGdtwIm8JWY^^t-c>_lzxdsn#CzAScir9w&; zv(!Jk2h2l^ZKtS5NPjSRum0^-AH`}AA=0{`Dwo4R@iHQ*lc+49z(`?#1MzY=ex@-=BxrsWQP~@ zqg>`k|27OBMCR71V{Dm+0^O(zCH>+wMcPK{e~a1r;XKLhsdnQwJytwtV0_*bsBrI@ z{Oge66hc=1wBSv09BMY|z5)`XOVFO2SuT8C;H2P8Z1U06FSJp@bf} z{%mIdGZSDLXD5K2ws~K`|0mby*{t42bqOYXr1bJG>_}}UHUWBs`|^iuT234gUcG7d z!a_oVpXEDZYu6gD&fM_YLj~t>05_d+jHx_SITg87KYZXpbHlmr?KS-|D%q4wD6-zp zt-0W{;1kITp{Og7V1@D@^E!`vGcQ|R>x@1T4QJNrA$sX0&qUeY^nK)8S4sZiCn|8J z{e#j&NbYVT?s0QNh1`-IqjrB$!C>v;KYY}m#3R;7oMg7#^`g$d;{;H2{<+f3Q})GY zw^d=WIidY+{YO2%qxK*NicAi+P`Q2j?fuo}sy#lqt8ZagK4XvaW^F-Pls{2!NYg~} zVcX$@7ly-iedc&`jvgO|OuB?&BkaTPG%?HM|87cpI(aTNr4w$?xq+v0s Tuple[int, str, str]: + """执行patchkit命令""" + try: + # 获取patchkit脚本路径 + script_dir = os.path.dirname(os.path.abspath(__file__)) + patchkit_path = os.path.join(script_dir, "patchkit.py") + + # 添加AI模型参数 + cmd = ["python3", patchkit_path] + if model_url: + cmd.extend(["--model_url", model_url]) + if model_api_key: + cmd.extend(["--api_key", model_api_key]) + if model_name: + cmd.extend(["--model_name", model_name]) + cmd.extend(args) + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=600 # 10分钟超时 + ) + return result.returncode, result.stdout, result.stderr + except subprocess.TimeoutExpired: + return -1, "", "命令执行超时" + except Exception as e: + return -1, "", str(e) + +@mcp.tool() +async def clone_repositories( + source_repo: str, + source_branch: str, + target_repo: str, + target_branch: str +) -> str: + """第一步:克隆源仓库和目标仓库 + + Args: + source_repo: 源仓库URL + source_branch: 源仓库分支 + target_repo: 目标仓库URL + target_branch: 目标仓库分支 + """ + try: + args = [ + "--action", "clone", + "--source_repo", source_repo, + "--source_branch", source_branch, + "--target_repo", target_repo, + "--target_branch", target_branch + ] + + returncode, stdout, stderr = _run_patchkit_command(args) + + if returncode == 0: + return f"[Success]仓库克隆成功\n{stdout}" + else: + return f"[Fail]仓库克隆失败\n{stderr}" + + except Exception as e: + return f"[Fail]执行过程中出错: {e}" + +@mcp.tool() +async def generate_patches_and_mapping( + commit_mapping: str +) -> str: + """第二步:生成patch文件并建立映射关系 + + Args: + commit_mapping: commit映射的JSON字符串,格式为: + [ + { + "commit_message": "commit描述", + "source_commit": "原始仓库commit id", + "target_commit": "合入仓库commit id (可选)" + } + ] + """ + try: + # 验证JSON格式 + commits = json.loads(commit_mapping) + if not isinstance(commits, list): + return "[Fail]commit_mapping必须是JSON数组格式" + + args = [ + "--action", "generate", + "--commit_list", commit_mapping + ] + + returncode, stdout, stderr = _run_patchkit_command(args) + + if returncode == 0: + return f"[Success]patch文件生成和映射关系建立成功\n{stdout}" + else: + return f"[Fail]patch文件生成失败\n{stderr}" + + except json.JSONDecodeError as e: + return f"[Fail]JSON解析失败: {e}" + except Exception as e: + return f"[Fail]执行过程中出错: {e}" + +@mcp.tool() +async def resolve_single_commit( + source_commit: str +) -> str: + """第三步:解决指定commit的冲突 + + Args: + source_commit: 要解决的源commit ID + """ + try: + args = [ + "--action", "resolve", + "--source_commit", source_commit + ] + + returncode, stdout, stderr = _run_patchkit_command(args) + + if returncode == 0: + return f"[Success]冲突解决成功\n{stdout}" + else: + return f"[Fail]冲突解决失败\n{stderr}" + + except Exception as e: + return f"[Fail]执行过程中出错: {e}" + +@mcp.tool() +async def get_patchkit_status() -> str: + """获取patchkit当前状态""" + try: + args = ["--action", "status"] + + returncode, stdout, stderr = _run_patchkit_command(args) + + if returncode == 0: + return f"[Success]状态信息\n{stdout}" + else: + return f"[Fail]获取状态失败\n{stderr}" + + except Exception as e: + return f"[Fail]执行过程中出错: {e}" + +@mcp.tool() +async def resolve_patch_conflicts_batch( + source_repo: str, + source_branch: str, + target_repo: str, + target_branch: str, + commit_mapping: str, + output_file: str = "patch_resolution_report.csv" +) -> str: + """批量解决patch冲突(完整流程) + + Args: + source_repo: 原始仓库URL + source_branch: 原始仓库分支 + target_repo: 合入仓库URL + target_branch: 合入仓库分支 + commit_mapping: commit映射的JSON字符串 + output_file: 输出报告文件名 + """ + try: + # 解析commit映射 + commits = json.loads(commit_mapping) + if not isinstance(commits, list): + return "[Fail]commit_mapping必须是JSON数组格式" + + # 第一步:克隆仓库 + print("第一步:克隆仓库...") + clone_result = await clone_repositories(source_repo, source_branch, target_repo, target_branch) + if not clone_result.startswith("[Success]"): + return clone_result + + # 第二步:生成patch和映射 + print("第二步:生成patch文件...") + generate_result = await generate_patches_and_mapping(commit_mapping) + if not generate_result.startswith("[Success]"): + return generate_result + + # 第三步:逐个解决冲突 + print("第三步:解决冲突...") + results = [] + + for commit_info in commits: + source_commit = commit_info.get("source_commit", "") + commit_message = commit_info.get("commit_message", "") + + print(f"解决commit: {commit_message} ({source_commit})") + resolve_result = await resolve_single_commit(source_commit) + + result = { + "commit_message": commit_message, + "source_commit": source_commit, + "target_commit": commit_info.get("target_commit", ""), + "status": "成功" if resolve_result.startswith("[Success]") else "失败", + "details": resolve_result + } + results.append(result) + + # 生成报告 + report_path = os.path.abspath(output_file) + with open(report_path, 'w', newline='', encoding='utf-8') as csvfile: + fieldnames = ['commit_message', 'source_commit', 'target_commit', 'status', 'details'] + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + + writer.writeheader() + for result in results: + writer.writerow(result) + + # 统计结果 + total = len(results) + success_count = sum(1 for r in results if r["status"] == "成功") + failed_count = total - success_count + + summary = f""" +批量处理完成! + +总计: {total} 个commit +成功: {success_count} 个 +失败: {failed_count} 个 + +详细报告已保存到: {report_path} +""" + + return f"[Success]{summary}" + + except json.JSONDecodeError as e: + return f"[Fail]JSON解析失败: {e}" + except Exception as e: + return f"[Fail]处理过程中出错: {e}" + +if __name__ == "__main__": + # Parse command line arguments + parser = argparse.ArgumentParser(description='AI驱动的Patch冲突解决MCP Server') + parser.add_argument('--model_url', required=True, help='Model url') + parser.add_argument('--api_key', required=True, help='API key for the vendor') + parser.add_argument('--model_name', required=True, help='Model name') + args = parser.parse_args() + + # 设置全局变量 + model_url = args.model_url + model_api_key = args.api_key + model_name = args.model_name + + # Initialize and run the server + mcp.run(transport='stdio') \ No newline at end of file diff --git a/servers/patchkit_mcp/src/patchkit.py b/servers/patchkit_mcp/src/patchkit.py new file mode 100644 index 0000000..ac01a20 --- /dev/null +++ b/servers/patchkit_mcp/src/patchkit.py @@ -0,0 +1,637 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +PatchKit - AI驱动的Patch冲突解决工具 +支持分步处理patch冲突,包括代码拉取、patch生成、冲突解决等 +""" + +import argparse +import json +import os +import subprocess +import shutil +import csv +from pathlib import Path +from typing import List, Dict, Optional, Tuple +from datetime import datetime +from openai import OpenAI + +# 全局配置 +SYSTEM_CONTENT = """你是一名资深的Linux内核开发专家,精通Git操作和patch冲突解决。 +你的任务是分析patch冲突并提供解决方案。请仔细分析冲突内容,理解代码逻辑, +然后提供正确的合并方案。如果冲突无法自动解决,请明确说明原因。""" + +class PatchKit: + def __init__(self, model_url: str = "", model_api_key: str = "", model_name: str = ""): + self.model_url = model_url + self.model_api_key = model_api_key + self.model_name = model_name + self.work_dir = self._get_work_directory() + self.mapping_file = os.path.join(self.work_dir, "commit_mapping.json") + + def _get_work_directory(self) -> str: + """获取工作目录 ~/.patchkit""" + home_dir = os.path.expanduser("~") + work_dir = os.path.join(home_dir, ".patchkit") + os.makedirs(work_dir, exist_ok=True) + return work_dir + + def _is_repository_valid(self, repo_dir: str) -> bool: + """检查仓库是否有效""" + if not os.path.exists(repo_dir): + return False + + git_dir = os.path.join(repo_dir, ".git") + if not os.path.exists(git_dir): + return False + + # 检查git状态 + cmd = ["git", "status"] + returncode, stdout, stderr = self._run_git_command(cmd, repo_dir) + return returncode == 0 + + def _run_git_command(self, cmd: List[str], cwd: str = None) -> Tuple[int, str, str]: + """执行Git命令""" + try: + result = subprocess.run( + cmd, + cwd=cwd, + capture_output=True, + text=True, + timeout=300 + ) + return result.returncode, result.stdout, result.stderr + except subprocess.TimeoutExpired: + return -1, "", "命令执行超时" + except Exception as e: + return -1, "", str(e) + + def _call_llm(self, content: str) -> str: + """调用LLM API""" + if not self.model_api_key or not self.model_url: + return None + + try: + client = OpenAI(api_key=self.model_api_key, base_url=self.model_url) + response = client.chat.completions.create( + model=self.model_name, + messages=[ + {"role": "system", "content": SYSTEM_CONTENT}, + {"role": "user", "content": content} + ], + temperature=0.1 + ) + return response.choices[0].message.content + except Exception as e: + print(f"LLM API调用失败: {e}") + return None + + def _clone_repository(self, repo_url: str, branch: str, target_dir: str) -> bool: + """克隆仓库,如果已存在则跳过""" + print(f"检查仓库: {repo_url} 分支: {branch}") + + # 检查目标目录是否已存在且有效 + if self._is_repository_valid(target_dir): + print(f"仓库已存在且有效,跳过下载: {target_dir}") + return True + + # 如果目录存在但无效,删除它 + if os.path.exists(target_dir): + print(f"删除无效的仓库目录: {target_dir}") + shutil.rmtree(target_dir) + + # 创建目标目录 + os.makedirs(target_dir, exist_ok=True) + + # 克隆仓库 + print(f"开始克隆仓库: {repo_url} 分支: {branch}") + cmd = ["git", "clone", "-b", branch, repo_url, target_dir] + returncode, stdout, stderr = self._run_git_command(cmd) + + if returncode != 0: + print(f"克隆失败: {stderr}") + return False + + print(f"成功克隆到: {target_dir}") + return True + + def _get_commit_info(self, repo_dir: str, commit_id: str) -> Optional[Dict]: + """获取commit信息""" + cmd = ["git", "show", "--format=fuller", "--no-patch", commit_id] + returncode, stdout, stderr = self._run_git_command(cmd, repo_dir) + + if returncode != 0: + print(f"获取commit信息失败: {stderr}") + return None + + # 解析commit信息 + lines = stdout.split('\n') + commit_info = {} + + for line in lines: + if line.startswith('commit '): + commit_info['id'] = line.split()[1] + elif line.startswith('Author: '): + commit_info['author'] = line[8:] + elif line.startswith('Date: '): + commit_info['date'] = line[6:] + elif line.startswith(' '): + if 'message' not in commit_info: + commit_info['message'] = line[4:] + else: + commit_info['message'] += '\n' + line[4:] + + return commit_info + + def _create_patch(self, repo_dir: str, commit_id: str, patch_file: str) -> bool: + """创建patch文件""" + # 记录生成前的文件列表 + patch_dir = os.path.dirname(patch_file) + existing_files = set(os.listdir(patch_dir)) if os.path.exists(patch_dir) else set() + + cmd = ["git", "format-patch", "-1", commit_id, "-o", patch_dir] + returncode, stdout, stderr = self._run_git_command(cmd, repo_dir) + + if returncode != 0: + print(f"创建patch失败: {stderr}") + return False + + # 找到新生成的文件 + current_files = set(os.listdir(patch_dir)) + new_files = current_files - existing_files + + if not new_files: + print(f"未找到新生成的patch文件") + return False + + # 取第一个新生成的文件 + new_file = list(new_files)[0] + generated_patch = os.path.join(patch_dir, new_file) + print(f"找到新生成的patch文件: {new_file}") + + # 重命名到目标文件名 + shutil.move(generated_patch, patch_file) + print(f"patch文件已保存到: {patch_file}") + return True + + def _reset_to_commit(self, repo_dir: str, commit_id: str) -> bool: + """重置到指定commit""" + cmd = ["git", "reset", "--hard", commit_id] + returncode, stdout, stderr = self._run_git_command(cmd, repo_dir) + + if returncode != 0: + print(f"重置失败: {stderr}") + return False + + print(f"成功重置到commit: {commit_id}") + return True + + def _ai_apply_patch(self, target_dir: str, patch_file: str) -> Tuple[bool, str]: + """使用AI理解patch意图并应用""" + print("开始使用AI理解patch意图并应用...") + + # 读取patch内容 + with open(patch_file, 'r', encoding='utf-8') as f: + patch_content = f.read() + + # 解析patch文件,找到要修改的文件 + import re + file_pattern = r'^diff --git a/(.+) b/\1' + files_to_modify = [] + + for line in patch_content.split('\n'): + match = re.match(file_pattern, line) + if match: + file_path = match.group(1) + files_to_modify.append(file_path) + + print(f"需要修改的文件: {files_to_modify}") + + if not files_to_modify: + return False, "无法解析patch文件中的文件路径" + + # 为每个文件应用修改 + for file_path in files_to_modify: + full_path = os.path.join(target_dir, file_path) + if not os.path.exists(full_path): + print(f"文件不存在: {full_path}") + continue + + print(f"处理文件: {file_path}") + + # 读取当前文件内容 + with open(full_path, 'r', encoding='utf-8') as f: + current_content = f.read() + + # 构建AI提示 + prompt = f""" +请帮我应用以下patch到文件中。 + +Patch内容: +{patch_content} + +当前文件内容: +{current_content} + +请根据patch内容修改文件,返回完整的修改后的文件内容。 +如果patch无法直接应用,请尝试理解patch的意图并进行相应的修改。 + +请只返回修改后的文件内容,不要包含其他解释。 +""" + + # 调用AI + ai_response = self._call_llm(prompt) + if not ai_response: + return False, f"AI调用失败,无法处理文件 {file_path}" + + # 应用AI的修改 + try: + with open(full_path, 'w', encoding='utf-8') as f: + f.write(ai_response) + + # 添加到暂存区 + cmd = ["git", "add", file_path] + returncode, stdout, stderr = self._run_git_command(cmd, target_dir) + if returncode != 0: + return False, f"添加文件失败: {stderr}" + + except Exception as e: + return False, f"写入文件失败: {e}" + + # 提交结果 + cmd = ["git", "commit", "-m", "Applied patch with AI assistance"] + returncode, stdout, stderr = self._run_git_command(cmd, target_dir) + if returncode == 0: + return True, "AI应用成功" + else: + return False, f"提交失败: {stderr}" + + def _commit_exists(self, repo_dir: str, commit_id: str) -> bool: + """检查commit是否存在""" + cmd = ["git", "rev-parse", "--quiet", "--verify", commit_id] + returncode, stdout, stderr = self._run_git_command(cmd, repo_dir) + return returncode == 0 + + def _reset_to_previous_commit(self, repo_dir: str, commit_id: str) -> bool: + """重置到指定commit的上一个commit""" + # 获取指定commit的上一个commit + cmd = ["git", "rev-parse", f"{commit_id}^"] + returncode, stdout, stderr = self._run_git_command(cmd, repo_dir) + + if returncode != 0: + print(f"获取上一个commit失败: {stderr}") + return False + + previous_commit = stdout.strip() + print(f"上一个commit: {previous_commit}") + + # 重置到上一个commit + cmd = ["git", "reset", "--hard", previous_commit] + returncode, stdout, stderr = self._run_git_command(cmd, repo_dir) + + if returncode != 0: + print(f"重置失败: {stderr}") + return False + + print(f"成功重置到commit {commit_id} 的上一个commit: {previous_commit}") + return True + + def _apply_patch_with_ai(self, target_dir: str, patch_file: str) -> Tuple[bool, str]: + """使用AI解决patch冲突""" + print(f"尝试应用patch: {patch_file}") + + # 首先尝试直接应用patch + cmd = ["git", "apply", "--check", patch_file] + returncode, stdout, stderr = self._run_git_command(cmd, target_dir) + + if returncode == 0: + # 没有冲突,直接应用 + cmd = ["git", "apply", patch_file] + returncode, stdout, stderr = self._run_git_command(cmd, target_dir) + if returncode == 0: + return True, "直接应用成功" + else: + return False, f"应用失败: {stderr}" + + # 有冲突,尝试使用3way模式应用 + print("检测到冲突,尝试使用3way模式应用...") + cmd = ["git", "apply", "--3way", patch_file] + returncode, stdout, stderr = self._run_git_command(cmd, target_dir) + + # 检查应用后的状态 + cmd = ["git", "status", "--porcelain"] + returncode, stdout, stderr = self._run_git_command(cmd, target_dir) + + if returncode != 0: + return False, f"获取状态失败: {stderr}" + + # 查找所有冲突文件(UU表示冲突,U表示未暂存) + conflict_files = [] + unstaged_files = [] + for line in stdout.split('\n'): + if line.startswith('UU '): + conflict_files.append(line[3:]) + elif line.startswith('U '): + unstaged_files.append(line[2:]) + + print(f"发现冲突文件: {conflict_files}") + print(f"发现未暂存文件: {unstaged_files}") + + # 如果没有冲突文件,但有未暂存文件,说明部分应用成功 + if not conflict_files and unstaged_files: + print("部分应用成功,添加未暂存文件...") + for file in unstaged_files: + cmd = ["git", "add", file] + returncode, stdout, stderr = self._run_git_command(cmd, target_dir) + if returncode != 0: + return False, f"添加文件失败: {stderr}" + + # 提交结果 + cmd = ["git", "commit", "-m", "Applied patch with AI assistance"] + returncode, stdout, stderr = self._run_git_command(cmd, target_dir) + if returncode == 0: + return True, "AI辅助应用成功" + else: + return False, f"提交失败: {stderr}" + + # 如果3way应用失败且没有冲突文件,说明patch无法直接应用 + # 这种情况下,我们需要让AI理解patch的意图并手动应用 + if not conflict_files and not unstaged_files: + print("3way应用失败,使用AI理解patch意图并应用...") + return self._ai_apply_patch(target_dir, patch_file) + + if not conflict_files: + return False, "未找到冲突文件,但patch应用可能失败" + + # 读取patch内容 + with open(patch_file, 'r', encoding='utf-8') as f: + patch_content = f.read() + + # 为每个冲突文件调用AI + for conflict_file in conflict_files: + file_path = os.path.join(target_dir, conflict_file) + if not os.path.exists(file_path): + continue + + # 读取冲突文件内容 + with open(file_path, 'r', encoding='utf-8') as f: + file_content = f.read() + + # 构建AI提示 + prompt = f""" +请帮我解决以下Git patch冲突。 + +Patch文件内容: +{patch_content} + +冲突文件内容: +{file_content} + +请分析冲突并提供解决方案。如果能够自动解决,请提供完整的合并后的文件内容。 +如果无法自动解决,请说明原因。 + +请只返回合并后的文件内容,不要包含其他解释。 +""" + + # 调用AI + ai_response = self._call_llm(prompt) + if not ai_response: + return False, "AI调用失败" + + # 应用AI的解决方案 + try: + with open(file_path, 'w', encoding='utf-8') as f: + f.write(ai_response) + + # 添加到暂存区 + cmd = ["git", "add", conflict_file] + returncode, stdout, stderr = self._run_git_command(cmd, target_dir) + if returncode != 0: + return False, f"添加文件失败: {stderr}" + + except Exception as e: + return False, f"写入文件失败: {e}" + + # 提交解决结果 + cmd = ["git", "commit", "-m", "AI resolved patch conflicts"] + returncode, stdout, stderr = self._run_git_command(cmd, target_dir) + + if returncode == 0: + return True, "AI成功解决冲突" + else: + return False, f"提交失败: {stderr}" + + def _load_mapping(self) -> Dict: + """加载commit映射关系""" + if os.path.exists(self.mapping_file): + try: + with open(self.mapping_file, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"加载映射文件失败: {e}") + return {} + + def _save_mapping(self, mapping: Dict): + """保存commit映射关系""" + try: + with open(self.mapping_file, 'w', encoding='utf-8') as f: + json.dump(mapping, f, indent=2, ensure_ascii=False) + print(f"映射关系已保存到: {self.mapping_file}") + except Exception as e: + print(f"保存映射文件失败: {e}") + + def step1_clone_repositories(self, source_repo: str, source_branch: str, + target_repo: str, target_branch: str) -> bool: + """第一步:克隆源仓库和目标仓库""" + print("=== 第一步:克隆仓库 ===") + + source_dir = os.path.join(self.work_dir, "source") + target_dir = os.path.join(self.work_dir, "target") + + # 克隆源仓库 + if not self._clone_repository(source_repo, source_branch, source_dir): + print("克隆源仓库失败") + return False + + # 克隆目标仓库 + if not self._clone_repository(target_repo, target_branch, target_dir): + print("克隆目标仓库失败") + return False + + print("仓库克隆完成") + return True + + def step2_generate_patches_and_mapping(self, commit_list: List[Dict]) -> bool: + """第二步:生成patch文件并建立映射关系""" + print("=== 第二步:生成patch文件并建立映射关系 ===") + + source_dir = os.path.join(self.work_dir, "source") + patches_dir = os.path.join(self.work_dir, "patches") + os.makedirs(patches_dir, exist_ok=True) + + # 加载现有映射 + mapping = self._load_mapping() + + for commit_info in commit_list: + commit_message = commit_info.get("commit_message", "") + source_commit = commit_info.get("source_commit", "") + target_commit = commit_info.get("target_commit", "") + + print(f"处理commit: {commit_message}") + print(f"源commit: {source_commit}") + print(f"目标commit: {target_commit}") + + # 生成patch文件 + patch_file = os.path.join(patches_dir, f"{source_commit}.patch") + if not self._create_patch(source_dir, source_commit, patch_file): + print(f"为commit {source_commit} 生成patch失败") + continue + + # 建立映射关系 + mapping[source_commit] = { + "commit_message": commit_message, + "source_commit": source_commit, + "target_commit": target_commit, + "patch_file": patch_file, + "status": "pending" + } + + # 保存映射关系 + self._save_mapping(mapping) + print("patch文件生成和映射关系建立完成") + return True + + def step3_resolve_conflicts(self, source_commit: str) -> Tuple[bool, str]: + """第三步:解决指定commit的冲突""" + print(f"=== 第三步:解决commit {source_commit} 的冲突 ===") + + target_dir = os.path.join(self.work_dir, "target") + mapping = self._load_mapping() + + if source_commit not in mapping: + return False, f"未找到commit {source_commit} 的映射关系" + + commit_info = mapping[source_commit] + target_commit = commit_info.get("target_commit", "") + patch_file = commit_info.get("patch_file", "") + + if not os.path.exists(patch_file): + return False, f"patch文件不存在: {patch_file}" + + # 如果目标仓库已有解决的commit,先回退到该commit的上一个commit + if target_commit: + print(f"目标仓库已有commit: {target_commit},检查是否需要回退") + # 检查目标commit是否存在 + if self._commit_exists(target_dir, target_commit): + print(f"目标commit存在,回退到上一个commit") + if not self._reset_to_previous_commit(target_dir, target_commit): + return False, f"回退到commit {target_commit} 的上一个commit失败" + else: + print(f"目标commit不存在,跳过回退步骤") + + # 使用AI应用patch + success, details = self._apply_patch_with_ai(target_dir, patch_file) + + # 更新映射状态 + if success: + mapping[source_commit]["status"] = "resolved" + mapping[source_commit]["resolution_time"] = datetime.now().isoformat() + mapping[source_commit]["resolution_details"] = details + else: + mapping[source_commit]["status"] = "failed" + mapping[source_commit]["error_details"] = details + + self._save_mapping(mapping) + + return success, details + + def get_status(self) -> Dict: + """获取当前状态""" + mapping = self._load_mapping() + source_dir = os.path.join(self.work_dir, "source") + target_dir = os.path.join(self.work_dir, "target") + + status = { + "work_directory": self.work_dir, + "source_repo_exists": self._is_repository_valid(source_dir), + "target_repo_exists": self._is_repository_valid(target_dir), + "mapping_file": self.mapping_file, + "total_commits": len(mapping), + "pending_commits": sum(1 for c in mapping.values() if c.get("status") == "pending"), + "resolved_commits": sum(1 for c in mapping.values() if c.get("status") == "resolved"), + "failed_commits": sum(1 for c in mapping.values() if c.get("status") == "failed"), + "commits": mapping + } + + return status + +def main(): + parser = argparse.ArgumentParser(description='PatchKit - AI驱动的Patch冲突解决工具') + parser.add_argument('--model_url', help='Model URL') + parser.add_argument('--api_key', help='API key') + parser.add_argument('--model_name', help='Model name') + parser.add_argument('--action', required=True, + choices=['clone', 'generate', 'resolve', 'status'], + help='执行的操作') + + # clone操作的参数 + parser.add_argument('--source_repo', help='源仓库URL') + parser.add_argument('--source_branch', help='源仓库分支') + parser.add_argument('--target_repo', help='目标仓库URL') + parser.add_argument('--target_branch', help='目标仓库分支') + + # generate操作的参数 + parser.add_argument('--commit_list', help='commit列表的JSON字符串') + + # resolve操作的参数 + parser.add_argument('--source_commit', help='要解决的源commit ID') + + args = parser.parse_args() + + # 创建PatchKit实例 + patchkit = PatchKit(args.model_url, args.api_key, args.model_name) + + if args.action == 'clone': + if not all([args.source_repo, args.source_branch, args.target_repo, args.target_branch]): + print("clone操作需要提供所有仓库参数") + return 1 + + success = patchkit.step1_clone_repositories( + args.source_repo, args.source_branch, + args.target_repo, args.target_branch + ) + return 0 if success else 1 + + elif args.action == 'generate': + if not args.commit_list: + print("generate操作需要提供commit_list参数") + return 1 + + try: + commit_list = json.loads(args.commit_list) + success = patchkit.step2_generate_patches_and_mapping(commit_list) + return 0 if success else 1 + except json.JSONDecodeError as e: + print(f"JSON解析失败: {e}") + return 1 + + elif args.action == 'resolve': + if not args.source_commit: + print("resolve操作需要提供source_commit参数") + return 1 + + success, details = patchkit.step3_resolve_conflicts(args.source_commit) + if success: + print(f"冲突解决成功: {details}") + return 0 + else: + print(f"冲突解决失败: {details}") + return 1 + + elif args.action == 'status': + status = patchkit.get_status() + print(json.dumps(status, indent=2, ensure_ascii=False)) + return 0 + +if __name__ == "__main__": + exit(main()) diff --git a/servers/patchkit_mcp/src/requirements.txt b/servers/patchkit_mcp/src/requirements.txt new file mode 100644 index 0000000..03de40e --- /dev/null +++ b/servers/patchkit_mcp/src/requirements.txt @@ -0,0 +1,3 @@ +mcp +openai +pyyaml -- Gitee