uni-diff-patch 0.0.1__py3-none-any.whl → 0.0.3__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.
uni_diff_patch/cli.py CHANGED
@@ -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
- return _create_unified_diff(patch_path, "", change.new_content, 3)
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
- return _create_unified_diff(patch_path, old, "", 3)
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
- if out.exists() and not overwrite:
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
- out.parent.mkdir(parents=True, exist_ok=True)
400
- out.write_text(combined, encoding="utf-8")
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 looking for 'diff --git' lines followed by
48
- rename/mode headers without any @@ hunk markers.
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
- # 匹配 "diff --git a/X b/Y"
55
- diff_blocks = re.split(r"(?=^diff --git )", content, flags=re.MULTILINE)
56
- for block in diff_blocks:
57
- block = block.strip()
58
- if not block.startswith("diff --git "):
59
- continue
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
- first_line = block.split("\n")[0]
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
- if is_rename or is_chmod:
75
- files.append(FileInfo(
76
- file_path=file_path,
77
- hunk_count=0,
78
- is_creation=False,
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
- # 如果 parse 结果为空,检查是否包含 git 扩展 header
146
- if not patches:
147
- header_files = _parse_header_only_diffs(patch_content)
148
- if header_files:
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
uni_diff_patch/server.py CHANGED
@@ -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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uni-diff-patch
3
- Version: 0.0.1
3
+ Version: 0.0.3
4
4
  Summary: MCP server and CLI tool for generating valid unified diff patches deterministically
5
5
  Author: LimLLL
6
6
  Author-email: LimLLL <github@awebapp.useforall.com>
@@ -0,0 +1,13 @@
1
+ uni_diff_patch/__init__.py,sha256=QZHH9fShZy0Uae-0TF-6HN35C_3nCbGHcYE56eNy83E,1066
2
+ uni_diff_patch/cli.py,sha256=IgqlrUySgehRRNbY1h-vgkXt0jiKU8zKJRyJWVWRREI,5286
3
+ uni_diff_patch/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ uni_diff_patch/core/diff_engine.py,sha256=E2-ZPkgyXv3QrQUFAMGh4t97Tvj-_gxbPZ6W6ItL_bY,14244
5
+ uni_diff_patch/core/search_replace.py,sha256=kwUfr-SqB9oZJwXK0A3LOEmccXi_fx5SGQD_MPN83Rk,4592
6
+ uni_diff_patch/core/validator.py,sha256=ZXO5xNCGNu1MjWr2r3xgWQA7mmMwAiWl03Rn-vBqnBk,7160
7
+ uni_diff_patch/models.py,sha256=M7PcWRcUWUz0CO8MK7z5CDwYxpoN-MCmihvAq7zH9x0,6154
8
+ uni_diff_patch/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ uni_diff_patch/server.py,sha256=9mswkL4i0zfMczEtHJFd3MQKkK-SPQln2H2KyUgJ1Hw,5014
10
+ uni_diff_patch-0.0.3.dist-info/WHEEL,sha256=YR4QUxGCbsyoOnWBVTv-p1I4yc3seKGPev46mvmc8Cg,81
11
+ uni_diff_patch-0.0.3.dist-info/entry_points.txt,sha256=SMaBpCbADshKmWiXYPpy_PSVvKWf0KQcwNJXi_FIsoM,60
12
+ uni_diff_patch-0.0.3.dist-info/METADATA,sha256=UtsHSKzmdqwqkqeLn-qzjlnbkF-DlhaV9NG2hxgDTEE,4870
13
+ uni_diff_patch-0.0.3.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- uni_diff_patch/__init__.py,sha256=QZHH9fShZy0Uae-0TF-6HN35C_3nCbGHcYE56eNy83E,1066
2
- uni_diff_patch/cli.py,sha256=XyqjzIUHsli_SGBkbFxxOL7Y02NY_Wz7x54fGgZDE9I,5109
3
- uni_diff_patch/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- uni_diff_patch/core/diff_engine.py,sha256=lARXSa8iacdLRRZlxI_H21hgwf2q9HC1PR1Ob0oddjI,13358
5
- uni_diff_patch/core/search_replace.py,sha256=kwUfr-SqB9oZJwXK0A3LOEmccXi_fx5SGQD_MPN83Rk,4592
6
- uni_diff_patch/core/validator.py,sha256=qf66MXYT3zvHsqQ3l6PlvyK5Go6_DXu89QcIIxlE5cg,6639
7
- uni_diff_patch/models.py,sha256=M7PcWRcUWUz0CO8MK7z5CDwYxpoN-MCmihvAq7zH9x0,6154
8
- uni_diff_patch/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- uni_diff_patch/server.py,sha256=NAjVofdUUqL3VI3AgtCT7ky7Csf9atSenF6d3-734cM,4845
10
- uni_diff_patch-0.0.1.dist-info/WHEEL,sha256=YR4QUxGCbsyoOnWBVTv-p1I4yc3seKGPev46mvmc8Cg,81
11
- uni_diff_patch-0.0.1.dist-info/entry_points.txt,sha256=SMaBpCbADshKmWiXYPpy_PSVvKWf0KQcwNJXi_FIsoM,60
12
- uni_diff_patch-0.0.1.dist-info/METADATA,sha256=mK4hc0wOlTpdUdyfNvtxTvZJf4cRY3uQ73_7vYX-sM4,4870
13
- uni_diff_patch-0.0.1.dist-info/RECORD,,