auto-coder 0.1.330__py3-none-any.whl → 0.1.331__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.

Files changed (40) hide show
  1. {auto_coder-0.1.330.dist-info → auto_coder-0.1.331.dist-info}/METADATA +1 -1
  2. {auto_coder-0.1.330.dist-info → auto_coder-0.1.331.dist-info}/RECORD +40 -40
  3. autocoder/agent/project_reader.py +1 -14
  4. autocoder/auto_coder.py +1 -24
  5. autocoder/command_args.py +1 -6
  6. autocoder/commands/tools.py +0 -13
  7. autocoder/common/__init__.py +6 -3
  8. autocoder/common/auto_coder_lang.py +12 -0
  9. autocoder/common/code_auto_generate.py +6 -160
  10. autocoder/common/code_auto_generate_diff.py +5 -111
  11. autocoder/common/code_auto_generate_editblock.py +5 -95
  12. autocoder/common/code_auto_generate_strict_diff.py +6 -112
  13. autocoder/common/code_auto_merge_editblock.py +1 -45
  14. autocoder/common/command_templates.py +2 -9
  15. autocoder/common/stream_out_type.py +3 -0
  16. autocoder/common/types.py +2 -1
  17. autocoder/common/v2/code_auto_generate.py +6 -4
  18. autocoder/common/v2/code_auto_generate_diff.py +4 -3
  19. autocoder/common/v2/code_auto_generate_editblock.py +9 -4
  20. autocoder/common/v2/code_auto_generate_strict_diff.py +182 -14
  21. autocoder/common/v2/code_auto_merge_diff.py +560 -306
  22. autocoder/common/v2/code_auto_merge_editblock.py +11 -44
  23. autocoder/common/v2/code_auto_merge_strict_diff.py +76 -7
  24. autocoder/common/v2/code_editblock_manager.py +141 -6
  25. autocoder/dispacher/actions/action.py +15 -28
  26. autocoder/dispacher/actions/plugins/action_regex_project.py +5 -9
  27. autocoder/helper/project_creator.py +0 -1
  28. autocoder/index/entry.py +0 -43
  29. autocoder/index/filter/normal_filter.py +0 -16
  30. autocoder/lang.py +2 -4
  31. autocoder/pyproject/__init__.py +2 -19
  32. autocoder/rag/cache/simple_cache.py +31 -6
  33. autocoder/regexproject/__init__.py +4 -22
  34. autocoder/suffixproject/__init__.py +6 -24
  35. autocoder/tsproject/__init__.py +5 -22
  36. autocoder/version.py +1 -1
  37. {auto_coder-0.1.330.dist-info → auto_coder-0.1.331.dist-info}/LICENSE +0 -0
  38. {auto_coder-0.1.330.dist-info → auto_coder-0.1.331.dist-info}/WHEEL +0 -0
  39. {auto_coder-0.1.330.dist-info → auto_coder-0.1.331.dist-info}/entry_points.txt +0 -0
  40. {auto_coder-0.1.330.dist-info → auto_coder-0.1.331.dist-info}/top_level.txt +0 -0
@@ -1,354 +1,608 @@
1
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
2
+ import difflib
3
+ from autocoder.common import AutoCoderArgs,git_utils
4
+ from typing import List,Union,Tuple
7
5
  import pydantic
8
6
  import byzerllm
9
-
7
+ from autocoder.common.action_yml_file_manager import ActionYmlFileManager
8
+ from autocoder.common.printer import Printer
10
9
  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
10
+ from pathlib import Path
11
+ from itertools import groupby
12
+ from autocoder.memory.active_context_manager import ActiveContextManager
13
+ from autocoder.common.search_replace import (
14
+ SearchTextNotUnique,
15
+ all_preprocs,
16
+ diff_lines,
17
+ flexible_search_and_replace,
18
+ search_and_replace,
19
+ )
18
20
  from autocoder.common.types import CodeGenerateResult, MergeCodeWithoutEffect
19
21
  from autocoder.common.code_modification_ranker import CodeModificationRanker
20
22
  from autocoder.common import files as FileUtils
21
- from autocoder.common.printer import Printer
22
- from autocoder.shadows.shadow_manager import ShadowManager
23
23
 
24
24
  class PathAndCode(pydantic.BaseModel):
25
25
  path: str
26
26
  content: str
27
27
 
28
+ def safe_abs_path(res):
29
+ "Gives an abs path, which safely returns a full (not 8.3) windows path"
30
+ res = Path(res).resolve()
31
+ return str(res)
32
+
33
+ def do_replace(fname, content, hunk):
34
+ fname = Path(fname)
35
+
36
+ before_text, after_text = hunk_to_before_after(hunk)
37
+
38
+ # does it want to make a new file?
39
+ if not fname.exists() and not before_text.strip():
40
+ fname.touch()
41
+ content = ""
42
+
43
+ if content is None:
44
+ return
45
+
46
+ # TODO: handle inserting into new file
47
+ if not before_text.strip():
48
+ # append to existing file, or start a new file
49
+ new_content = content + after_text
50
+ return new_content
51
+
52
+ new_content = None
53
+
54
+ new_content = apply_hunk(content, hunk)
55
+ if new_content:
56
+ return new_content
57
+
58
+
59
+ def collapse_repeats(s):
60
+ return "".join(k for k, g in groupby(s))
61
+
62
+
63
+ def apply_hunk(content, hunk):
64
+ before_text, after_text = hunk_to_before_after(hunk)
65
+
66
+ res = directly_apply_hunk(content, hunk)
67
+ if res:
68
+ return res
69
+
70
+ hunk = make_new_lines_explicit(content, hunk)
71
+
72
+ # just consider space vs not-space
73
+ ops = "".join([line[0] for line in hunk])
74
+ ops = ops.replace("-", "x")
75
+ ops = ops.replace("+", "x")
76
+ ops = ops.replace("\n", " ")
77
+
78
+ cur_op = " "
79
+ section = []
80
+ sections = []
81
+
82
+ for i in range(len(ops)):
83
+ op = ops[i]
84
+ if op != cur_op:
85
+ sections.append(section)
86
+ section = []
87
+ cur_op = op
88
+ section.append(hunk[i])
89
+
90
+ sections.append(section)
91
+ if cur_op != " ":
92
+ sections.append([])
93
+
94
+ all_done = True
95
+ for i in range(2, len(sections), 2):
96
+ preceding_context = sections[i - 2]
97
+ changes = sections[i - 1]
98
+ following_context = sections[i]
99
+
100
+ res = apply_partial_hunk(content, preceding_context, changes, following_context)
101
+ if res:
102
+ content = res
103
+ else:
104
+ all_done = False
105
+ # FAILED!
106
+ # this_hunk = preceding_context + changes + following_context
107
+ break
108
+
109
+ if all_done:
110
+ return content
111
+
112
+
113
+ def flexi_just_search_and_replace(texts):
114
+ strategies = [
115
+ (search_and_replace, all_preprocs),
116
+ ]
117
+
118
+ return flexible_search_and_replace(texts, strategies)
119
+
120
+
121
+ def make_new_lines_explicit(content, hunk):
122
+ before, after = hunk_to_before_after(hunk)
123
+
124
+ diff = diff_lines(before, content)
125
+
126
+ back_diff = []
127
+ for line in diff:
128
+ if line[0] == "+":
129
+ continue
130
+ # if line[0] == "-":
131
+ # line = "+" + line[1:]
132
+
133
+ back_diff.append(line)
134
+
135
+ new_before = directly_apply_hunk(before, back_diff)
136
+ if not new_before:
137
+ return hunk
138
+
139
+ if len(new_before.strip()) < 10:
140
+ return hunk
141
+
142
+ before = before.splitlines(keepends=True)
143
+ new_before = new_before.splitlines(keepends=True)
144
+ after = after.splitlines(keepends=True)
145
+
146
+ if len(new_before) < len(before) * 0.66:
147
+ return hunk
148
+
149
+ new_hunk = difflib.unified_diff(new_before, after, n=max(len(new_before), len(after)))
150
+ new_hunk = list(new_hunk)[3:]
151
+
152
+ return new_hunk
153
+
154
+
155
+ def cleanup_pure_whitespace_lines(lines):
156
+ res = [
157
+ line if line.strip() else line[-(len(line) - len(line.rstrip("\r\n")))] for line in lines
158
+ ]
159
+ return res
160
+
161
+
162
+ def normalize_hunk(hunk):
163
+ before, after = hunk_to_before_after(hunk, lines=True)
164
+
165
+ before = cleanup_pure_whitespace_lines(before)
166
+ after = cleanup_pure_whitespace_lines(after)
167
+
168
+ diff = difflib.unified_diff(before, after, n=max(len(before), len(after)))
169
+ diff = list(diff)[3:]
170
+ return diff
171
+
172
+
173
+ def directly_apply_hunk(content, hunk):
174
+ before, after = hunk_to_before_after(hunk)
175
+
176
+ if not before:
177
+ return
178
+
179
+ before_lines, _ = hunk_to_before_after(hunk, lines=True)
180
+ before_lines = "".join([line.strip() for line in before_lines])
181
+
182
+ # Refuse to do a repeated search and replace on a tiny bit of non-whitespace context
183
+ if len(before_lines) < 10 and content.count(before) > 1:
184
+ return
185
+
186
+ try:
187
+ new_content = flexi_just_search_and_replace([before, after, content])
188
+ except SearchTextNotUnique:
189
+ new_content = None
190
+
191
+ return new_content
192
+
193
+
194
+ def apply_partial_hunk(content, preceding_context, changes, following_context):
195
+ len_prec = len(preceding_context)
196
+ len_foll = len(following_context)
197
+
198
+ use_all = len_prec + len_foll
199
+
200
+ # if there is a - in the hunk, we can go all the way to `use=0`
201
+ for drop in range(use_all + 1):
202
+ use = use_all - drop
203
+
204
+ for use_prec in range(len_prec, -1, -1):
205
+ if use_prec > use:
206
+ continue
207
+
208
+ use_foll = use - use_prec
209
+ if use_foll > len_foll:
210
+ continue
211
+
212
+ if use_prec:
213
+ this_prec = preceding_context[-use_prec:]
214
+ else:
215
+ this_prec = []
216
+
217
+ this_foll = following_context[:use_foll]
218
+
219
+ res = directly_apply_hunk(content, this_prec + changes + this_foll)
220
+ if res:
221
+ return res
222
+
223
+
224
+ def find_diffs(content):
225
+ # We can always fence with triple-quotes, because all the udiff content
226
+ # is prefixed with +/-/space.
227
+
228
+ if not content.endswith("\n"):
229
+ content = content + "\n"
230
+
231
+ lines = content.splitlines(keepends=True)
232
+ line_num = 0
233
+ edits = []
234
+ while line_num < len(lines):
235
+ while line_num < len(lines):
236
+ line = lines[line_num]
237
+ if line.startswith("```diff"):
238
+ line_num, these_edits = process_fenced_block(lines, line_num + 1)
239
+ edits += these_edits
240
+ break
241
+ line_num += 1
242
+
243
+ # For now, just take 1!
244
+ # edits = edits[:1]
245
+
246
+ return edits
247
+
248
+
249
+ def process_fenced_block(lines, start_line_num):
250
+ for line_num in range(start_line_num, len(lines)):
251
+ line = lines[line_num]
252
+ if line.startswith("```"):
253
+ break
254
+
255
+ block = lines[start_line_num:line_num]
256
+ block.append("@@ @@")
257
+
258
+ if block[0].startswith("--- ") and block[1].startswith("+++ "):
259
+ # Extract the file path, considering that it might contain spaces
260
+ fname = block[1][4:].strip()
261
+ block = block[2:]
262
+ else:
263
+ fname = None
264
+
265
+ edits = []
266
+
267
+ keeper = False
268
+ hunk = []
269
+ op = " "
270
+ for line in block:
271
+ hunk.append(line)
272
+ if len(line) < 2:
273
+ continue
274
+
275
+ if line.startswith("+++ ") and hunk[-2].startswith("--- "):
276
+ if hunk[-3] == "\n":
277
+ hunk = hunk[:-3]
278
+ else:
279
+ hunk = hunk[:-2]
280
+
281
+ edits.append((fname, hunk))
282
+ hunk = []
283
+ keeper = False
284
+
285
+ fname = line[4:].strip()
286
+ continue
287
+
288
+ op = line[0]
289
+ if op in "-+":
290
+ keeper = True
291
+ continue
292
+ if op != "@":
293
+ continue
294
+ if not keeper:
295
+ hunk = []
296
+ continue
297
+
298
+ hunk = hunk[:-1]
299
+ edits.append((fname, hunk))
300
+ hunk = []
301
+ keeper = False
302
+
303
+ return line_num + 1, edits
304
+
305
+
306
+ def hunk_to_before_after(hunk, lines=False):
307
+ before = []
308
+ after = []
309
+ op = " "
310
+ for line in hunk:
311
+ if len(line) < 2:
312
+ op = " "
313
+ line = line
314
+ else:
315
+ op = line[0]
316
+ line = line[1:]
317
+
318
+ if op == " ":
319
+ before.append(line)
320
+ after.append(line)
321
+ elif op == "-":
322
+ before.append(line)
323
+ elif op == "+":
324
+ after.append(line)
325
+
326
+ if lines:
327
+ return before, after
328
+
329
+ before = "".join(before)
330
+ after = "".join(after)
331
+
332
+ return before, after
333
+
334
+ no_match_error = """UnifiedDiffNoMatch: hunk failed to apply!
335
+
336
+ {path} does not contain lines that match the diff you provided!
337
+ Try again.
338
+ DO NOT skip blank lines, comments, docstrings, etc!
339
+ The diff needs to apply cleanly to the lines in {path}!
340
+
341
+ {path} does not contain these {num_lines} exact lines in a row:
342
+ ```
343
+ {original}```
344
+ """
345
+
346
+
347
+ not_unique_error = """UnifiedDiffNotUnique: hunk failed to apply!
348
+
349
+ {path} contains multiple sets of lines that match the diff you provided!
350
+ Try again.
351
+ Use additional ` ` lines to provide context that uniquely indicates which code needs to be changed.
352
+ The diff needs to apply to a unique set of lines in {path}!
353
+
354
+ {path} contains multiple copies of these {num_lines} lines:
355
+ ```
356
+ {original}```
357
+ """
358
+
359
+ other_hunks_applied = (
360
+ "Note: some hunks did apply successfully. See the updated source code shown above.\n\n"
361
+ )
28
362
 
29
363
  class CodeAutoMergeDiff:
30
- def __init__(
31
- self,
32
- llm: byzerllm.ByzerLLM,
33
- args: AutoCoderArgs,
34
- ):
364
+ def __init__(self, llm:byzerllm.ByzerLLM,args:AutoCoderArgs):
35
365
  self.llm = llm
36
366
  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)
367
+ self.printer = Printer()
97
368
 
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 = []
369
+ def get_edits(self,content:str):
370
+ # might raise ValueError for malformed ORIG/UPD blocks
371
+ raw_edits = list(find_diffs(content))
106
372
 
107
- for path, hunk in edits:
108
- if not os.path.exists(path):
109
- file_content_mapping[path] = hunk
373
+ last_path = None
374
+ edits = []
375
+ for path, hunk in raw_edits:
376
+ if path:
377
+ last_path = path
110
378
  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))
379
+ path = last_path
380
+ edits.append((path, hunk))
123
381
 
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()
382
+ return edits
383
+
384
+ def merge_code(self, generate_result: CodeGenerateResult, force_skip_git: bool = False):
385
+ result = self.choose_best_choice(generate_result)
386
+ self._merge_code(result.contents[0], force_skip_git)
387
+ return result
388
+
389
+ def choose_best_choice(self, generate_result: CodeGenerateResult) -> CodeGenerateResult:
390
+ if len(generate_result.contents) == 1:
391
+ return generate_result
141
392
 
142
- # Skip the file paths and the @@ line
143
- lines = lines[2:]
393
+ merge_results = []
394
+ for content,conversations in zip(generate_result.contents,generate_result.conversations):
395
+ merge_result = self._merge_code_without_effect(content)
396
+ merge_results.append(merge_result)
397
+
398
+ # If all merge results are None, return first one
399
+ if all(len(result.failed_blocks) != 0 for result in merge_results):
400
+ self.printer.print_in_terminal("all_merge_results_failed")
401
+ return CodeGenerateResult(contents=[generate_result.contents[0]], conversations=[generate_result.conversations[0]])
144
402
 
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)
403
+ # If only one merge result is not None, return that one
404
+ not_none_indices = [i for i, result in enumerate(merge_results) if len(result.failed_blocks) == 0]
405
+ if len(not_none_indices) == 1:
406
+ idx = not_none_indices[0]
407
+ self.printer.print_in_terminal("only_one_merge_result_success")
408
+ return CodeGenerateResult(contents=[generate_result.contents[idx]], conversations=[generate_result.conversations[idx]])
409
+
410
+ # 最后,如果有多个,那么根据质量排序再返回
411
+ ranker = CodeModificationRanker(self.llm, self.args)
412
+ ranked_result = ranker.rank_modifications(generate_result,merge_results)
413
+
414
+ ## 得到的结果,再做一次合并,第一个通过的返回 , 返回做合并有点重复低效,未来修改。
415
+ for content,conversations in zip(ranked_result.contents,ranked_result.conversations):
416
+ merge_result = self._merge_code_without_effect(content)
417
+ if not merge_result.failed_blocks:
418
+ return CodeGenerateResult(contents=[content], conversations=[conversations])
419
+
420
+ # 最后保底,但实际不会出现
421
+ return CodeGenerateResult(contents=[ranked_result.contents[0]], conversations=[ranked_result.conversations[0]])
422
+
423
+ @byzerllm.prompt(render="jinja2")
424
+ def git_require_msg(self,source_dir:str,error:str)->str:
425
+ '''
426
+ auto_merge only works for git repositories.
427
+
428
+ Try to use git init in the source directory.
153
429
 
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)
430
+ ```shell
431
+ cd {{ source_dir }}
432
+ git init .
433
+ ```
169
434
 
170
- edits = self.get_edits(content)
171
- changes_to_make = []
172
- changes_made = False
173
- unmerged_blocks = []
174
- merged_blocks = []
435
+ Then try to run auto-coder again.
436
+ Error: {{ error }}
437
+ '''
438
+
439
+ def abs_root_path(self, path):
440
+ if path.startswith(self.args.source_dir):
441
+ return safe_abs_path(Path(path))
442
+ res = Path(self.args.source_dir) / path
443
+ return safe_abs_path(res)
175
444
 
176
- # First, check if there are any changes to be made
177
- file_content_mapping = {}
445
+ def apply_edits(self, edits):
446
+ seen = set()
447
+ uniq = []
178
448
  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
449
+ hunk = normalize_hunk(hunk)
450
+ if not hunk:
451
+ continue
452
+
453
+ this = [path + "\n"] + hunk
454
+ this = "".join(this)
455
+
456
+ if this in seen:
457
+ continue
458
+ seen.add(this)
459
+
460
+ uniq.append((path, hunk))
461
+
462
+ errors = []
463
+ for path, hunk in uniq:
464
+ full_path = self.abs_root_path(path)
465
+
466
+ if not os.path.exists(full_path):
467
+ with open(full_path, "w") as f:
468
+ f.write("")
213
469
 
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:
470
+ content = FileUtils.read_file(full_path)
471
+
472
+ original, _ = hunk_to_before_after(hunk)
473
+
228
474
  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
- }
475
+ content = do_replace(full_path, content, hunk)
476
+ except SearchTextNotUnique:
477
+ errors.append(
478
+ not_unique_error.format(
479
+ path=path, original=original, num_lines=len(original.splitlines())
480
+ )
257
481
  )
482
+ continue
258
483
 
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}",
484
+ if not content:
485
+ errors.append(
486
+ no_match_error.format(
487
+ path=path, original=original, num_lines=len(original.splitlines())
265
488
  )
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")
489
+ )
490
+ continue
491
+
492
+ # SUCCESS!
493
+ with open(full_path, "w") as f:
494
+ f.write(content)
319
495
 
496
+ if errors:
497
+ errors = "\n\n".join(errors)
498
+ if len(errors) < len(uniq):
499
+ errors += other_hunks_applied
500
+ raise ValueError(errors)
320
501
 
321
- def print_merged_blocks(self, merged_blocks: List[tuple]):
322
- """Print search/replace blocks for user review using rich library"""
502
+ def _merge_code_without_effect(self, content: str) -> MergeCodeWithoutEffect:
503
+ """Merge code without any side effects like git operations or file writing.
504
+ Returns a tuple of:
505
+ - list of (file_path, new_content) tuples for successfully merged blocks
506
+ - list of (file_path, hunk) tuples for failed to merge blocks"""
507
+ edits = self.get_edits(content)
508
+ file_content_mapping = {}
509
+ failed_blocks = []
510
+
511
+ for path, hunk in edits:
512
+ full_path = self.abs_root_path(path)
513
+ if not os.path.exists(full_path):
514
+ _, after = hunk_to_before_after(hunk)
515
+ file_content_mapping[full_path] = after
516
+ continue
517
+
518
+ if full_path not in file_content_mapping:
519
+ file_content_mapping[full_path] = FileUtils.read_file(full_path)
520
+
521
+ content = file_content_mapping[full_path]
522
+ new_content = do_replace(full_path, content, hunk)
523
+ if new_content:
524
+ file_content_mapping[full_path] = new_content
525
+ else:
526
+ failed_blocks.append((full_path, "\n".join(hunk)))
527
+
528
+ return MergeCodeWithoutEffect(
529
+ success_blocks=[(path, content) for path, content in file_content_mapping.items()],
530
+ failed_blocks=failed_blocks
531
+ )
532
+
533
+ def print_edits(self, edits: List[Tuple[str, List[str]]]):
534
+ """Print diffs for user review using rich library"""
323
535
  from rich.syntax import Syntax
324
536
  from rich.panel import Panel
325
537
 
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))
538
+ # Group edits by file path
539
+ file_edits = {}
540
+ for path, hunk in edits:
541
+ if path not in file_edits:
542
+ file_edits[path] = []
543
+ file_edits[path].append(hunk)
332
544
 
333
545
  # Generate formatted text for each file
334
546
  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"
547
+ for path, hunks in file_edits.items():
548
+ formatted_text += f"##File: {path}\n"
549
+ for hunk in hunks:
550
+ formatted_text += "".join(hunk)
343
551
  formatted_text += "\n"
344
552
 
345
553
  # Print with rich panel
346
- self.printer.print_in_terminal("merged_blocks_title", style="bold green")
554
+ self.printer.print_in_terminal("edits_title", style="bold green")
347
555
  self.printer.console.print(
348
556
  Panel(
349
557
  Syntax(formatted_text, "diff", theme="monokai"),
350
- title="Merged Changes",
558
+ title="Edits",
351
559
  border_style="green",
352
560
  expand=False
353
561
  )
354
- )
562
+ )
563
+
564
+ def _merge_code(self, content: str,force_skip_git:bool=False):
565
+ total = 0
566
+
567
+ file_content = FileUtils.read_file(self.args.file)
568
+ md5 = hashlib.md5(file_content.encode('utf-8')).hexdigest()
569
+ # get the file name
570
+ file_name = os.path.basename(self.args.file)
571
+
572
+ if not force_skip_git and not self.args.skip_commit:
573
+ try:
574
+ git_utils.commit_changes(self.args.source_dir, f"auto_coder_pre_{file_name}_{md5}")
575
+ except Exception as e:
576
+ self.printer.print_in_terminal("git_init_required", style="red", source_dir=self.args.source_dir, error=str(e))
577
+ return
578
+
579
+ edits = self.get_edits(content)
580
+ self.apply_edits(edits)
581
+
582
+ self.printer.print_in_terminal("files_merged_total", total=total)
583
+ if not force_skip_git and not self.args.skip_commit:
584
+ commit_result = git_utils.commit_changes(self.args.source_dir, f"{self.args.query}\nauto_coder_{file_name}")
585
+
586
+ action_yml_file_manager = ActionYmlFileManager(self.args.source_dir)
587
+ action_file_name = os.path.basename(self.args.file)
588
+ add_updated_urls = []
589
+ commit_result.changed_files
590
+ for file in commit_result.changed_files:
591
+ add_updated_urls.append(os.path.join(self.args.source_dir, file))
592
+
593
+ self.args.add_updated_urls = add_updated_urls
594
+ update_yaml_success = action_yml_file_manager.update_yaml_field(action_file_name, "add_updated_urls", add_updated_urls)
595
+ if not update_yaml_success:
596
+ self.printer.print_in_terminal("yaml_save_error", style="red", yaml_file=action_file_name)
597
+
598
+ if self.args.enable_active_context:
599
+ active_context_manager = ActiveContextManager(self.llm, self.args.source_dir)
600
+ task_id = active_context_manager.process_changes(self.args)
601
+ self.printer.print_in_terminal("active_context_background_task",
602
+ style="blue",
603
+ task_id=task_id)
604
+
605
+ git_utils.print_commit_info(commit_result=commit_result)
606
+ else:
607
+ # Print edits for review
608
+ self.print_edits(edits)