uni-diff-patch 0.0.1__tar.gz → 0.0.3__tar.gz
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.
- {uni_diff_patch-0.0.1 → uni_diff_patch-0.0.3}/PKG-INFO +1 -1
- {uni_diff_patch-0.0.1 → uni_diff_patch-0.0.3}/pyproject.toml +1 -1
- {uni_diff_patch-0.0.1 → uni_diff_patch-0.0.3}/src/uni_diff_patch/cli.py +7 -0
- {uni_diff_patch-0.0.1 → uni_diff_patch-0.0.3}/src/uni_diff_patch/core/diff_engine.py +21 -6
- {uni_diff_patch-0.0.1 → uni_diff_patch-0.0.3}/src/uni_diff_patch/core/validator.py +47 -27
- {uni_diff_patch-0.0.1 → uni_diff_patch-0.0.3}/src/uni_diff_patch/server.py +4 -0
- {uni_diff_patch-0.0.1 → uni_diff_patch-0.0.3}/README.md +0 -0
- {uni_diff_patch-0.0.1 → uni_diff_patch-0.0.3}/src/uni_diff_patch/__init__.py +0 -0
- {uni_diff_patch-0.0.1 → uni_diff_patch-0.0.3}/src/uni_diff_patch/core/__init__.py +0 -0
- {uni_diff_patch-0.0.1 → uni_diff_patch-0.0.3}/src/uni_diff_patch/core/search_replace.py +0 -0
- {uni_diff_patch-0.0.1 → uni_diff_patch-0.0.3}/src/uni_diff_patch/models.py +0 -0
- {uni_diff_patch-0.0.1 → uni_diff_patch-0.0.3}/src/uni_diff_patch/py.typed +0 -0
|
@@ -46,11 +46,17 @@ def main() -> None:
|
|
|
46
46
|
is_flag=True,
|
|
47
47
|
help="Overwrite existing output file.",
|
|
48
48
|
)
|
|
49
|
+
@click.option(
|
|
50
|
+
"--no-append",
|
|
51
|
+
is_flag=True,
|
|
52
|
+
help="Disable append mode (default appends to existing file).",
|
|
53
|
+
)
|
|
49
54
|
def generate(
|
|
50
55
|
spec_source: str,
|
|
51
56
|
output_override: str | None,
|
|
52
57
|
work_dir: str | None,
|
|
53
58
|
overwrite: bool,
|
|
59
|
+
no_append: bool,
|
|
54
60
|
) -> None:
|
|
55
61
|
"""Generate a unified diff patch from a JSON spec.
|
|
56
62
|
|
|
@@ -91,6 +97,7 @@ def generate(
|
|
|
91
97
|
output_path=final_output,
|
|
92
98
|
work_dir=work_dir,
|
|
93
99
|
overwrite=overwrite,
|
|
100
|
+
append=not no_append,
|
|
94
101
|
)
|
|
95
102
|
except (DiffGenerationError, SearchReplaceError) as e:
|
|
96
103
|
raise click.ClickException(str(e))
|
|
@@ -263,12 +263,18 @@ def _generate_single_diff(
|
|
|
263
263
|
f"{change.file_path}"
|
|
264
264
|
)
|
|
265
265
|
patch_path = _make_patch_path(change.file_path, work_dir)
|
|
266
|
-
|
|
266
|
+
diff = _create_unified_diff(patch_path, "", change.new_content, 3)
|
|
267
|
+
# 替换 --- 行为 /dev/null,标准的新建文件格式
|
|
268
|
+
diff = diff.replace(f"--- a/{patch_path}", "--- /dev/null", 1)
|
|
269
|
+
return diff
|
|
267
270
|
|
|
268
271
|
case DeleteFileChange():
|
|
269
272
|
old = _read_file(change.file_path, change.encoding)
|
|
270
273
|
patch_path = _make_patch_path(change.file_path, work_dir)
|
|
271
|
-
|
|
274
|
+
diff = _create_unified_diff(patch_path, old, "", 3)
|
|
275
|
+
# 替换 +++ 行为 /dev/null,让 git apply 真正删除文件
|
|
276
|
+
diff = diff.replace(f"+++ b/{patch_path}", "+++ /dev/null", 1)
|
|
277
|
+
return diff
|
|
272
278
|
|
|
273
279
|
case TextPairChange():
|
|
274
280
|
if change.old_text == change.new_text:
|
|
@@ -315,6 +321,7 @@ def generate_patch(
|
|
|
315
321
|
work_dir: str | None = None,
|
|
316
322
|
self_validate: bool = True,
|
|
317
323
|
overwrite: bool = False,
|
|
324
|
+
append: bool = True,
|
|
318
325
|
) -> GenerateResult:
|
|
319
326
|
"""Generate a unified diff patch from one or more file changes.
|
|
320
327
|
|
|
@@ -325,6 +332,8 @@ def generate_patch(
|
|
|
325
332
|
Defaults to current working directory.
|
|
326
333
|
self_validate: If True, validate the generated patch with mpatch.parse_auto.
|
|
327
334
|
overwrite: If True, overwrite existing output files. Default False.
|
|
335
|
+
append: If True, append to existing output file instead of overwriting.
|
|
336
|
+
Useful for incremental patch generation (e.g. Codex workflow).
|
|
328
337
|
|
|
329
338
|
Returns:
|
|
330
339
|
GenerateResult with the patch string and metadata.
|
|
@@ -391,12 +400,18 @@ def generate_patch(
|
|
|
391
400
|
|
|
392
401
|
if output_path is not None:
|
|
393
402
|
out = Path(output_path)
|
|
394
|
-
|
|
403
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
404
|
+
if append and out.exists():
|
|
405
|
+
# 追加模式:在已有 diff 后追加新内容
|
|
406
|
+
existing = out.read_text(encoding="utf-8")
|
|
407
|
+
separator = "\n" if existing and not existing.endswith("\n") else ""
|
|
408
|
+
out.write_text(existing + separator + combined, encoding="utf-8")
|
|
409
|
+
elif out.exists() and not overwrite:
|
|
395
410
|
raise DiffGenerationError(
|
|
396
411
|
f"Output file already exists: {output_path}. "
|
|
397
|
-
f"Use overwrite=True to replace."
|
|
412
|
+
f"Use overwrite=True to replace, or append=True to append."
|
|
398
413
|
)
|
|
399
|
-
|
|
400
|
-
|
|
414
|
+
else:
|
|
415
|
+
out.write_text(combined, encoding="utf-8")
|
|
401
416
|
|
|
402
417
|
return result
|
|
@@ -44,39 +44,57 @@ def _parse_header_only_diffs(content: str) -> list[FileInfo]:
|
|
|
44
44
|
"""Parse git extended header-only diffs (rename, chmod without content changes).
|
|
45
45
|
|
|
46
46
|
mpatch.parse_auto cannot parse these since they have no hunk body.
|
|
47
|
-
We detect them by
|
|
48
|
-
|
|
47
|
+
We detect them by scanning for 'diff --git' lines and checking the
|
|
48
|
+
header section (between 'diff --git' and the first '---' or next
|
|
49
|
+
'diff --git') for rename/mode patterns.
|
|
49
50
|
|
|
50
51
|
Returns:
|
|
51
52
|
List of FileInfo for recognized header-only diffs, empty if none found.
|
|
52
53
|
"""
|
|
53
54
|
files: list[FileInfo] = []
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if not
|
|
59
|
-
|
|
60
|
-
# 如果有 @@ hunk,mpatch 应该已经处理了,这里只处理无 hunk 的
|
|
61
|
-
if "@@" in block:
|
|
55
|
+
lines = content.split("\n")
|
|
56
|
+
i = 0
|
|
57
|
+
while i < len(lines):
|
|
58
|
+
line = lines[i]
|
|
59
|
+
if not line.startswith("diff --git "):
|
|
60
|
+
i += 1
|
|
62
61
|
continue
|
|
63
62
|
|
|
64
63
|
# 提取文件路径
|
|
65
|
-
|
|
66
|
-
m = re.match(r"diff --git a/(.+?) b/(.+)", first_line)
|
|
64
|
+
m = re.match(r"diff --git a/(.+?) b/(.+)", line)
|
|
67
65
|
if not m:
|
|
66
|
+
i += 1
|
|
68
67
|
continue
|
|
69
68
|
|
|
70
69
|
file_path = m.group(2)
|
|
71
|
-
is_rename = "rename from" in block
|
|
72
|
-
is_chmod = "old mode" in block or "new mode" in block
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
# 收集 header 行(diff --git 之后到下一个 diff --git / --- / 空内容)
|
|
72
|
+
header_lines: list[str] = []
|
|
73
|
+
has_hunk = False
|
|
74
|
+
j = i + 1
|
|
75
|
+
while j < len(lines):
|
|
76
|
+
l = lines[j]
|
|
77
|
+
if l.startswith("diff --git ") or l.startswith("--- "):
|
|
78
|
+
break
|
|
79
|
+
if l.startswith("@@ "):
|
|
80
|
+
has_hunk = True
|
|
81
|
+
break
|
|
82
|
+
header_lines.append(l)
|
|
83
|
+
j += 1
|
|
84
|
+
|
|
85
|
+
# 只处理无 hunk body 的 header-only diff
|
|
86
|
+
if not has_hunk:
|
|
87
|
+
header_text = "\n".join(header_lines)
|
|
88
|
+
is_rename = "rename from" in header_text
|
|
89
|
+
is_chmod = "old mode" in header_text or "new mode" in header_text
|
|
90
|
+
if is_rename or is_chmod:
|
|
91
|
+
files.append(FileInfo(
|
|
92
|
+
file_path=file_path,
|
|
93
|
+
hunk_count=0,
|
|
94
|
+
is_creation=False,
|
|
95
|
+
))
|
|
96
|
+
|
|
97
|
+
i = j if j > i + 1 else i + 1
|
|
80
98
|
|
|
81
99
|
return files
|
|
82
100
|
|
|
@@ -142,19 +160,21 @@ def validate_patch(
|
|
|
142
160
|
return ValidationResult(valid=False, parse_error=str(e))
|
|
143
161
|
|
|
144
162
|
# mpatch.parse_auto 无法解析 header-only diff(纯 rename/chmod)
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return ValidationResult(
|
|
150
|
-
valid=True, files=header_files, total_hunks=0,
|
|
151
|
-
)
|
|
163
|
+
# 始终检查 header-only diff,与 parse_auto 结果合并
|
|
164
|
+
header_files = _parse_header_only_diffs(patch_content)
|
|
165
|
+
|
|
166
|
+
if not patches and not header_files:
|
|
152
167
|
return ValidationResult(
|
|
153
168
|
valid=False, parse_error="No patches found in content"
|
|
154
169
|
)
|
|
155
170
|
|
|
156
171
|
files: list[FileInfo] = []
|
|
157
172
|
total_hunks = 0
|
|
173
|
+
|
|
174
|
+
# header-only diff(rename/chmod 无 hunk body)
|
|
175
|
+
files.extend(header_files)
|
|
176
|
+
|
|
177
|
+
# hunked diff(mpatch 解析结果)
|
|
158
178
|
for p in patches:
|
|
159
179
|
hunk_count = len(p)
|
|
160
180
|
total_hunks += hunk_count
|
|
@@ -62,6 +62,7 @@ def generate_patch_tool(
|
|
|
62
62
|
output_path: str | None = None,
|
|
63
63
|
work_dir: str | None = None,
|
|
64
64
|
overwrite: bool = False,
|
|
65
|
+
append: bool = True,
|
|
65
66
|
) -> dict:
|
|
66
67
|
"""Generate a valid unified diff patch from structured file changes.
|
|
67
68
|
|
|
@@ -73,6 +74,8 @@ def generate_patch_tool(
|
|
|
73
74
|
output_path: Optional file path to write the patch to.
|
|
74
75
|
work_dir: Base directory for relativizing absolute paths in diff headers.
|
|
75
76
|
overwrite: If true, overwrite existing output file. Default false.
|
|
77
|
+
append: If true (default), append to existing output file.
|
|
78
|
+
Set to false to require a fresh file.
|
|
76
79
|
|
|
77
80
|
Returns:
|
|
78
81
|
Dict with patch, files_changed, self_valid, skipped_files, or error.
|
|
@@ -83,6 +86,7 @@ def generate_patch_tool(
|
|
|
83
86
|
output_path=output_path,
|
|
84
87
|
work_dir=work_dir,
|
|
85
88
|
overwrite=overwrite,
|
|
89
|
+
append=append,
|
|
86
90
|
)
|
|
87
91
|
return {
|
|
88
92
|
"patch": result.patch,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|