mseep-patche 1.0.1__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.
Patche/utils/parse.py ADDED
@@ -0,0 +1,269 @@
1
+ import re
2
+ from typing import List, Optional
3
+
4
+ from whatthepatch_pydantic import parse_patch as wtp_parse_patch
5
+ from whatthepatch_pydantic.model import Diff as WTPDiff
6
+
7
+ from Patche.config import settings
8
+ from Patche.model import Change, Diff, Hunk, Patch
9
+ from Patche.utils.header import CHANGE_LINE, HEADER_OLD, HUNK_START, parse_header
10
+
11
+ git_diffcmd_header = re.compile("^diff --git a/(.+) b/(.+)$")
12
+ unified_diff_header = re.compile("^---\s{1}")
13
+ spliter_line = re.compile("^---$")
14
+
15
+
16
+ def changes_to_hunks(changes: list[Change]) -> list[Hunk]:
17
+ """
18
+ Convert a list of changes to a list of hunks
19
+
20
+ Args:
21
+ changes (list[Change]): A list of changes
22
+
23
+ Returns:
24
+ list[Hunk]: A list of hunks
25
+ """
26
+
27
+ # 首先统计 Hunk 数
28
+ hunk_indexes = []
29
+ for change in changes:
30
+ if change.hunk not in hunk_indexes:
31
+ hunk_indexes.append(change.hunk)
32
+
33
+ # 将changes按照hunk分组,注意同一个 hunk 中的 change 要进行分类,前三行要放入前置上下文,中间的要放入中间上下文,后三行要放入后置上下文
34
+ hunk_list: list[Hunk] = []
35
+ for hunk_index in hunk_indexes:
36
+ hunk_changes = [change for change in changes if change.hunk == hunk_index]
37
+
38
+ # 这里遍历的顺序已经是正确的顺序
39
+ hunk_context = []
40
+ hunk_middle = []
41
+ hunk_post = []
42
+ # 首先正向遍历,获取前置上下文
43
+ for change in hunk_changes:
44
+ if change.old is not None and change.new is not None:
45
+ hunk_context.append(change)
46
+ else:
47
+ break
48
+
49
+ # 然后反向遍历,获取后置上下文
50
+ for change in reversed(hunk_changes):
51
+ if change.old is not None and change.new is not None:
52
+ hunk_post.append(change)
53
+ else:
54
+ break
55
+
56
+ assert len(hunk_context) <= settings.max_diff_lines
57
+ assert len(hunk_post) <= settings.max_diff_lines
58
+
59
+ # 最后获取中间代码
60
+ for change in hunk_changes:
61
+ if change not in hunk_context and change not in hunk_post:
62
+ hunk_middle.append(change)
63
+
64
+ # 注意把后置上下文反转回来
65
+ hunk_post = list(reversed(hunk_post))
66
+
67
+ hunk_list.append(
68
+ Hunk(
69
+ index=hunk_index,
70
+ context=hunk_context,
71
+ middle=hunk_middle,
72
+ post=hunk_post,
73
+ all_=hunk_changes,
74
+ )
75
+ )
76
+
77
+ return hunk_list
78
+
79
+
80
+ def wtp_diff_to_diff(wtp_diff: WTPDiff) -> Diff:
81
+ """
82
+ Convert a whatthepatch Diff object to a patche Diff object
83
+
84
+ Args:
85
+ wtp_diff (WTPDiff): A whatthepatch Diff object
86
+
87
+ Returns:
88
+ Diff: A patche Diff object
89
+ """
90
+
91
+ return Diff(
92
+ header=wtp_diff.header,
93
+ changes=wtp_diff.changes,
94
+ text=wtp_diff.text,
95
+ hunks=changes_to_hunks(wtp_diff.changes),
96
+ )
97
+
98
+
99
+ def parse_unified_diff(text: str) -> Optional[List[Diff]]:
100
+ """解析 unified diff 格式的补丁"""
101
+ lines = iter(text.splitlines())
102
+ diffs: List[Diff] = []
103
+
104
+ while True:
105
+ try:
106
+ header = parse_header(lines)
107
+ if not header:
108
+ break
109
+
110
+ changes: List[Change] = []
111
+ hunk_index = 0
112
+
113
+ for line in lines:
114
+ # 检查是否是新的 diff 块开始
115
+ if HEADER_OLD.match(line):
116
+ lines = iter([line] + list(lines))
117
+ break
118
+
119
+ # 解析 hunk 头
120
+ hunk_match = HUNK_START.match(line)
121
+ if hunk_match:
122
+ old_start = int(hunk_match.group(1))
123
+ old_count = int(hunk_match.group(2) or "1")
124
+ new_start = int(hunk_match.group(3))
125
+ new_count = int(hunk_match.group(4) or "1")
126
+
127
+ old_current = old_start
128
+ new_current = new_start
129
+ hunk_index += 1
130
+ continue
131
+
132
+ # 解析变更行
133
+ change_match = CHANGE_LINE.match(line)
134
+ if change_match:
135
+ change_type = change_match.group(1)
136
+ content = change_match.group(2)
137
+
138
+ if change_type == " ":
139
+ # 上下文行 / 中间行
140
+ changes.append(
141
+ Change(
142
+ old=old_current,
143
+ new=new_current,
144
+ line=content,
145
+ hunk=hunk_index,
146
+ )
147
+ )
148
+ old_current += 1
149
+ new_current += 1
150
+ elif change_type == "-":
151
+ # 删除行
152
+ changes.append(
153
+ Change(
154
+ old=old_current, new=None, line=content, hunk=hunk_index
155
+ )
156
+ )
157
+ old_current += 1
158
+ elif change_type == "+":
159
+ # 新增行
160
+ changes.append(
161
+ Change(
162
+ old=None, new=new_current, line=content, hunk=hunk_index
163
+ )
164
+ )
165
+ new_current += 1
166
+
167
+ if header and changes:
168
+ diffs.append(
169
+ Diff(
170
+ header=header,
171
+ changes=changes,
172
+ text=text,
173
+ hunks=changes_to_hunks(changes),
174
+ )
175
+ )
176
+
177
+ except StopIteration:
178
+ break
179
+
180
+ return diffs if diffs else None
181
+
182
+
183
+ def parse_patch(text: str) -> Patch:
184
+ """
185
+ Parse a patch file
186
+ Diiference between this and whatthepatch.parse_patch is that this function also
187
+ returns the sha, author, date and message of the commit
188
+ """
189
+
190
+ lines = text.splitlines()
191
+
192
+ idx = 0
193
+ for i, line in enumerate(lines):
194
+ # 这里考虑 git log 格式和 git format-patch 格式
195
+ if (
196
+ git_diffcmd_header.match(line)
197
+ or spliter_line.match(line)
198
+ or unified_diff_header.match(line)
199
+ ):
200
+ idx = i
201
+ break
202
+
203
+ else:
204
+ # raise ValueError(
205
+ # "No diff --git line found, check if the input is a valid patch"
206
+ # )
207
+ idx = len(lines) + 1
208
+
209
+ git_message_lines: list[str] = []
210
+ if idx == 0:
211
+ return Patch(
212
+ sha=None,
213
+ author=None,
214
+ date=None,
215
+ subject=None,
216
+ message=None,
217
+ # diff=[wtp_diff_to_diff(diff) for diff in wtp_parse_patch(text)],
218
+ diff=parse_unified_diff(text),
219
+ )
220
+ else:
221
+ git_message_lines = lines[:idx]
222
+
223
+ message = "\n".join(git_message_lines)
224
+
225
+ sha_line = git_message_lines.pop(0)
226
+ if sha_line.startswith("From ") or sha_line.startswith("commit "):
227
+ sha = sha_line.split(" ")[1]
228
+ else:
229
+ sha = None
230
+
231
+ author_line = git_message_lines.pop(0)
232
+ if author_line.startswith("Author: ") or author_line.startswith("From:"):
233
+ author = " ".join(author_line.split(" ")[1:])
234
+ else:
235
+ author = None
236
+
237
+ date_line = git_message_lines.pop(0)
238
+ if date_line.startswith("Date: "):
239
+ date_str = date_line.split("Date: ")[1]
240
+ # 解析 Thu, 7 Mar 2024 15:41:57 +0800 或 Tue Feb 2 16:07:37 2021 +0100
241
+ # if "," in date_str:
242
+ # date_fromat = "%a, %d %b %Y %H:%M:%S %z"
243
+ # else:
244
+ # date_fromat = "%a %b %d %H:%M:%S %Y %z"
245
+
246
+ # date = datetime.datetime.strptime(date_str.strip(), date_fromat)
247
+ date = date_str.strip()
248
+ else:
249
+ date = None
250
+
251
+ # 如果接下来的一行以 Subject 开头,则直接解析出 subject
252
+ if git_message_lines[0].startswith("Subject: "):
253
+ subject = git_message_lines.pop(0).split("Subject: ")[1]
254
+ else:
255
+ # 否则找到剩余的行里第一个非换行/非空行作为 subject
256
+ subject = None
257
+ for line in git_message_lines:
258
+ if line.strip() != "":
259
+ subject = line
260
+ break
261
+
262
+ return Patch(
263
+ sha=sha.strip() if sha else None,
264
+ author=author.strip() if author else None,
265
+ date=date,
266
+ subject=subject.strip() if subject else None,
267
+ message=message,
268
+ diff=[wtp_diff_to_diff(diff) for diff in wtp_parse_patch(text)],
269
+ )
@@ -0,0 +1,168 @@
1
+ from Patche.app import logger
2
+ from Patche.config import settings
3
+ from Patche.model import ApplyResult, Hunk, Line
4
+ from Patche.utils.common import find_list_positions
5
+
6
+
7
+ def apply_change(
8
+ hunk_list: list[Hunk],
9
+ target: list[Line],
10
+ reverse: bool = False,
11
+ flag_hunk_list: list[int] = None,
12
+ fuzz: int = 2, # set fuzz=2 according GNU Patch
13
+ ) -> ApplyResult:
14
+ """Apply a diff to a target string."""
15
+
16
+ flag_hunk_list = [] if flag_hunk_list is None else flag_hunk_list
17
+
18
+ if fuzz > settings.max_diff_lines or fuzz < 0:
19
+ raise Exception(f"fuzz value should be less than {settings.max_diff_lines}")
20
+
21
+ if reverse:
22
+ for hunk in hunk_list:
23
+ for change in hunk.context + hunk.middle + hunk.post:
24
+ change.old, change.new = change.new, change.old
25
+
26
+ # 然后对每个hunk进行处理,添加偏移
27
+ failed_hunk_list: list[Hunk] = []
28
+ last_pos = None
29
+
30
+ last_offset = 0
31
+ line_count_diff = 0
32
+ for hunk in hunk_list:
33
+
34
+ current_hunk_fuzz = 0
35
+
36
+ while current_hunk_fuzz <= fuzz:
37
+
38
+ # hunk.context = hunk.context[1:]
39
+ # hunk.post = hunk.post[: fuzz - current_hunk_fuzz]
40
+
41
+ logger.debug(
42
+ f"current_fuzz: {current_hunk_fuzz} len(hunk.context): {len(hunk.context)} len(hunk.post): {len(hunk.post)}"
43
+ )
44
+
45
+ changes_to_search = hunk.context + hunk.middle + hunk.post
46
+ pos_list = find_list_positions(
47
+ [line.content for line in target],
48
+ [change.line for change in changes_to_search if change.old is not None],
49
+ )
50
+
51
+ if len(pos_list) != 0:
52
+ break
53
+
54
+ current_hunk_fuzz += 1
55
+
56
+ if current_hunk_fuzz <= fuzz:
57
+ hunk.context = hunk.context[1:]
58
+ hunk.post = hunk.post[: 3 - current_hunk_fuzz]
59
+
60
+ # 初始位置是 context 的第一个
61
+ # 注意,前几个有可能是空
62
+ pos_origin = None
63
+ for change in changes_to_search:
64
+ if change.old is not None:
65
+ pos_origin = change.old
66
+ break
67
+
68
+ # TODO: 这里不太对,要想一下怎么处理,不应该是加入 failed hunk list
69
+ # 仅在 -F 3 且只有添加行 的情况下出现(指与 GNU patch 行为不一致)
70
+ # 也可以看一下这样的情况有多少
71
+ if current_hunk_fuzz == fuzz and pos_origin is not None:
72
+ # failed_hunk_list.append(hunk)
73
+ # logger.debug(f"Could not determine pos_origin")
74
+ # logger.warning(f"Apply failed with hunk {hunk.index}")
75
+ # continue
76
+ for change in changes_to_search:
77
+ if change.new is not None:
78
+ pos_origin = change.new
79
+ break
80
+
81
+ # 使用上一次偏移加上行数变化差值
82
+ min_offset = last_offset
83
+ else:
84
+ if len(pos_list) == 0:
85
+ failed_hunk_list.append(hunk)
86
+ logger.debug(f"Could not determine proper position")
87
+ logger.warning(f"Apply failed with hunk {hunk.index}")
88
+ continue
89
+
90
+ offset_list = [
91
+ pos + 1 - pos_origin for pos in pos_list
92
+ ] # 确认这里是否需要 1?
93
+
94
+ # 计算最小 offset
95
+ min_offset = None
96
+ for offset in offset_list:
97
+ if min_offset is None or abs(offset) < abs(min_offset):
98
+ min_offset = offset
99
+
100
+ if reverse:
101
+ min_offset += line_count_diff
102
+ pos_origin -= line_count_diff
103
+
104
+ last_offset = min_offset
105
+
106
+ # 更新行数变化差值
107
+ hunk_add_count = sum(
108
+ 1 for c in changes_to_search if c.old is None and c.new is not None
109
+ )
110
+ hunk_del_count = sum(
111
+ 1 for c in changes_to_search if c.new is None and c.old is not None
112
+ )
113
+ line_count_diff += hunk_del_count - hunk_add_count
114
+
115
+ logger.info(
116
+ f"Apply hunk {hunk.index} with offset {min_offset} fuzz {current_hunk_fuzz} line_diff {line_count_diff}"
117
+ )
118
+
119
+ # 直接按照 pos 进行替换
120
+ # 选择 offset 最小的 pos
121
+ pos_new = pos_origin + min_offset - 1
122
+
123
+ # 处理 pos_new 小于 last_pos 的情况
124
+ logger.debug(f"pos_origin: {pos_origin}, last_pos: {last_pos}")
125
+ if last_pos is None:
126
+ last_pos = pos_new
127
+ elif pos_new < last_pos:
128
+ # 特别主要 pos_new 小于 last_pos 的情况
129
+ logger.warning(f"Apply failed with hunk {hunk.index}")
130
+ logger.error(f"pos: {pos_new} is greater than last_pos: {last_pos}")
131
+ failed_hunk_list.append(hunk)
132
+ continue
133
+ else:
134
+ last_pos = pos_new
135
+
136
+ old_lines = [
137
+ change.line
138
+ for change in hunk.context + hunk.middle + hunk.post
139
+ if change.old is not None
140
+ ]
141
+ new_lines = [
142
+ change.line
143
+ for change in hunk.context + hunk.middle + hunk.post
144
+ if change.new is not None
145
+ ]
146
+
147
+ # 检查 pos_new 位置的行是否和 old_lines 一致
148
+ for i in range(len(old_lines)):
149
+ if target[pos_new + i].content != old_lines[i]:
150
+ raise Exception(
151
+ f'line {pos_new + i}, "{target[pos_new + i].content}" does not match "{old_lines[i]}"'
152
+ )
153
+
154
+ # 以切片的方式进行替换
155
+ target = (
156
+ target[:pos_new]
157
+ + [
158
+ Line(index=pos_new + i, content=new_lines[i])
159
+ for i in range(len(new_lines))
160
+ ]
161
+ + target[pos_new + len(old_lines) :]
162
+ )
163
+
164
+ return ApplyResult(
165
+ new_line_list=target,
166
+ conflict_hunk_num_list=[],
167
+ failed_hunk_list=failed_hunk_list,
168
+ )
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.1
2
+ Name: mseep-patche
3
+ Version: 1.0.1
4
+ Summary: Modern Patch in Python
5
+ Author-Email: mseep <support@skydeck.ai>
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: typer[all]>=0.9.0
9
+ Requires-Dist: pydantic>=2.5.3
10
+ Requires-Dist: pydantic-settings>=2.2.1
11
+ Requires-Dist: whatthepatch-pydantic==1.0.6a3
12
+ Description-Content-Type: text/plain
13
+
14
+ Package managed by MseeP.ai
@@ -0,0 +1,24 @@
1
+ Patche/__init__.py,sha256=PVoMA8KWQKPFthAyEIn4oNZATeGNTGXVuqLfqoOjfmE,34
2
+ Patche/__main__.py,sha256=QXfE3TKpFZsct7wDbpv5g3tQGmrUkBMjL_nXg_wJvEM,28
3
+ Patche/__version__.py,sha256=wtfcRFm73_vwwNMAiQol_VGC7QetgMQWRsGYiTuqF0c,185
4
+ Patche/app.py,sha256=I5AjgCmHmIE6jPaYYdbRwBL4dv5BK3nB4fKFDgI3u_w,838
5
+ Patche/commands/apply.py,sha256=POcKV3oxtqD6jbkrujKycOnG9ruk1Wj6QadXlNQCb4U,3509
6
+ Patche/commands/help.py,sha256=nEGX9GHT2CJHchLh02ZJ4RxPwWf84j6j4YpSFwS6cLo,214
7
+ Patche/commands/show.py,sha256=Io-9vruhgrPf5GalJTfEm6KNseQ2LY6Eu0MiDLtcSFs,1105
8
+ Patche/config.py,sha256=6k9RZ8skRTQ8hPYI1I63PkupsLkZSCmVxIhJZdwRVNU,499
9
+ Patche/mcp/__init__.py,sha256=Y6bkduOn9Ljy76x8bOFGrcjx4RFB8zrY_FCVC7grfEY,815
10
+ Patche/mcp/__main__.py,sha256=3xkjNDGmST-F8nTIpnE0OVBBBGL3sjToNbc-QRebi9k,36
11
+ Patche/mcp/model.py,sha256=p_uTVFVB9PubhFJaUbqNeOlJr63WvTK70lmLDx7uLxM,472
12
+ Patche/mcp/prompts.py,sha256=VFc0h6ayMmWvHg-NKTJgoCFS7KzSA6ttzy5JK8TT7nM,7726
13
+ Patche/mcp/server.py,sha256=5fhkLIAgC5jf6qGgV0dh9lqEd13VvycUaBTdPjXYt_Y,3450
14
+ Patche/mcp/tools.py,sha256=u7gNMd9NEXGpI20atx3pIEjeZLMCo6bUvtrSs_eoFy8,4886
15
+ Patche/model.py,sha256=Dm-LneXe3idcxNRbAhZ01Z6hqw-HDioHeJ07h2aPdvM,1842
16
+ Patche/utils/common.py,sha256=_4o3z2Dq-9qG2ahX3UmIfShTO1mesZ3DXGu0EIeS6BE,2020
17
+ Patche/utils/header.py,sha256=t4wlitYDaZRq2zGlP0FqIP2ihz83eg_7jJ9VDuXnIYY,1075
18
+ Patche/utils/parse.py,sha256=RVWxCAbweEDN7y6RQ44R-rRw-GDLFvRamMFxW9hOveA,8573
19
+ Patche/utils/resolve.py,sha256=4bpUYtSfDK5dwBldw4essjirI4zbMOxutiMBnhPubkw,5854
20
+ mseep_patche-1.0.1.dist-info/METADATA,sha256=GA-Wovcm8kAphOFhOuZs-zFpStPivRMS53Z_Jyyk5V4,381
21
+ mseep_patche-1.0.1.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
22
+ mseep_patche-1.0.1.dist-info/entry_points.txt,sha256=OhS7Z9mx7By8K1v0_XTqzwGs7v5BQEocPpu20F0WrOE,100
23
+ mseep_patche-1.0.1.dist-info/licenses/LICENSE,sha256=2I7sSSPi_J8S7pOQIvPdaRQubMmIpsDI6qNblsWv5ek,1066
24
+ mseep_patche-1.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: pdm-backend (2.4.5)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,6 @@
1
+ [console_scripts]
2
+ patche = Patche.__init__:app
3
+ patche-mcp = Patche.mcp.__init__:app
4
+
5
+ [gui_scripts]
6
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 jingfelix
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.