jarvis-ai-assistant 0.1.222__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_agent/__init__.py +1143 -245
- jarvis/jarvis_agent/agent_manager.py +97 -0
- jarvis/jarvis_agent/builtin_input_handler.py +12 -10
- jarvis/jarvis_agent/config_editor.py +57 -0
- jarvis/jarvis_agent/edit_file_handler.py +392 -99
- jarvis/jarvis_agent/event_bus.py +48 -0
- jarvis/jarvis_agent/events.py +157 -0
- jarvis/jarvis_agent/file_context_handler.py +79 -0
- jarvis/jarvis_agent/file_methodology_manager.py +117 -0
- jarvis/jarvis_agent/jarvis.py +1117 -147
- jarvis/jarvis_agent/main.py +78 -34
- jarvis/jarvis_agent/memory_manager.py +195 -0
- jarvis/jarvis_agent/methodology_share_manager.py +174 -0
- jarvis/jarvis_agent/prompt_manager.py +82 -0
- jarvis/jarvis_agent/prompts.py +46 -9
- jarvis/jarvis_agent/protocols.py +4 -1
- jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
- jarvis/jarvis_agent/run_loop.py +146 -0
- jarvis/jarvis_agent/session_manager.py +9 -9
- jarvis/jarvis_agent/share_manager.py +228 -0
- jarvis/jarvis_agent/shell_input_handler.py +23 -3
- jarvis/jarvis_agent/stdio_redirect.py +295 -0
- jarvis/jarvis_agent/task_analyzer.py +212 -0
- jarvis/jarvis_agent/task_manager.py +154 -0
- jarvis/jarvis_agent/task_planner.py +496 -0
- jarvis/jarvis_agent/tool_executor.py +8 -4
- jarvis/jarvis_agent/tool_share_manager.py +139 -0
- jarvis/jarvis_agent/user_interaction.py +42 -0
- jarvis/jarvis_agent/utils.py +54 -0
- jarvis/jarvis_agent/web_bridge.py +189 -0
- jarvis/jarvis_agent/web_output_sink.py +53 -0
- jarvis/jarvis_agent/web_server.py +751 -0
- jarvis/jarvis_c2rust/__init__.py +26 -0
- jarvis/jarvis_c2rust/cli.py +613 -0
- jarvis/jarvis_c2rust/collector.py +258 -0
- jarvis/jarvis_c2rust/library_replacer.py +1122 -0
- jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
- jarvis/jarvis_c2rust/optimizer.py +960 -0
- jarvis/jarvis_c2rust/scanner.py +1681 -0
- jarvis/jarvis_c2rust/transpiler.py +2325 -0
- jarvis/jarvis_code_agent/build_validation_config.py +133 -0
- jarvis/jarvis_code_agent/code_agent.py +1605 -178
- jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -0
- jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +102 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
- jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -0
- jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
- jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
- jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
- jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
- jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
- jarvis/jarvis_code_agent/code_analyzer/language_support.py +89 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
- jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
- jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
- jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
- jarvis/jarvis_code_agent/lint.py +275 -13
- jarvis/jarvis_code_agent/utils.py +142 -0
- jarvis/jarvis_code_analysis/checklists/loader.py +20 -6
- jarvis/jarvis_code_analysis/code_review.py +583 -548
- jarvis/jarvis_data/config_schema.json +339 -28
- jarvis/jarvis_git_squash/main.py +22 -13
- jarvis/jarvis_git_utils/git_commiter.py +171 -55
- jarvis/jarvis_mcp/sse_mcp_client.py +22 -15
- jarvis/jarvis_mcp/stdio_mcp_client.py +4 -4
- jarvis/jarvis_mcp/streamable_mcp_client.py +36 -16
- jarvis/jarvis_memory_organizer/memory_organizer.py +753 -0
- jarvis/jarvis_methodology/main.py +48 -63
- jarvis/jarvis_multi_agent/__init__.py +302 -43
- jarvis/jarvis_multi_agent/main.py +70 -24
- jarvis/jarvis_platform/ai8.py +40 -23
- jarvis/jarvis_platform/base.py +210 -49
- jarvis/jarvis_platform/human.py +11 -1
- jarvis/jarvis_platform/kimi.py +82 -76
- jarvis/jarvis_platform/openai.py +73 -1
- jarvis/jarvis_platform/registry.py +8 -15
- jarvis/jarvis_platform/tongyi.py +115 -101
- jarvis/jarvis_platform/yuanbao.py +89 -63
- jarvis/jarvis_platform_manager/main.py +194 -132
- jarvis/jarvis_platform_manager/service.py +122 -86
- jarvis/jarvis_rag/cli.py +156 -53
- jarvis/jarvis_rag/embedding_manager.py +155 -12
- jarvis/jarvis_rag/llm_interface.py +10 -13
- jarvis/jarvis_rag/query_rewriter.py +63 -12
- jarvis/jarvis_rag/rag_pipeline.py +222 -40
- jarvis/jarvis_rag/reranker.py +26 -3
- jarvis/jarvis_rag/retriever.py +270 -14
- jarvis/jarvis_sec/__init__.py +3605 -0
- jarvis/jarvis_sec/checkers/__init__.py +32 -0
- jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
- jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
- jarvis/jarvis_sec/cli.py +116 -0
- jarvis/jarvis_sec/report.py +257 -0
- jarvis/jarvis_sec/status.py +264 -0
- jarvis/jarvis_sec/types.py +20 -0
- jarvis/jarvis_sec/workflow.py +219 -0
- jarvis/jarvis_smart_shell/main.py +405 -137
- jarvis/jarvis_stats/__init__.py +13 -0
- jarvis/jarvis_stats/cli.py +387 -0
- jarvis/jarvis_stats/stats.py +711 -0
- jarvis/jarvis_stats/storage.py +612 -0
- jarvis/jarvis_stats/visualizer.py +282 -0
- jarvis/jarvis_tools/ask_user.py +1 -0
- jarvis/jarvis_tools/base.py +18 -2
- jarvis/jarvis_tools/clear_memory.py +239 -0
- jarvis/jarvis_tools/cli/main.py +220 -144
- jarvis/jarvis_tools/execute_script.py +52 -12
- jarvis/jarvis_tools/file_analyzer.py +17 -12
- jarvis/jarvis_tools/generate_new_tool.py +46 -24
- jarvis/jarvis_tools/read_code.py +277 -18
- jarvis/jarvis_tools/read_symbols.py +141 -0
- jarvis/jarvis_tools/read_webpage.py +86 -13
- jarvis/jarvis_tools/registry.py +294 -90
- jarvis/jarvis_tools/retrieve_memory.py +227 -0
- jarvis/jarvis_tools/save_memory.py +194 -0
- jarvis/jarvis_tools/search_web.py +62 -28
- jarvis/jarvis_tools/sub_agent.py +205 -0
- jarvis/jarvis_tools/sub_code_agent.py +217 -0
- jarvis/jarvis_tools/virtual_tty.py +330 -62
- jarvis/jarvis_utils/builtin_replace_map.py +4 -5
- jarvis/jarvis_utils/clipboard.py +90 -0
- jarvis/jarvis_utils/config.py +607 -50
- jarvis/jarvis_utils/embedding.py +3 -0
- jarvis/jarvis_utils/fzf.py +57 -0
- jarvis/jarvis_utils/git_utils.py +251 -29
- jarvis/jarvis_utils/globals.py +174 -17
- jarvis/jarvis_utils/http.py +58 -79
- jarvis/jarvis_utils/input.py +899 -153
- jarvis/jarvis_utils/methodology.py +210 -83
- jarvis/jarvis_utils/output.py +220 -137
- jarvis/jarvis_utils/utils.py +1906 -135
- jarvis_ai_assistant-0.7.0.dist-info/METADATA +465 -0
- jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +8 -2
- jarvis/jarvis_git_details/main.py +0 -265
- jarvis/jarvis_platform/oyi.py +0 -357
- jarvis/jarvis_tools/edit_file.py +0 -255
- jarvis/jarvis_tools/rewrite_file.py +0 -195
- jarvis_ai_assistant-0.1.222.dist-info/METADATA +0 -767
- jarvis_ai_assistant-0.1.222.dist-info/RECORD +0 -110
- /jarvis/{jarvis_git_details → jarvis_memory_organizer}/__init__.py +0 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
"""编辑影响范围分析模块。
|
|
2
|
+
|
|
3
|
+
提供代码编辑影响范围分析功能,识别可能受影响的文件、函数、测试等。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import ast
|
|
9
|
+
import subprocess
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import List, Optional, Set, Dict
|
|
12
|
+
from enum import Enum
|
|
13
|
+
|
|
14
|
+
from .context_manager import ContextManager
|
|
15
|
+
from .file_ignore import filter_walk_dirs
|
|
16
|
+
from .symbol_extractor import Symbol
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ImpactType(Enum):
|
|
21
|
+
"""影响类型枚举"""
|
|
22
|
+
REFERENCE = "reference" # 符号引用
|
|
23
|
+
DEPENDENT = "dependent" # 依赖的符号
|
|
24
|
+
TEST = "test" # 测试文件
|
|
25
|
+
INTERFACE_CHANGE = "interface_change" # 接口变更
|
|
26
|
+
DEPENDENCY_CHAIN = "dependency_chain" # 依赖链
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RiskLevel(Enum):
|
|
30
|
+
"""风险等级枚举"""
|
|
31
|
+
LOW = "low"
|
|
32
|
+
MEDIUM = "medium"
|
|
33
|
+
HIGH = "high"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Impact:
|
|
38
|
+
"""表示一个影响项"""
|
|
39
|
+
impact_type: ImpactType
|
|
40
|
+
target: str # 受影响的目标(文件路径、符号名等)
|
|
41
|
+
description: str = ""
|
|
42
|
+
line: Optional[int] = None
|
|
43
|
+
severity: RiskLevel = RiskLevel.LOW
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class InterfaceChange:
|
|
48
|
+
"""表示接口变更"""
|
|
49
|
+
symbol_name: str
|
|
50
|
+
change_type: str # 'signature', 'return_type', 'parameter', 'removed', 'added'
|
|
51
|
+
file_path: str
|
|
52
|
+
line: int
|
|
53
|
+
before: Optional[str] = None
|
|
54
|
+
after: Optional[str] = None
|
|
55
|
+
description: str = ""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class ImpactReport:
|
|
60
|
+
"""影响分析报告"""
|
|
61
|
+
affected_files: List[str] = field(default_factory=list)
|
|
62
|
+
affected_symbols: List[Symbol] = field(default_factory=list)
|
|
63
|
+
affected_tests: List[str] = field(default_factory=list)
|
|
64
|
+
interface_changes: List[InterfaceChange] = field(default_factory=list)
|
|
65
|
+
impacts: List[Impact] = field(default_factory=list)
|
|
66
|
+
risk_level: RiskLevel = RiskLevel.LOW
|
|
67
|
+
recommendations: List[str] = field(default_factory=list)
|
|
68
|
+
|
|
69
|
+
def to_string(self, project_root: str = "") -> str:
|
|
70
|
+
"""生成可读的影响报告字符串"""
|
|
71
|
+
lines = []
|
|
72
|
+
lines.append("=" * 60)
|
|
73
|
+
lines.append("编辑影响范围分析报告")
|
|
74
|
+
lines.append("=" * 60)
|
|
75
|
+
|
|
76
|
+
# 风险等级
|
|
77
|
+
risk_emoji = {
|
|
78
|
+
RiskLevel.LOW: "🟢",
|
|
79
|
+
RiskLevel.MEDIUM: "🟡",
|
|
80
|
+
RiskLevel.HIGH: "🔴"
|
|
81
|
+
}
|
|
82
|
+
lines.append(f"\n风险等级: {risk_emoji.get(self.risk_level, '⚪')} {self.risk_level.value.upper()}")
|
|
83
|
+
|
|
84
|
+
# 受影响文件
|
|
85
|
+
if self.affected_files:
|
|
86
|
+
lines.append(f"\n受影响文件 ({len(self.affected_files)}):")
|
|
87
|
+
for file_path in self.affected_files[:10]:
|
|
88
|
+
rel_path = os.path.relpath(file_path, project_root) if project_root else file_path
|
|
89
|
+
lines.append(f" - {rel_path}")
|
|
90
|
+
if len(self.affected_files) > 10:
|
|
91
|
+
lines.append(f" ... 还有 {len(self.affected_files) - 10} 个文件")
|
|
92
|
+
|
|
93
|
+
# 受影响符号
|
|
94
|
+
if self.affected_symbols:
|
|
95
|
+
lines.append(f"\n受影响符号 ({len(self.affected_symbols)}):")
|
|
96
|
+
for symbol in self.affected_symbols[:10]:
|
|
97
|
+
lines.append(f" - {symbol.kind} {symbol.name} ({os.path.basename(symbol.file_path)}:{symbol.line_start})")
|
|
98
|
+
if len(self.affected_symbols) > 10:
|
|
99
|
+
lines.append(f" ... 还有 {len(self.affected_symbols) - 10} 个符号")
|
|
100
|
+
|
|
101
|
+
# 受影响测试
|
|
102
|
+
if self.affected_tests:
|
|
103
|
+
lines.append(f"\n受影响测试 ({len(self.affected_tests)}):")
|
|
104
|
+
for test_file in self.affected_tests[:10]:
|
|
105
|
+
rel_path = os.path.relpath(test_file, project_root) if project_root else test_file
|
|
106
|
+
lines.append(f" - {rel_path}")
|
|
107
|
+
if len(self.affected_tests) > 10:
|
|
108
|
+
lines.append(f" ... 还有 {len(self.affected_tests) - 10} 个测试文件")
|
|
109
|
+
|
|
110
|
+
# 接口变更
|
|
111
|
+
if self.interface_changes:
|
|
112
|
+
lines.append(f"\n接口变更 ({len(self.interface_changes)}):")
|
|
113
|
+
for change in self.interface_changes[:10]:
|
|
114
|
+
lines.append(f" - {change.symbol_name}: {change.change_type}")
|
|
115
|
+
if change.description:
|
|
116
|
+
lines.append(f" {change.description}")
|
|
117
|
+
if len(self.interface_changes) > 10:
|
|
118
|
+
lines.append(f" ... 还有 {len(self.interface_changes) - 10} 个接口变更")
|
|
119
|
+
|
|
120
|
+
# 建议
|
|
121
|
+
if self.recommendations:
|
|
122
|
+
lines.append("\n建议:")
|
|
123
|
+
for i, rec in enumerate(self.recommendations, 1):
|
|
124
|
+
lines.append(f" {i}. {rec}")
|
|
125
|
+
|
|
126
|
+
lines.append("\n" + "=" * 60)
|
|
127
|
+
return "\n".join(lines)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class Edit:
|
|
132
|
+
"""表示一个编辑操作"""
|
|
133
|
+
file_path: str
|
|
134
|
+
line_start: int
|
|
135
|
+
line_end: int
|
|
136
|
+
before: str = ""
|
|
137
|
+
after: str = ""
|
|
138
|
+
edit_type: str = "modify" # 'modify', 'add', 'delete'
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TestDiscoverer:
|
|
142
|
+
"""测试文件发现器"""
|
|
143
|
+
|
|
144
|
+
# 测试文件命名模式
|
|
145
|
+
TEST_PATTERNS = {
|
|
146
|
+
'python': [
|
|
147
|
+
r'test_.*\.py$',
|
|
148
|
+
r'.*_test\.py$',
|
|
149
|
+
],
|
|
150
|
+
'javascript': [
|
|
151
|
+
r'.*\.test\.(js|ts|jsx|tsx)$',
|
|
152
|
+
r'.*\.spec\.(js|ts|jsx|tsx)$',
|
|
153
|
+
],
|
|
154
|
+
'rust': [
|
|
155
|
+
r'.*_test\.rs$',
|
|
156
|
+
],
|
|
157
|
+
'java': [
|
|
158
|
+
r'.*Test\.java$',
|
|
159
|
+
r'.*Tests\.java$',
|
|
160
|
+
],
|
|
161
|
+
'go': [
|
|
162
|
+
r'.*_test\.go$',
|
|
163
|
+
],
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
def __init__(self, project_root: str):
|
|
167
|
+
self.project_root = project_root
|
|
168
|
+
|
|
169
|
+
def find_test_files(self, file_path: str) -> List[str]:
|
|
170
|
+
"""查找与文件相关的测试文件"""
|
|
171
|
+
test_files: List[str] = []
|
|
172
|
+
|
|
173
|
+
# 检测语言
|
|
174
|
+
language = self._detect_language(file_path)
|
|
175
|
+
if not language:
|
|
176
|
+
return test_files
|
|
177
|
+
|
|
178
|
+
# 获取测试文件模式
|
|
179
|
+
patterns = self.TEST_PATTERNS.get(language, [])
|
|
180
|
+
if not patterns:
|
|
181
|
+
return test_files
|
|
182
|
+
|
|
183
|
+
# 获取文件的基础名称(不含扩展名)
|
|
184
|
+
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
|
185
|
+
|
|
186
|
+
# 在项目根目录搜索测试文件
|
|
187
|
+
for root, dirs, files in os.walk(self.project_root):
|
|
188
|
+
# 跳过隐藏目录和常见忽略目录
|
|
189
|
+
dirs[:] = filter_walk_dirs(dirs)
|
|
190
|
+
|
|
191
|
+
for file in files:
|
|
192
|
+
file_path_full = os.path.join(root, file)
|
|
193
|
+
|
|
194
|
+
# 检查是否匹配测试文件模式
|
|
195
|
+
for pattern in patterns:
|
|
196
|
+
if re.match(pattern, file, re.IGNORECASE):
|
|
197
|
+
# 检查测试文件是否可能测试目标文件
|
|
198
|
+
if self._might_test_file(file_path_full, file_path, base_name):
|
|
199
|
+
test_files.append(file_path_full)
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
return list(set(test_files))
|
|
203
|
+
|
|
204
|
+
def _might_test_file(self, test_file: str, target_file: str, base_name: str) -> bool:
|
|
205
|
+
"""判断测试文件是否可能测试目标文件"""
|
|
206
|
+
# 读取测试文件内容,查找目标文件的引用
|
|
207
|
+
try:
|
|
208
|
+
with open(test_file, 'r', encoding='utf-8', errors='replace') as f:
|
|
209
|
+
content = f.read()
|
|
210
|
+
|
|
211
|
+
# 检查是否导入或引用了目标文件
|
|
212
|
+
# 简单的启发式方法:检查文件名、模块名等
|
|
213
|
+
target_base = os.path.splitext(os.path.basename(target_file))[0]
|
|
214
|
+
|
|
215
|
+
# 检查导入语句
|
|
216
|
+
import_patterns = [
|
|
217
|
+
rf'import\s+.*{re.escape(target_base)}',
|
|
218
|
+
rf'from\s+.*{re.escape(target_base)}',
|
|
219
|
+
rf'use\s+.*{re.escape(target_base)}', # Rust
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
for pattern in import_patterns:
|
|
223
|
+
if re.search(pattern, content, re.IGNORECASE):
|
|
224
|
+
return True
|
|
225
|
+
|
|
226
|
+
# 检查文件名是否出现在测试文件中
|
|
227
|
+
if target_base.lower() in content.lower():
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
except Exception:
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
def _detect_language(self, file_path: str) -> Optional[str]:
|
|
236
|
+
"""检测文件语言"""
|
|
237
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
238
|
+
ext_map = {
|
|
239
|
+
'.py': 'python',
|
|
240
|
+
'.js': 'javascript',
|
|
241
|
+
'.ts': 'javascript',
|
|
242
|
+
'.jsx': 'javascript',
|
|
243
|
+
'.tsx': 'javascript',
|
|
244
|
+
'.rs': 'rust',
|
|
245
|
+
'.java': 'java',
|
|
246
|
+
'.go': 'go',
|
|
247
|
+
}
|
|
248
|
+
return ext_map.get(ext)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class ImpactAnalyzer:
|
|
252
|
+
"""编辑影响范围分析器"""
|
|
253
|
+
|
|
254
|
+
def __init__(self, context_manager: ContextManager):
|
|
255
|
+
self.context_manager = context_manager
|
|
256
|
+
self.project_root = context_manager.project_root
|
|
257
|
+
self.test_discoverer = TestDiscoverer(self.project_root)
|
|
258
|
+
|
|
259
|
+
def analyze_edit_impact(self, file_path: str, edits: List[Edit]) -> ImpactReport:
|
|
260
|
+
"""分析编辑的影响范围
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
file_path: 被编辑的文件路径
|
|
264
|
+
edits: 编辑操作列表
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
ImpactReport: 影响分析报告
|
|
268
|
+
"""
|
|
269
|
+
impacts: List[Impact] = []
|
|
270
|
+
affected_symbols: Set[Symbol] = set()
|
|
271
|
+
affected_files: Set[str] = {file_path}
|
|
272
|
+
interface_changes: List[InterfaceChange] = []
|
|
273
|
+
|
|
274
|
+
# 1. 分析每个编辑的影响
|
|
275
|
+
for edit in edits:
|
|
276
|
+
# 分析符号影响
|
|
277
|
+
symbols_in_edit = self._find_symbols_in_edit(file_path, edit)
|
|
278
|
+
for symbol in symbols_in_edit:
|
|
279
|
+
affected_symbols.add(symbol)
|
|
280
|
+
symbol_impacts = self._analyze_symbol_impact(symbol, edit)
|
|
281
|
+
impacts.extend(symbol_impacts)
|
|
282
|
+
|
|
283
|
+
# 收集受影响的文件
|
|
284
|
+
for impact in symbol_impacts:
|
|
285
|
+
if impact.impact_type == ImpactType.REFERENCE:
|
|
286
|
+
affected_files.add(impact.target)
|
|
287
|
+
elif impact.impact_type == ImpactType.DEPENDENT:
|
|
288
|
+
affected_files.add(impact.target)
|
|
289
|
+
|
|
290
|
+
# 2. 分析依赖链影响
|
|
291
|
+
dependency_impacts = self._analyze_dependency_chain(file_path)
|
|
292
|
+
impacts.extend(dependency_impacts)
|
|
293
|
+
for impact in dependency_impacts:
|
|
294
|
+
affected_files.add(impact.target)
|
|
295
|
+
|
|
296
|
+
# 3. 检测接口变更
|
|
297
|
+
if edits:
|
|
298
|
+
# 需要读取文件内容来比较
|
|
299
|
+
interface_changes = self._detect_interface_changes(file_path, edits)
|
|
300
|
+
for change in interface_changes:
|
|
301
|
+
affected_files.add(change.file_path)
|
|
302
|
+
|
|
303
|
+
# 4. 查找相关测试
|
|
304
|
+
test_files = self.test_discoverer.find_test_files(file_path)
|
|
305
|
+
for test_file in test_files:
|
|
306
|
+
impacts.append(Impact(
|
|
307
|
+
impact_type=ImpactType.TEST,
|
|
308
|
+
target=test_file,
|
|
309
|
+
description=f"可能测试 {os.path.basename(file_path)} 的测试文件"
|
|
310
|
+
))
|
|
311
|
+
affected_files.add(test_file)
|
|
312
|
+
|
|
313
|
+
# 5. 评估风险等级
|
|
314
|
+
risk_level = self._assess_risk(impacts, interface_changes)
|
|
315
|
+
|
|
316
|
+
# 6. 生成建议
|
|
317
|
+
recommendations = self._generate_recommendations(
|
|
318
|
+
impacts, interface_changes, affected_files, test_files
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
return ImpactReport(
|
|
322
|
+
affected_files=list(affected_files),
|
|
323
|
+
affected_symbols=list(affected_symbols),
|
|
324
|
+
affected_tests=test_files,
|
|
325
|
+
interface_changes=interface_changes,
|
|
326
|
+
impacts=impacts,
|
|
327
|
+
risk_level=risk_level,
|
|
328
|
+
recommendations=recommendations,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def _find_symbols_in_edit(self, file_path: str, edit: Edit) -> List[Symbol]:
|
|
332
|
+
"""查找编辑区域内的符号"""
|
|
333
|
+
symbols = self.context_manager.symbol_table.get_file_symbols(file_path)
|
|
334
|
+
|
|
335
|
+
# 找出在编辑范围内的符号
|
|
336
|
+
affected_symbols = []
|
|
337
|
+
for symbol in symbols:
|
|
338
|
+
# 检查符号是否与编辑区域重叠
|
|
339
|
+
if (symbol.line_start <= edit.line_end and
|
|
340
|
+
symbol.line_end >= edit.line_start):
|
|
341
|
+
affected_symbols.append(symbol)
|
|
342
|
+
|
|
343
|
+
return affected_symbols
|
|
344
|
+
|
|
345
|
+
def _analyze_symbol_impact(self, symbol: Symbol, edit: Edit) -> List[Impact]:
|
|
346
|
+
"""分析符号编辑的影响"""
|
|
347
|
+
impacts = []
|
|
348
|
+
|
|
349
|
+
# 1. 查找所有引用该符号的位置
|
|
350
|
+
references = self.context_manager.find_references(symbol.name, symbol.file_path)
|
|
351
|
+
for ref in references:
|
|
352
|
+
impacts.append(Impact(
|
|
353
|
+
impact_type=ImpactType.REFERENCE,
|
|
354
|
+
target=ref.file_path,
|
|
355
|
+
description=f"引用符号 {symbol.name}",
|
|
356
|
+
line=ref.line,
|
|
357
|
+
severity=RiskLevel.MEDIUM if symbol.kind in ('function', 'class') else RiskLevel.LOW
|
|
358
|
+
))
|
|
359
|
+
|
|
360
|
+
# 2. 查找依赖该符号的其他符号(在同一文件中)
|
|
361
|
+
if symbol.kind in ('function', 'class'):
|
|
362
|
+
dependents = self._find_dependent_symbols(symbol)
|
|
363
|
+
for dep in dependents:
|
|
364
|
+
impacts.append(Impact(
|
|
365
|
+
impact_type=ImpactType.DEPENDENT,
|
|
366
|
+
target=dep.file_path,
|
|
367
|
+
description=f"依赖符号 {symbol.name}",
|
|
368
|
+
line=dep.line_start,
|
|
369
|
+
severity=RiskLevel.MEDIUM
|
|
370
|
+
))
|
|
371
|
+
|
|
372
|
+
return impacts
|
|
373
|
+
|
|
374
|
+
def _find_dependent_symbols(self, symbol: Symbol) -> List[Symbol]:
|
|
375
|
+
"""查找依赖该符号的其他符号"""
|
|
376
|
+
dependents = []
|
|
377
|
+
|
|
378
|
+
# 获取同一文件中的所有符号
|
|
379
|
+
file_symbols = self.context_manager.symbol_table.get_file_symbols(symbol.file_path)
|
|
380
|
+
|
|
381
|
+
# 查找在符号定义之后的符号(可能使用该符号)
|
|
382
|
+
for other_symbol in file_symbols:
|
|
383
|
+
if (other_symbol.line_start > symbol.line_end and
|
|
384
|
+
other_symbol.name != symbol.name):
|
|
385
|
+
# 简单检查:如果符号名出现在其他符号的范围内,可能依赖
|
|
386
|
+
# 这里使用简单的启发式方法
|
|
387
|
+
content = self.context_manager._get_file_content(symbol.file_path)
|
|
388
|
+
if content:
|
|
389
|
+
# 提取其他符号的代码区域
|
|
390
|
+
lines = content.split('\n')
|
|
391
|
+
if other_symbol.line_start <= len(lines) and other_symbol.line_end <= len(lines):
|
|
392
|
+
region = '\n'.join(lines[other_symbol.line_start-1:other_symbol.line_end])
|
|
393
|
+
if symbol.name in region:
|
|
394
|
+
dependents.append(other_symbol)
|
|
395
|
+
|
|
396
|
+
return dependents
|
|
397
|
+
|
|
398
|
+
def _analyze_dependency_chain(self, file_path: str) -> List[Impact]:
|
|
399
|
+
"""分析依赖链,找出所有可能受影响的文件"""
|
|
400
|
+
impacts = []
|
|
401
|
+
|
|
402
|
+
# 获取依赖该文件的所有文件(传递闭包)
|
|
403
|
+
visited = set()
|
|
404
|
+
to_process = [file_path]
|
|
405
|
+
|
|
406
|
+
while to_process:
|
|
407
|
+
current = to_process.pop(0)
|
|
408
|
+
if current in visited:
|
|
409
|
+
continue
|
|
410
|
+
visited.add(current)
|
|
411
|
+
|
|
412
|
+
dependents = self.context_manager.dependency_graph.get_dependents(current)
|
|
413
|
+
for dependent in dependents:
|
|
414
|
+
if dependent not in visited:
|
|
415
|
+
impacts.append(Impact(
|
|
416
|
+
impact_type=ImpactType.DEPENDENCY_CHAIN,
|
|
417
|
+
target=dependent,
|
|
418
|
+
description=f"间接依赖 {os.path.basename(file_path)}",
|
|
419
|
+
severity=RiskLevel.LOW
|
|
420
|
+
))
|
|
421
|
+
to_process.append(dependent)
|
|
422
|
+
|
|
423
|
+
return impacts
|
|
424
|
+
|
|
425
|
+
def _detect_interface_changes(self, file_path: str, edits: List[Edit]) -> List[InterfaceChange]:
|
|
426
|
+
"""检测接口变更(函数签名、类定义等)"""
|
|
427
|
+
changes: List[InterfaceChange] = []
|
|
428
|
+
|
|
429
|
+
# 读取文件内容
|
|
430
|
+
content_before = self._get_file_content_before_edit(file_path, edits)
|
|
431
|
+
content_after = self._get_file_content_after_edit(file_path, edits)
|
|
432
|
+
|
|
433
|
+
if not content_before or not content_after:
|
|
434
|
+
return changes
|
|
435
|
+
|
|
436
|
+
# 解析AST并比较
|
|
437
|
+
try:
|
|
438
|
+
tree_before = ast.parse(content_before, filename=file_path)
|
|
439
|
+
tree_after = ast.parse(content_after, filename=file_path)
|
|
440
|
+
|
|
441
|
+
# 提取函数和类定义
|
|
442
|
+
defs_before = self._extract_definitions(tree_before)
|
|
443
|
+
defs_after = self._extract_definitions(tree_after)
|
|
444
|
+
|
|
445
|
+
# 比较定义
|
|
446
|
+
for name, def_before in defs_before.items():
|
|
447
|
+
if name in defs_after:
|
|
448
|
+
def_after = defs_after[name]
|
|
449
|
+
change = self._compare_definition(name, def_before, def_after, file_path)
|
|
450
|
+
if change:
|
|
451
|
+
changes.append(change)
|
|
452
|
+
else:
|
|
453
|
+
# 定义被删除
|
|
454
|
+
changes.append(InterfaceChange(
|
|
455
|
+
symbol_name=name,
|
|
456
|
+
change_type='removed',
|
|
457
|
+
file_path=file_path,
|
|
458
|
+
line=def_before['line'],
|
|
459
|
+
description=f"符号 {name} 被删除"
|
|
460
|
+
))
|
|
461
|
+
|
|
462
|
+
# 检查新增的定义
|
|
463
|
+
for name, def_after in defs_after.items():
|
|
464
|
+
if name not in defs_before:
|
|
465
|
+
changes.append(InterfaceChange(
|
|
466
|
+
symbol_name=name,
|
|
467
|
+
change_type='added',
|
|
468
|
+
file_path=file_path,
|
|
469
|
+
line=def_after['line'],
|
|
470
|
+
description=f"新增符号 {name}"
|
|
471
|
+
))
|
|
472
|
+
|
|
473
|
+
except SyntaxError:
|
|
474
|
+
# 如果解析失败,跳过接口变更检测
|
|
475
|
+
pass
|
|
476
|
+
|
|
477
|
+
return changes
|
|
478
|
+
|
|
479
|
+
def _extract_definitions(self, tree: ast.AST) -> Dict[str, Dict]:
|
|
480
|
+
"""从AST中提取函数和类定义"""
|
|
481
|
+
definitions = {}
|
|
482
|
+
|
|
483
|
+
for node in ast.walk(tree):
|
|
484
|
+
if isinstance(node, ast.FunctionDef):
|
|
485
|
+
# 提取函数签名
|
|
486
|
+
args = [arg.arg for arg in node.args.args]
|
|
487
|
+
signature = f"{node.name}({', '.join(args)})"
|
|
488
|
+
definitions[node.name] = {
|
|
489
|
+
'type': 'function',
|
|
490
|
+
'line': node.lineno,
|
|
491
|
+
'signature': signature,
|
|
492
|
+
'args': args,
|
|
493
|
+
'node': node,
|
|
494
|
+
}
|
|
495
|
+
elif isinstance(node, ast.ClassDef):
|
|
496
|
+
definitions[node.name] = {
|
|
497
|
+
'type': 'class',
|
|
498
|
+
'line': node.lineno,
|
|
499
|
+
'signature': node.name,
|
|
500
|
+
'node': node,
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return definitions
|
|
504
|
+
|
|
505
|
+
def _compare_definition(self, name: str, def_before: Dict, def_after: Dict, file_path: str) -> Optional[InterfaceChange]:
|
|
506
|
+
"""比较两个定义,检测接口变更"""
|
|
507
|
+
if def_before['type'] != def_after['type']:
|
|
508
|
+
return InterfaceChange(
|
|
509
|
+
symbol_name=name,
|
|
510
|
+
change_type='signature',
|
|
511
|
+
file_path=file_path,
|
|
512
|
+
line=def_after['line'],
|
|
513
|
+
before=def_before['signature'],
|
|
514
|
+
after=def_after['signature'],
|
|
515
|
+
description=f"符号 {name} 的类型从 {def_before['type']} 变为 {def_after['type']}"
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
if def_before['type'] == 'function':
|
|
519
|
+
# 比较函数参数
|
|
520
|
+
args_before = def_before.get('args', [])
|
|
521
|
+
args_after = def_after.get('args', [])
|
|
522
|
+
|
|
523
|
+
if args_before != args_after:
|
|
524
|
+
return InterfaceChange(
|
|
525
|
+
symbol_name=name,
|
|
526
|
+
change_type='signature',
|
|
527
|
+
file_path=file_path,
|
|
528
|
+
line=def_after['line'],
|
|
529
|
+
before=def_before['signature'],
|
|
530
|
+
after=def_after['signature'],
|
|
531
|
+
description=f"函数 {name} 的参数从 ({', '.join(args_before)}) 变为 ({', '.join(args_after)})"
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
return None
|
|
535
|
+
|
|
536
|
+
def _get_file_content_before_edit(self, file_path: str, edits: List[Edit]) -> Optional[str]:
|
|
537
|
+
"""获取编辑前的文件内容"""
|
|
538
|
+
try:
|
|
539
|
+
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
540
|
+
return f.read()
|
|
541
|
+
except Exception:
|
|
542
|
+
return None
|
|
543
|
+
|
|
544
|
+
def _get_file_content_after_edit(self, file_path: str, edits: List[Edit]) -> Optional[str]:
|
|
545
|
+
"""获取编辑后的文件内容(模拟)"""
|
|
546
|
+
# 这里应该根据edits模拟编辑后的内容
|
|
547
|
+
# 为了简化,我们直接读取当前文件内容
|
|
548
|
+
# 在实际使用中,应该根据edits应用变更
|
|
549
|
+
try:
|
|
550
|
+
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
551
|
+
return f.read()
|
|
552
|
+
except Exception:
|
|
553
|
+
return None
|
|
554
|
+
|
|
555
|
+
def _assess_risk(self, impacts: List[Impact], interface_changes: List[InterfaceChange]) -> RiskLevel:
|
|
556
|
+
"""评估编辑风险等级"""
|
|
557
|
+
# 统计高风险因素
|
|
558
|
+
high_risk_count = 0
|
|
559
|
+
medium_risk_count = 0
|
|
560
|
+
|
|
561
|
+
# 接口变更通常是高风险
|
|
562
|
+
if interface_changes:
|
|
563
|
+
high_risk_count += len(interface_changes)
|
|
564
|
+
|
|
565
|
+
# 统计影响数量
|
|
566
|
+
reference_count = sum(1 for i in impacts if i.impact_type == ImpactType.REFERENCE)
|
|
567
|
+
if reference_count > 10:
|
|
568
|
+
high_risk_count += 1
|
|
569
|
+
elif reference_count > 5:
|
|
570
|
+
medium_risk_count += 1
|
|
571
|
+
|
|
572
|
+
# 检查是否有高风险的影响
|
|
573
|
+
for impact in impacts:
|
|
574
|
+
if impact.severity == RiskLevel.HIGH:
|
|
575
|
+
high_risk_count += 1
|
|
576
|
+
elif impact.severity == RiskLevel.MEDIUM:
|
|
577
|
+
medium_risk_count += 1
|
|
578
|
+
|
|
579
|
+
# 评估风险等级
|
|
580
|
+
if high_risk_count > 0 or medium_risk_count > 3:
|
|
581
|
+
return RiskLevel.HIGH
|
|
582
|
+
elif medium_risk_count > 0 or len(impacts) > 5:
|
|
583
|
+
return RiskLevel.MEDIUM
|
|
584
|
+
else:
|
|
585
|
+
return RiskLevel.LOW
|
|
586
|
+
|
|
587
|
+
def _generate_recommendations(
|
|
588
|
+
self,
|
|
589
|
+
impacts: List[Impact],
|
|
590
|
+
interface_changes: List[InterfaceChange],
|
|
591
|
+
affected_files: Set[str],
|
|
592
|
+
test_files: List[str],
|
|
593
|
+
) -> List[str]:
|
|
594
|
+
"""生成修复建议"""
|
|
595
|
+
recommendations = []
|
|
596
|
+
|
|
597
|
+
# 如果有接口变更,建议检查所有调用点
|
|
598
|
+
if interface_changes:
|
|
599
|
+
recommendations.append(
|
|
600
|
+
f"检测到 {len(interface_changes)} 个接口变更,请检查所有调用点并更新相关代码"
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
# 如果有测试文件,建议运行测试
|
|
604
|
+
if test_files:
|
|
605
|
+
recommendations.append(
|
|
606
|
+
f"发现 {len(test_files)} 个相关测试文件,建议运行测试确保功能正常"
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
# 如果影响文件较多,建议增量测试
|
|
610
|
+
if len(affected_files) > 5:
|
|
611
|
+
recommendations.append(
|
|
612
|
+
f"编辑影响了 {len(affected_files)} 个文件,建议进行增量测试"
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# 如果有大量引用,建议代码审查
|
|
616
|
+
reference_count = sum(1 for i in impacts if i.impact_type == ImpactType.REFERENCE)
|
|
617
|
+
if reference_count > 10:
|
|
618
|
+
recommendations.append(
|
|
619
|
+
f"检测到 {reference_count} 个符号引用,建议进行代码审查"
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
if not recommendations:
|
|
623
|
+
recommendations.append("编辑影响范围较小,建议进行基本测试")
|
|
624
|
+
|
|
625
|
+
return recommendations
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def parse_git_diff_to_edits(file_path: str, project_root: str) -> List[Edit]:
|
|
629
|
+
"""从git diff中解析编辑操作
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
file_path: 文件路径
|
|
633
|
+
project_root: 项目根目录
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
List[Edit]: 编辑操作列表
|
|
637
|
+
"""
|
|
638
|
+
edits: List[Edit] = []
|
|
639
|
+
|
|
640
|
+
try:
|
|
641
|
+
# 获取文件的git diff
|
|
642
|
+
abs_path = os.path.abspath(file_path)
|
|
643
|
+
if not os.path.exists(abs_path):
|
|
644
|
+
return edits
|
|
645
|
+
|
|
646
|
+
# 检查是否有git仓库
|
|
647
|
+
try:
|
|
648
|
+
subprocess.run(
|
|
649
|
+
["git", "rev-parse", "--git-dir"],
|
|
650
|
+
cwd=project_root,
|
|
651
|
+
check=True,
|
|
652
|
+
capture_output=True,
|
|
653
|
+
stderr=subprocess.DEVNULL,
|
|
654
|
+
)
|
|
655
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
656
|
+
# 不是git仓库或git不可用,返回空列表
|
|
657
|
+
return edits
|
|
658
|
+
|
|
659
|
+
# 获取HEAD的hash
|
|
660
|
+
try:
|
|
661
|
+
result = subprocess.run(
|
|
662
|
+
["git", "rev-parse", "HEAD"],
|
|
663
|
+
cwd=project_root,
|
|
664
|
+
capture_output=True,
|
|
665
|
+
text=True,
|
|
666
|
+
check=False,
|
|
667
|
+
)
|
|
668
|
+
head_exists = result.returncode == 0 and result.stdout.strip()
|
|
669
|
+
except Exception:
|
|
670
|
+
head_exists = False
|
|
671
|
+
|
|
672
|
+
# 临时添加文件到git索引(如果是新文件)
|
|
673
|
+
subprocess.run(
|
|
674
|
+
["git", "add", "-N", "--", abs_path],
|
|
675
|
+
cwd=project_root,
|
|
676
|
+
check=False,
|
|
677
|
+
stdout=subprocess.DEVNULL,
|
|
678
|
+
stderr=subprocess.DEVNULL,
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
try:
|
|
682
|
+
# 获取diff
|
|
683
|
+
cmd = ["git", "diff"] + (["HEAD"] if head_exists else []) + ["--", abs_path]
|
|
684
|
+
result = subprocess.run(
|
|
685
|
+
cmd,
|
|
686
|
+
cwd=project_root,
|
|
687
|
+
capture_output=True,
|
|
688
|
+
text=True,
|
|
689
|
+
encoding="utf-8",
|
|
690
|
+
errors="replace",
|
|
691
|
+
check=False,
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
if result.returncode != 0 or not result.stdout:
|
|
695
|
+
return edits
|
|
696
|
+
|
|
697
|
+
diff_text = result.stdout
|
|
698
|
+
|
|
699
|
+
# 解析diff文本
|
|
700
|
+
lines = diff_text.split('\n')
|
|
701
|
+
current_hunk_start = None
|
|
702
|
+
current_line_num: Optional[int] = None
|
|
703
|
+
before_lines: List[str] = []
|
|
704
|
+
after_lines: List[str] = []
|
|
705
|
+
in_hunk = False
|
|
706
|
+
|
|
707
|
+
for line in lines:
|
|
708
|
+
# 解析hunk header: @@ -start,count +start,count @@
|
|
709
|
+
if line.startswith('@@'):
|
|
710
|
+
# 保存之前的hunk
|
|
711
|
+
if in_hunk and current_hunk_start is not None:
|
|
712
|
+
if before_lines or after_lines:
|
|
713
|
+
edits.append(Edit(
|
|
714
|
+
file_path=abs_path,
|
|
715
|
+
line_start=current_hunk_start,
|
|
716
|
+
line_end=current_hunk_start + len(after_lines) - 1 if after_lines else current_hunk_start,
|
|
717
|
+
before='\n'.join(before_lines),
|
|
718
|
+
after='\n'.join(after_lines),
|
|
719
|
+
edit_type='modify' if before_lines and after_lines else ('delete' if before_lines else 'add')
|
|
720
|
+
))
|
|
721
|
+
|
|
722
|
+
# 解析新的hunk
|
|
723
|
+
match = re.search(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@', line)
|
|
724
|
+
if match:
|
|
725
|
+
old_start = int(match.group(1))
|
|
726
|
+
new_start = int(match.group(3))
|
|
727
|
+
|
|
728
|
+
current_hunk_start = new_start
|
|
729
|
+
current_line_num = old_start
|
|
730
|
+
before_lines = []
|
|
731
|
+
after_lines = []
|
|
732
|
+
in_hunk = True
|
|
733
|
+
continue
|
|
734
|
+
|
|
735
|
+
if not in_hunk:
|
|
736
|
+
continue
|
|
737
|
+
|
|
738
|
+
# 解析diff行
|
|
739
|
+
if line.startswith('-') and not line.startswith('---'):
|
|
740
|
+
# 删除的行
|
|
741
|
+
before_lines.append(line[1:])
|
|
742
|
+
if current_line_num is not None:
|
|
743
|
+
current_line_num += 1
|
|
744
|
+
elif line.startswith('+') and not line.startswith('+++'):
|
|
745
|
+
# 新增的行
|
|
746
|
+
after_lines.append(line[1:])
|
|
747
|
+
elif line.startswith(' '):
|
|
748
|
+
# 未改变的行
|
|
749
|
+
before_lines.append(line[1:])
|
|
750
|
+
after_lines.append(line[1:])
|
|
751
|
+
if current_line_num is not None:
|
|
752
|
+
current_line_num += 1
|
|
753
|
+
|
|
754
|
+
# 保存最后一个hunk
|
|
755
|
+
if in_hunk and current_hunk_start is not None:
|
|
756
|
+
if before_lines or after_lines:
|
|
757
|
+
edits.append(Edit(
|
|
758
|
+
file_path=abs_path,
|
|
759
|
+
line_start=current_hunk_start,
|
|
760
|
+
line_end=current_hunk_start + len(after_lines) - 1 if after_lines else current_hunk_start,
|
|
761
|
+
before='\n'.join(before_lines),
|
|
762
|
+
after='\n'.join(after_lines),
|
|
763
|
+
edit_type='modify' if before_lines and after_lines else ('delete' if before_lines else 'add')
|
|
764
|
+
))
|
|
765
|
+
|
|
766
|
+
finally:
|
|
767
|
+
# 清理临时添加的文件
|
|
768
|
+
subprocess.run(
|
|
769
|
+
["git", "reset", "--", abs_path],
|
|
770
|
+
cwd=project_root,
|
|
771
|
+
check=False,
|
|
772
|
+
stdout=subprocess.DEVNULL,
|
|
773
|
+
stderr=subprocess.DEVNULL,
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
except Exception:
|
|
777
|
+
# 解析失败时返回空列表
|
|
778
|
+
pass
|
|
779
|
+
|
|
780
|
+
return edits
|
|
781
|
+
|