uni-diff-patch 0.0.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uni-diff-patch
3
- Version: 0.0.2
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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "uni-diff-patch"
3
- version = "0.0.2"
3
+ version = "0.0.3"
4
4
  description = "MCP server and CLI tool for generating valid unified diff patches deterministically"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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:
@@ -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
File without changes