auto-coder 0.1.316__py3-none-any.whl → 0.1.317__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.
Potentially problematic release.
This version of auto-coder might be problematic. Click here for more details.
- {auto_coder-0.1.316.dist-info → auto_coder-0.1.317.dist-info}/METADATA +1 -1
- {auto_coder-0.1.316.dist-info → auto_coder-0.1.317.dist-info}/RECORD +41 -20
- autocoder/auto_coder_runner.py +1 -2
- autocoder/common/__init__.py +3 -0
- autocoder/common/auto_coder_lang.py +24 -0
- autocoder/common/code_auto_merge_editblock.py +2 -42
- autocoder/common/git_utils.py +2 -2
- autocoder/common/token_cost_caculate.py +103 -42
- autocoder/common/v2/__init__.py +0 -0
- autocoder/common/v2/code_auto_generate.py +199 -0
- autocoder/common/v2/code_auto_generate_diff.py +361 -0
- autocoder/common/v2/code_auto_generate_editblock.py +380 -0
- autocoder/common/v2/code_auto_generate_strict_diff.py +269 -0
- autocoder/common/v2/code_auto_merge.py +211 -0
- autocoder/common/v2/code_auto_merge_diff.py +354 -0
- autocoder/common/v2/code_auto_merge_editblock.py +523 -0
- autocoder/common/v2/code_auto_merge_strict_diff.py +259 -0
- autocoder/common/v2/code_diff_manager.py +266 -0
- autocoder/common/v2/code_editblock_manager.py +275 -0
- autocoder/common/v2/code_manager.py +238 -0
- autocoder/common/v2/code_strict_diff_manager.py +241 -0
- autocoder/dispacher/actions/action.py +16 -0
- autocoder/dispacher/actions/plugins/action_regex_project.py +6 -0
- autocoder/events/event_manager_singleton.py +2 -2
- autocoder/helper/__init__.py +0 -0
- autocoder/helper/project_creator.py +570 -0
- autocoder/linters/linter_factory.py +44 -25
- autocoder/linters/models.py +220 -0
- autocoder/linters/python_linter.py +1 -7
- autocoder/linters/reactjs_linter.py +580 -0
- autocoder/linters/shadow_linter.py +390 -0
- autocoder/linters/vue_linter.py +576 -0
- autocoder/memory/active_context_manager.py +0 -4
- autocoder/memory/active_package.py +12 -12
- autocoder/shadows/__init__.py +0 -0
- autocoder/shadows/shadow_manager.py +235 -0
- autocoder/version.py +1 -1
- {auto_coder-0.1.316.dist-info → auto_coder-0.1.317.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.316.dist-info → auto_coder-0.1.317.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.316.dist-info → auto_coder-0.1.317.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.316.dist-info → auto_coder-0.1.317.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from byzerllm.utils.client import code_utils
|
|
3
|
+
from autocoder.common import AutoCoderArgs, git_utils, SourceCodeList, SourceCode
|
|
4
|
+
from autocoder.common.action_yml_file_manager import ActionYmlFileManager
|
|
5
|
+
from autocoder.common.text import TextSimilarity
|
|
6
|
+
from autocoder.memory.active_context_manager import ActiveContextManager
|
|
7
|
+
import pydantic
|
|
8
|
+
import byzerllm
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.syntax import Syntax
|
|
16
|
+
import json
|
|
17
|
+
from typing import Union, List, Tuple, Dict
|
|
18
|
+
from autocoder.common.types import CodeGenerateResult, MergeCodeWithoutEffect
|
|
19
|
+
from autocoder.common.code_modification_ranker import CodeModificationRanker
|
|
20
|
+
from autocoder.common import files as FileUtils
|
|
21
|
+
from autocoder.common.printer import Printer
|
|
22
|
+
from autocoder.shadows.shadow_manager import ShadowManager
|
|
23
|
+
|
|
24
|
+
class PathAndCode(pydantic.BaseModel):
|
|
25
|
+
path: str
|
|
26
|
+
content: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CodeAutoMerge:
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
llm: byzerllm.ByzerLLM,
|
|
33
|
+
args: AutoCoderArgs,
|
|
34
|
+
):
|
|
35
|
+
self.llm = llm
|
|
36
|
+
self.args = args
|
|
37
|
+
self.printer = Printer()
|
|
38
|
+
|
|
39
|
+
def run_pylint(self, code: str) -> tuple[bool, str]:
|
|
40
|
+
with tempfile.NamedTemporaryFile(
|
|
41
|
+
mode="w", suffix=".py", delete=False
|
|
42
|
+
) as temp_file:
|
|
43
|
+
temp_file.write(code)
|
|
44
|
+
temp_file_path = temp_file.name
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
result = subprocess.run(
|
|
48
|
+
[
|
|
49
|
+
"pylint",
|
|
50
|
+
"--disable=all",
|
|
51
|
+
"--enable=E0001,W0311,W0312",
|
|
52
|
+
temp_file_path,
|
|
53
|
+
],
|
|
54
|
+
capture_output=True,
|
|
55
|
+
text=True,
|
|
56
|
+
check=False,
|
|
57
|
+
)
|
|
58
|
+
os.unlink(temp_file_path)
|
|
59
|
+
if result.returncode != 0:
|
|
60
|
+
error_message = result.stdout.strip() or result.stderr.strip()
|
|
61
|
+
self.printer.print_in_terminal("pylint_check_failed", error_message=error_message)
|
|
62
|
+
return False, error_message
|
|
63
|
+
return True, ""
|
|
64
|
+
except subprocess.CalledProcessError as e:
|
|
65
|
+
error_message = f"Error running pylint: {str(e)}"
|
|
66
|
+
self.printer.print_in_terminal("pylint_error", error_message=error_message)
|
|
67
|
+
os.unlink(temp_file_path)
|
|
68
|
+
return False, error_message
|
|
69
|
+
|
|
70
|
+
def get_source_code_list_from_shadow_files(self, shadow_files: Dict[str, str]) -> SourceCodeList:
|
|
71
|
+
"""
|
|
72
|
+
将影子文件转换为SourceCodeList对象
|
|
73
|
+
|
|
74
|
+
参数:
|
|
75
|
+
shadow_files (Dict[str, str]): 映射 {影子文件路径: 内容}
|
|
76
|
+
|
|
77
|
+
返回:
|
|
78
|
+
SourceCodeList: 包含原始路径和内容的SourceCodeList对象
|
|
79
|
+
"""
|
|
80
|
+
sources = []
|
|
81
|
+
shadow_manager = ShadowManager(self.args.source_dir,event_file_id=self.args.event_file)
|
|
82
|
+
for shadow_path, content in shadow_files.items():
|
|
83
|
+
# 将影子路径转换回原始文件路径
|
|
84
|
+
file_path = shadow_manager.from_shadow_path(shadow_path)
|
|
85
|
+
# 创建SourceCode对象并添加到sources列表
|
|
86
|
+
source = SourceCode(module_name=file_path, source_code=content)
|
|
87
|
+
sources.append(source)
|
|
88
|
+
|
|
89
|
+
return SourceCodeList(sources)
|
|
90
|
+
|
|
91
|
+
def choose_best_choice(self, generate_result: CodeGenerateResult) -> CodeGenerateResult:
|
|
92
|
+
if len(generate_result.contents) == 1:
|
|
93
|
+
return generate_result
|
|
94
|
+
|
|
95
|
+
merge_results = []
|
|
96
|
+
for content,conversations in zip(generate_result.contents,generate_result.conversations):
|
|
97
|
+
merge_result = self._merge_code_without_effect(content)
|
|
98
|
+
merge_results.append(merge_result)
|
|
99
|
+
|
|
100
|
+
# If all merge results are None, return first one
|
|
101
|
+
if all(len(result.failed_blocks) != 0 for result in merge_results):
|
|
102
|
+
self.printer.print_in_terminal("all_merge_results_failed")
|
|
103
|
+
return CodeGenerateResult(contents=[generate_result.contents[0]], conversations=[generate_result.conversations[0]])
|
|
104
|
+
|
|
105
|
+
# If only one merge result is not None, return that one
|
|
106
|
+
not_none_indices = [i for i, result in enumerate(merge_results) if len(result.failed_blocks) == 0]
|
|
107
|
+
if len(not_none_indices) == 1:
|
|
108
|
+
idx = not_none_indices[0]
|
|
109
|
+
self.printer.print_in_terminal("only_one_merge_result_success")
|
|
110
|
+
return CodeGenerateResult(contents=[generate_result.contents[idx]], conversations=[generate_result.conversations[idx]])
|
|
111
|
+
|
|
112
|
+
# 最后,如果有多个,那么根据质量排序再返回
|
|
113
|
+
ranker = CodeModificationRanker(self.llm, self.args)
|
|
114
|
+
ranked_result = ranker.rank_modifications(generate_result,merge_results)
|
|
115
|
+
|
|
116
|
+
## 得到的结果,再做一次合并,第一个通过的返回 , 返回做合并有点重复低效,未来修改。
|
|
117
|
+
for content,conversations in zip(ranked_result.contents,ranked_result.conversations):
|
|
118
|
+
merge_result = self._merge_code_without_effect(content)
|
|
119
|
+
if not merge_result.failed_blocks:
|
|
120
|
+
return CodeGenerateResult(contents=[content], conversations=[conversations])
|
|
121
|
+
|
|
122
|
+
# 最后保底,但实际不会出现
|
|
123
|
+
return CodeGenerateResult(contents=[ranked_result.contents[0]], conversations=[ranked_result.conversations[0]])
|
|
124
|
+
|
|
125
|
+
@byzerllm.prompt(render="jinja2")
|
|
126
|
+
def git_require_msg(self,source_dir:str,error:str)->str:
|
|
127
|
+
'''
|
|
128
|
+
auto_merge only works for git repositories.
|
|
129
|
+
|
|
130
|
+
Try to use git init in the source directory.
|
|
131
|
+
|
|
132
|
+
```shell
|
|
133
|
+
cd {{ source_dir }}
|
|
134
|
+
git init .
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Then try to run auto-coder again.
|
|
138
|
+
Error: {{ error }}
|
|
139
|
+
'''
|
|
140
|
+
|
|
141
|
+
def _merge_code(self, content: str, force_skip_git: bool = False):
|
|
142
|
+
file_content = FileUtils.read_file(self.args.file)
|
|
143
|
+
md5 = hashlib.md5(file_content.encode("utf-8")).hexdigest()
|
|
144
|
+
file_name = os.path.basename(self.args.file)
|
|
145
|
+
|
|
146
|
+
if not force_skip_git and not self.args.skip_commit:
|
|
147
|
+
try:
|
|
148
|
+
git_utils.commit_changes(
|
|
149
|
+
self.args.source_dir, f"auto_coder_pre_{file_name}_{md5}"
|
|
150
|
+
)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
self.printer.print_str_in_terminal(
|
|
153
|
+
self.git_require_msg(source_dir=self.args.source_dir, error=str(e)),
|
|
154
|
+
style="red"
|
|
155
|
+
)
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
merge_result = self._merge_code_without_effect(content)
|
|
159
|
+
if not merge_result.success_blocks:
|
|
160
|
+
self.printer.print_in_terminal("no_changes_to_merge")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
# Apply changes
|
|
164
|
+
for file_path, new_content in merge_result.success_blocks:
|
|
165
|
+
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
|
166
|
+
with open(file_path, "w") as f:
|
|
167
|
+
f.write(new_content)
|
|
168
|
+
|
|
169
|
+
if not force_skip_git and not self.args.skip_commit:
|
|
170
|
+
try:
|
|
171
|
+
commit_result = git_utils.commit_changes(
|
|
172
|
+
self.args.source_dir,
|
|
173
|
+
f"{self.args.query}\nauto_coder_{file_name}",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
action_yml_file_manager = ActionYmlFileManager(self.args.source_dir)
|
|
177
|
+
action_file_name = os.path.basename(self.args.file)
|
|
178
|
+
add_updated_urls = []
|
|
179
|
+
commit_result.changed_files
|
|
180
|
+
for file in commit_result.changed_files:
|
|
181
|
+
add_updated_urls.append(os.path.join(self.args.source_dir, file))
|
|
182
|
+
|
|
183
|
+
self.args.add_updated_urls = add_updated_urls
|
|
184
|
+
update_yaml_success = action_yml_file_manager.update_yaml_field(action_file_name, "add_updated_urls", add_updated_urls)
|
|
185
|
+
if not update_yaml_success:
|
|
186
|
+
self.printer.print_in_terminal("yaml_save_error", style="red", yaml_file=action_file_name)
|
|
187
|
+
|
|
188
|
+
if self.args.enable_active_context:
|
|
189
|
+
active_context_manager = ActiveContextManager(self.llm, self.args.source_dir)
|
|
190
|
+
task_id = active_context_manager.process_changes(self.args)
|
|
191
|
+
self.printer.print_in_terminal("active_context_background_task",
|
|
192
|
+
style="blue",
|
|
193
|
+
task_id=task_id)
|
|
194
|
+
git_utils.print_commit_info(commit_result=commit_result)
|
|
195
|
+
except Exception as e:
|
|
196
|
+
self.printer.print_str_in_terminal(
|
|
197
|
+
self.git_require_msg(source_dir=self.args.source_dir, error=str(e)),
|
|
198
|
+
style="red"
|
|
199
|
+
)
|
|
200
|
+
else:
|
|
201
|
+
self.print_merged_blocks(merge_result.success_blocks)
|
|
202
|
+
|
|
203
|
+
self.printer.print_in_terminal("merge_success",
|
|
204
|
+
num_files=len(merge_result.success_blocks),
|
|
205
|
+
num_changes=len(merge_result.success_blocks),
|
|
206
|
+
total_blocks=len(merge_result.success_blocks) + len(merge_result.failed_blocks))
|
|
207
|
+
|
|
208
|
+
def merge_code(self, generate_result: CodeGenerateResult, force_skip_git: bool = False):
|
|
209
|
+
result = self.choose_best_choice(generate_result)
|
|
210
|
+
self._merge_code(result.contents[0], force_skip_git)
|
|
211
|
+
return result
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from byzerllm.utils.client import code_utils
|
|
3
|
+
from autocoder.common import AutoCoderArgs, git_utils, SourceCodeList, SourceCode
|
|
4
|
+
from autocoder.common.action_yml_file_manager import ActionYmlFileManager
|
|
5
|
+
from autocoder.common.text import TextSimilarity
|
|
6
|
+
from autocoder.memory.active_context_manager import ActiveContextManager
|
|
7
|
+
import pydantic
|
|
8
|
+
import byzerllm
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.syntax import Syntax
|
|
16
|
+
import json
|
|
17
|
+
from typing import Union, List, Tuple, Dict
|
|
18
|
+
from autocoder.common.types import CodeGenerateResult, MergeCodeWithoutEffect
|
|
19
|
+
from autocoder.common.code_modification_ranker import CodeModificationRanker
|
|
20
|
+
from autocoder.common import files as FileUtils
|
|
21
|
+
from autocoder.common.printer import Printer
|
|
22
|
+
from autocoder.shadows.shadow_manager import ShadowManager
|
|
23
|
+
|
|
24
|
+
class PathAndCode(pydantic.BaseModel):
|
|
25
|
+
path: str
|
|
26
|
+
content: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CodeAutoMergeDiff:
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
llm: byzerllm.ByzerLLM,
|
|
33
|
+
args: AutoCoderArgs,
|
|
34
|
+
):
|
|
35
|
+
self.llm = llm
|
|
36
|
+
self.args = args
|
|
37
|
+
self.printer = Printer()
|
|
38
|
+
|
|
39
|
+
def run_pylint(self, code: str) -> tuple[bool, str]:
|
|
40
|
+
with tempfile.NamedTemporaryFile(
|
|
41
|
+
mode="w", suffix=".py", delete=False
|
|
42
|
+
) as temp_file:
|
|
43
|
+
temp_file.write(code)
|
|
44
|
+
temp_file_path = temp_file.name
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
result = subprocess.run(
|
|
48
|
+
[
|
|
49
|
+
"pylint",
|
|
50
|
+
"--disable=all",
|
|
51
|
+
"--enable=E0001,W0311,W0312",
|
|
52
|
+
temp_file_path,
|
|
53
|
+
],
|
|
54
|
+
capture_output=True,
|
|
55
|
+
text=True,
|
|
56
|
+
check=False,
|
|
57
|
+
)
|
|
58
|
+
os.unlink(temp_file_path)
|
|
59
|
+
if result.returncode != 0:
|
|
60
|
+
error_message = result.stdout.strip() or result.stderr.strip()
|
|
61
|
+
self.printer.print_in_terminal("pylint_check_failed", error_message=error_message)
|
|
62
|
+
return False, error_message
|
|
63
|
+
return True, ""
|
|
64
|
+
except subprocess.CalledProcessError as e:
|
|
65
|
+
error_message = f"Error running pylint: {str(e)}"
|
|
66
|
+
self.printer.print_in_terminal("pylint_error", error_message=error_message)
|
|
67
|
+
os.unlink(temp_file_path)
|
|
68
|
+
return False, error_message
|
|
69
|
+
|
|
70
|
+
def get_edits(self, content: str):
|
|
71
|
+
edits = self.parse_whole_text(content)
|
|
72
|
+
result = []
|
|
73
|
+
for edit in edits:
|
|
74
|
+
result.append((edit.path, edit.content))
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
def get_source_code_list_from_shadow_files(self, shadow_files: Dict[str, str]) -> SourceCodeList:
|
|
78
|
+
"""
|
|
79
|
+
将影子文件转换为SourceCodeList对象
|
|
80
|
+
|
|
81
|
+
参数:
|
|
82
|
+
shadow_files (Dict[str, str]): 映射 {影子文件路径: 内容}
|
|
83
|
+
|
|
84
|
+
返回:
|
|
85
|
+
SourceCodeList: 包含原始路径和内容的SourceCodeList对象
|
|
86
|
+
"""
|
|
87
|
+
sources = []
|
|
88
|
+
shadow_manager = ShadowManager(self.args.source_dir,event_file_id=self.args.event_file)
|
|
89
|
+
for shadow_path, content in shadow_files.items():
|
|
90
|
+
# 将影子路径转换回原始文件路径
|
|
91
|
+
file_path = shadow_manager.from_shadow_path(shadow_path)
|
|
92
|
+
# 创建SourceCode对象并添加到sources列表
|
|
93
|
+
source = SourceCode(module_name=file_path, source_code=content)
|
|
94
|
+
sources.append(source)
|
|
95
|
+
|
|
96
|
+
return SourceCodeList(sources)
|
|
97
|
+
|
|
98
|
+
def _merge_code_without_effect(self, content: str) -> MergeCodeWithoutEffect:
|
|
99
|
+
"""Merge code without any side effects like git operations, linting or file writing.
|
|
100
|
+
Returns a tuple of:
|
|
101
|
+
- list of (file_path, new_content) tuples for successfully merged blocks
|
|
102
|
+
- list of (file_path, hunk) tuples for failed to merge blocks"""
|
|
103
|
+
edits = self.get_edits(content)
|
|
104
|
+
file_content_mapping = {}
|
|
105
|
+
failed_blocks = []
|
|
106
|
+
|
|
107
|
+
for path, hunk in edits:
|
|
108
|
+
if not os.path.exists(path):
|
|
109
|
+
file_content_mapping[path] = hunk
|
|
110
|
+
else:
|
|
111
|
+
if path not in file_content_mapping:
|
|
112
|
+
file_content_mapping[path] = FileUtils.read_file(path)
|
|
113
|
+
existing_content = file_content_mapping[path]
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
new_content = self.apply_hunk(existing_content, hunk)
|
|
117
|
+
if new_content:
|
|
118
|
+
file_content_mapping[path] = new_content
|
|
119
|
+
else:
|
|
120
|
+
failed_blocks.append((path, hunk))
|
|
121
|
+
except Exception as e:
|
|
122
|
+
failed_blocks.append((path, hunk))
|
|
123
|
+
|
|
124
|
+
return MergeCodeWithoutEffect(
|
|
125
|
+
success_blocks=[(path, content)
|
|
126
|
+
for path, content in file_content_mapping.items()],
|
|
127
|
+
failed_blocks=failed_blocks
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def apply_hunk(self, content: str, hunk: str) -> str:
|
|
131
|
+
"""Apply a unified diff hunk to content"""
|
|
132
|
+
import difflib
|
|
133
|
+
import re
|
|
134
|
+
|
|
135
|
+
# Split hunk into lines
|
|
136
|
+
lines = hunk.splitlines()
|
|
137
|
+
|
|
138
|
+
# Extract file paths
|
|
139
|
+
old_path = lines[0].replace("--- ", "").strip()
|
|
140
|
+
new_path = lines[1].replace("+++ ", "").strip()
|
|
141
|
+
|
|
142
|
+
# Skip the file paths and the @@ line
|
|
143
|
+
lines = lines[2:]
|
|
144
|
+
|
|
145
|
+
# Find the @@ line
|
|
146
|
+
for i, line in enumerate(lines):
|
|
147
|
+
if line.startswith("@@ "):
|
|
148
|
+
lines = lines[i+1:]
|
|
149
|
+
break
|
|
150
|
+
|
|
151
|
+
# Create a patch
|
|
152
|
+
patch = "\n".join(lines)
|
|
153
|
+
|
|
154
|
+
# Apply the patch
|
|
155
|
+
try:
|
|
156
|
+
result = difflib.unified_diff(content.splitlines(keepends=True),
|
|
157
|
+
patch.splitlines(keepends=True),
|
|
158
|
+
fromfile=old_path,
|
|
159
|
+
tofile=new_path)
|
|
160
|
+
# Convert back to string
|
|
161
|
+
return "".join(result)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
def _merge_code(self, content: str, force_skip_git: bool = False):
|
|
166
|
+
file_content = FileUtils.read_file(self.args.file)
|
|
167
|
+
md5 = hashlib.md5(file_content.encode("utf-8")).hexdigest()
|
|
168
|
+
file_name = os.path.basename(self.args.file)
|
|
169
|
+
|
|
170
|
+
edits = self.get_edits(content)
|
|
171
|
+
changes_to_make = []
|
|
172
|
+
changes_made = False
|
|
173
|
+
unmerged_blocks = []
|
|
174
|
+
merged_blocks = []
|
|
175
|
+
|
|
176
|
+
# First, check if there are any changes to be made
|
|
177
|
+
file_content_mapping = {}
|
|
178
|
+
for path, hunk in edits:
|
|
179
|
+
if not os.path.exists(path):
|
|
180
|
+
changes_to_make.append((path, None, hunk))
|
|
181
|
+
file_content_mapping[path] = hunk
|
|
182
|
+
merged_blocks.append((path, "", hunk, 1))
|
|
183
|
+
changes_made = True
|
|
184
|
+
else:
|
|
185
|
+
if path not in file_content_mapping:
|
|
186
|
+
file_content_mapping[path] = FileUtils.read_file(path)
|
|
187
|
+
existing_content = file_content_mapping[path]
|
|
188
|
+
new_content = self.apply_hunk(existing_content, hunk)
|
|
189
|
+
if new_content:
|
|
190
|
+
changes_to_make.append(
|
|
191
|
+
(path, existing_content, new_content))
|
|
192
|
+
file_content_mapping[path] = new_content
|
|
193
|
+
merged_blocks.append((path, hunk, new_content, 1))
|
|
194
|
+
changes_made = True
|
|
195
|
+
else:
|
|
196
|
+
unmerged_blocks.append(
|
|
197
|
+
(path, hunk, existing_content, 0))
|
|
198
|
+
|
|
199
|
+
if unmerged_blocks:
|
|
200
|
+
if self.args.request_id and not self.args.skip_events:
|
|
201
|
+
# collect unmerged blocks
|
|
202
|
+
event_data = []
|
|
203
|
+
for file_path, head, update, similarity in unmerged_blocks:
|
|
204
|
+
event_data.append(
|
|
205
|
+
{
|
|
206
|
+
"file_path": file_path,
|
|
207
|
+
"head": head,
|
|
208
|
+
"update": update,
|
|
209
|
+
"similarity": similarity,
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
self.printer.print_in_terminal("unmerged_blocks_warning", num_blocks=len(unmerged_blocks))
|
|
215
|
+
self._print_unmerged_blocks(unmerged_blocks)
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
# lint check
|
|
219
|
+
for file_path, new_content in file_content_mapping.items():
|
|
220
|
+
if file_path.endswith(".py"):
|
|
221
|
+
pylint_passed, error_message = self.run_pylint(new_content)
|
|
222
|
+
if not pylint_passed:
|
|
223
|
+
self.printer.print_in_terminal("pylint_file_check_failed",
|
|
224
|
+
file_path=file_path,
|
|
225
|
+
error_message=error_message)
|
|
226
|
+
|
|
227
|
+
if changes_made and not force_skip_git and not self.args.skip_commit:
|
|
228
|
+
try:
|
|
229
|
+
git_utils.commit_changes(
|
|
230
|
+
self.args.source_dir, f"auto_coder_pre_{file_name}_{md5}"
|
|
231
|
+
)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
self.printer.print_str_in_terminal(
|
|
234
|
+
self.git_require_msg(source_dir=self.args.source_dir, error=str(e)),
|
|
235
|
+
style="red"
|
|
236
|
+
)
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
# Now, apply the changes
|
|
240
|
+
for file_path, new_content in file_content_mapping.items():
|
|
241
|
+
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
|
242
|
+
with open(file_path, "w") as f:
|
|
243
|
+
f.write(new_content)
|
|
244
|
+
|
|
245
|
+
if self.args.request_id and not self.args.skip_events:
|
|
246
|
+
# collect modified files
|
|
247
|
+
event_data = []
|
|
248
|
+
for code in merged_blocks:
|
|
249
|
+
file_path, head, update, similarity = code
|
|
250
|
+
event_data.append(
|
|
251
|
+
{
|
|
252
|
+
"file_path": file_path,
|
|
253
|
+
"head": head,
|
|
254
|
+
"update": update,
|
|
255
|
+
"similarity": similarity,
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if changes_made:
|
|
260
|
+
if not force_skip_git and not self.args.skip_commit:
|
|
261
|
+
try:
|
|
262
|
+
commit_result = git_utils.commit_changes(
|
|
263
|
+
self.args.source_dir,
|
|
264
|
+
f"{self.args.query}\nauto_coder_{file_name}",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
action_yml_file_manager = ActionYmlFileManager(self.args.source_dir)
|
|
268
|
+
action_file_name = os.path.basename(self.args.file)
|
|
269
|
+
add_updated_urls = []
|
|
270
|
+
commit_result.changed_files
|
|
271
|
+
for file in commit_result.changed_files:
|
|
272
|
+
add_updated_urls.append(os.path.join(self.args.source_dir, file))
|
|
273
|
+
|
|
274
|
+
self.args.add_updated_urls = add_updated_urls
|
|
275
|
+
update_yaml_success = action_yml_file_manager.update_yaml_field(action_file_name, "add_updated_urls", add_updated_urls)
|
|
276
|
+
if not update_yaml_success:
|
|
277
|
+
self.printer.print_in_terminal("yaml_save_error", style="red", yaml_file=action_file_name)
|
|
278
|
+
|
|
279
|
+
if self.args.enable_active_context:
|
|
280
|
+
active_context_manager = ActiveContextManager(self.llm, self.args.source_dir)
|
|
281
|
+
task_id = active_context_manager.process_changes(self.args)
|
|
282
|
+
self.printer.print_in_terminal("active_context_background_task",
|
|
283
|
+
style="blue",
|
|
284
|
+
task_id=task_id)
|
|
285
|
+
git_utils.print_commit_info(commit_result=commit_result)
|
|
286
|
+
except Exception as e:
|
|
287
|
+
self.printer.print_str_in_terminal(
|
|
288
|
+
self.git_require_msg(source_dir=self.args.source_dir, error=str(e)),
|
|
289
|
+
style="red"
|
|
290
|
+
)
|
|
291
|
+
else:
|
|
292
|
+
self.print_merged_blocks(merged_blocks)
|
|
293
|
+
|
|
294
|
+
self.printer.print_in_terminal("merge_success",
|
|
295
|
+
num_files=len(file_content_mapping.keys()),
|
|
296
|
+
num_changes=len(changes_to_make),
|
|
297
|
+
total_blocks=len(edits))
|
|
298
|
+
|
|
299
|
+
else:
|
|
300
|
+
self.printer.print_in_terminal("no_changes_made")
|
|
301
|
+
|
|
302
|
+
def _print_unmerged_blocks(self, unmerged_blocks: List[tuple]):
|
|
303
|
+
self.printer.print_in_terminal("unmerged_blocks_title", style="bold red")
|
|
304
|
+
for file_path, head, update, similarity in unmerged_blocks:
|
|
305
|
+
self.printer.print_str_in_terminal(
|
|
306
|
+
f"\n{self.printer.get_message_from_key_with_format('unmerged_file_path',file_path=file_path)}",
|
|
307
|
+
style="bold blue"
|
|
308
|
+
)
|
|
309
|
+
self.printer.print_str_in_terminal(
|
|
310
|
+
f"\n{self.printer.get_message_from_key_with_format('unmerged_search_block',similarity=similarity)}",
|
|
311
|
+
style="bold green"
|
|
312
|
+
)
|
|
313
|
+
syntax = Syntax(head, "python", theme="monokai", line_numbers=True)
|
|
314
|
+
self.printer.console.print(Panel(syntax, expand=False))
|
|
315
|
+
self.printer.print_in_terminal("unmerged_replace_block", style="bold yellow")
|
|
316
|
+
syntax = Syntax(update, "python", theme="monokai", line_numbers=True)
|
|
317
|
+
self.printer.console.print(Panel(syntax, expand=False))
|
|
318
|
+
self.printer.print_in_terminal("unmerged_blocks_total", num_blocks=len(unmerged_blocks), style="bold red")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def print_merged_blocks(self, merged_blocks: List[tuple]):
|
|
322
|
+
"""Print search/replace blocks for user review using rich library"""
|
|
323
|
+
from rich.syntax import Syntax
|
|
324
|
+
from rich.panel import Panel
|
|
325
|
+
|
|
326
|
+
# Group blocks by file path
|
|
327
|
+
file_blocks = {}
|
|
328
|
+
for file_path, head, update, similarity in merged_blocks:
|
|
329
|
+
if file_path not in file_blocks:
|
|
330
|
+
file_blocks[file_path] = []
|
|
331
|
+
file_blocks[file_path].append((head, update, similarity))
|
|
332
|
+
|
|
333
|
+
# Generate formatted text for each file
|
|
334
|
+
formatted_text = ""
|
|
335
|
+
for file_path, blocks in file_blocks.items():
|
|
336
|
+
formatted_text += f"##File: {file_path}\n"
|
|
337
|
+
for head, update, similarity in blocks:
|
|
338
|
+
formatted_text += "<<<<<<< SEARCH\n"
|
|
339
|
+
formatted_text += head + "\n"
|
|
340
|
+
formatted_text += "=======\n"
|
|
341
|
+
formatted_text += update + "\n"
|
|
342
|
+
formatted_text += ">>>>>>> REPLACE\n"
|
|
343
|
+
formatted_text += "\n"
|
|
344
|
+
|
|
345
|
+
# Print with rich panel
|
|
346
|
+
self.printer.print_in_terminal("merged_blocks_title", style="bold green")
|
|
347
|
+
self.printer.console.print(
|
|
348
|
+
Panel(
|
|
349
|
+
Syntax(formatted_text, "diff", theme="monokai"),
|
|
350
|
+
title="Merged Changes",
|
|
351
|
+
border_style="green",
|
|
352
|
+
expand=False
|
|
353
|
+
)
|
|
354
|
+
)
|