patch-fixer 0.4.0__py3-none-any.whl → 0.4.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.
- patch_fixer/patch_fixer.py +47 -46
- {patch_fixer-0.4.0.dist-info → patch_fixer-0.4.1.dist-info}/METADATA +1 -1
- patch_fixer-0.4.1.dist-info/RECORD +10 -0
- patch_fixer-0.4.0.dist-info/RECORD +0 -10
- {patch_fixer-0.4.0.dist-info → patch_fixer-0.4.1.dist-info}/WHEEL +0 -0
- {patch_fixer-0.4.0.dist-info → patch_fixer-0.4.1.dist-info}/entry_points.txt +0 -0
- {patch_fixer-0.4.0.dist-info → patch_fixer-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {patch_fixer-0.4.0.dist-info → patch_fixer-0.4.1.dist-info}/top_level.txt +0 -0
patch_fixer/patch_fixer.py
CHANGED
@@ -7,18 +7,18 @@ from pathlib import Path
|
|
7
7
|
|
8
8
|
from git import Repo
|
9
9
|
|
10
|
-
path_regex = r'(?:[
|
10
|
+
path_regex = r'[^ \n\t]+(?: [^ \n\t]+)*'
|
11
11
|
regexes = {
|
12
|
-
"DIFF_LINE": re.compile(rf'diff --git (a/{path_regex}) (b/{path_regex})'),
|
13
|
-
"MODE_LINE": re.compile(r'(new|deleted) file mode [0-7]{6}'),
|
14
|
-
"INDEX_LINE": re.compile(r'index [0-9a-f]{7,64}\.\.[0-9a-f]{7,64}(?: [0-7]{6})
|
15
|
-
"BINARY_LINE": re.compile(rf'Binary files (a/{path_regex}|/dev/null) and (b/{path_regex}|/dev/null) differ'),
|
16
|
-
"RENAME_FROM": re.compile(rf'rename from ({path_regex})'),
|
17
|
-
"RENAME_TO": re.compile(rf'rename to ({path_regex})'),
|
18
|
-
"FILE_HEADER_START": re.compile(rf'
|
19
|
-
"FILE_HEADER_END": re.compile(rf'
|
12
|
+
"DIFF_LINE": re.compile(rf'^diff --git (a/{path_regex}) (b/{path_regex})$'),
|
13
|
+
"MODE_LINE": re.compile(r'^(new|deleted) file mode [0-7]{6}$'),
|
14
|
+
"INDEX_LINE": re.compile(r'^index [0-9a-f]{7,64}\.\.[0-9a-f]{7,64}(?: [0-7]{6})?$|^similarity index ([0-9]+)%$'),
|
15
|
+
"BINARY_LINE": re.compile(rf'^Binary files (a/{path_regex}|/dev/null) and (b/{path_regex}|/dev/null) differ$'),
|
16
|
+
"RENAME_FROM": re.compile(rf'^rename from ({path_regex})$'),
|
17
|
+
"RENAME_TO": re.compile(rf'^rename to ({path_regex})$'),
|
18
|
+
"FILE_HEADER_START": re.compile(rf'^--- (a/{path_regex}|/dev/null)$'),
|
19
|
+
"FILE_HEADER_END": re.compile(rf'^\+\+\+ (b/{path_regex}|/dev/null)$'),
|
20
20
|
"HUNK_HEADER": re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$'),
|
21
|
-
"END_LINE": re.compile(r'
|
21
|
+
"END_LINE": re.compile(r'^\$'),
|
22
22
|
}
|
23
23
|
|
24
24
|
|
@@ -62,6 +62,12 @@ class OutOfOrderHunk(HunkErrorBase):
|
|
62
62
|
f"\nOccurs before previous hunk with header {self.prev_header}")
|
63
63
|
|
64
64
|
|
65
|
+
class EmptyHunk(Exception):
|
66
|
+
# don't inherit from HunkErrorBase since this is a sentinel exception
|
67
|
+
# meant to catch the case where the very last hunk is empty
|
68
|
+
pass
|
69
|
+
|
70
|
+
|
65
71
|
class BadCarriageReturn(ValueError):
|
66
72
|
pass
|
67
73
|
|
@@ -145,19 +151,6 @@ def find_hunk_start(context_lines, original_lines, fuzzy=False):
|
|
145
151
|
if all(equal_lines):
|
146
152
|
return i
|
147
153
|
|
148
|
-
# try with more flexible whitespace matching
|
149
|
-
for i in range(len(original_lines) - len(ctx) + 1):
|
150
|
-
equal_lines = []
|
151
|
-
for j in range(len(ctx)):
|
152
|
-
orig_line = original_lines[i + j].strip()
|
153
|
-
ctx_line = ctx[j].strip()
|
154
|
-
# normalize whitespace: convert multiple spaces/tabs to single space
|
155
|
-
orig_normalized = ' '.join(orig_line.split())
|
156
|
-
ctx_normalized = ' '.join(ctx_line.split())
|
157
|
-
equal_lines.append(orig_normalized == ctx_normalized)
|
158
|
-
if all(equal_lines):
|
159
|
-
return i
|
160
|
-
|
161
154
|
# if fuzzy matching is enabled and exact match failed, try fuzzy match
|
162
155
|
if fuzzy:
|
163
156
|
best_match_score = 0.0
|
@@ -226,9 +219,13 @@ def find_all_hunk_starts(hunk_lines, search_lines, fuzzy=False):
|
|
226
219
|
def capture_hunk(current_hunk, original_lines, offset, last_hunk, old_header, fuzzy=False):
|
227
220
|
"""
|
228
221
|
Try to locate the hunk's true position in the original file.
|
222
|
+
|
229
223
|
If multiple possible matches exist, pick the one closest to the expected
|
230
224
|
(possibly corrupted) line number derived from the old hunk header.
|
231
225
|
"""
|
226
|
+
if not current_hunk:
|
227
|
+
raise EmptyHunk
|
228
|
+
|
232
229
|
# extract needed info from old header match groups
|
233
230
|
expected_old_start = int(old_header[0]) if old_header else 0
|
234
231
|
try:
|
@@ -236,11 +233,27 @@ def capture_hunk(current_hunk, original_lines, offset, last_hunk, old_header, fu
|
|
236
233
|
except IndexError:
|
237
234
|
hunk_context = ""
|
238
235
|
|
236
|
+
# presence or absence of end line shouldn't affect line counts
|
237
|
+
if regexes["END_LINE"].match(current_hunk[-1]):
|
238
|
+
hunk_len = len(current_hunk) - 1
|
239
|
+
else:
|
240
|
+
hunk_len = len(current_hunk)
|
241
|
+
|
239
242
|
# compute line counts
|
240
|
-
|
241
|
-
|
243
|
+
context_count = sum(1 for l in current_hunk if l.startswith(' '))
|
244
|
+
minus_count = sum(1 for l in current_hunk if l.startswith('-'))
|
245
|
+
plus_count = sum(1 for l in current_hunk if l.startswith('+'))
|
242
246
|
|
243
|
-
|
247
|
+
old_count = context_count + minus_count
|
248
|
+
new_count = context_count + plus_count
|
249
|
+
|
250
|
+
if minus_count == hunk_len: # file deletion
|
251
|
+
old_start = 1
|
252
|
+
new_start = 0
|
253
|
+
elif plus_count == hunk_len: # file creation
|
254
|
+
old_start = 0
|
255
|
+
new_start = 1
|
256
|
+
else: # file modification
|
244
257
|
search_index = last_hunk
|
245
258
|
search_lines = original_lines[search_index:]
|
246
259
|
|
@@ -260,8 +273,10 @@ def capture_hunk(current_hunk, original_lines, offset, last_hunk, old_header, fu
|
|
260
273
|
# pick first match if no expected line info
|
261
274
|
old_start = candidate_positions[0] + 1
|
262
275
|
else:
|
263
|
-
# try from start of file
|
264
|
-
|
276
|
+
# try from start of file, excluding lines already searched
|
277
|
+
search_index += hunk_len
|
278
|
+
search_lines = original_lines[:search_index]
|
279
|
+
matches = find_all_hunk_starts(current_hunk, search_lines, fuzzy=fuzzy)
|
265
280
|
if not matches:
|
266
281
|
raise MissingHunkError(current_hunk)
|
267
282
|
if expected_old_start:
|
@@ -279,11 +294,6 @@ def capture_hunk(current_hunk, original_lines, offset, last_hunk, old_header, fu
|
|
279
294
|
new_start = 0
|
280
295
|
else:
|
281
296
|
new_start = old_start + offset
|
282
|
-
else:
|
283
|
-
# old count of zero can only mean file creation, since adding lines to
|
284
|
-
# an existing file requires surrounding context lines without a +
|
285
|
-
old_start = 0
|
286
|
-
new_start = 1 # line numbers are 1-indexed in the real world
|
287
297
|
|
288
298
|
offset += (new_count - old_count)
|
289
299
|
|
@@ -362,7 +372,6 @@ def fix_patch(patch_lines, original, remove_binary=False, fuzzy=False, add_newli
|
|
362
372
|
file_start_header = False
|
363
373
|
file_end_header = False
|
364
374
|
look_for_rename = False
|
365
|
-
similarity_index = None
|
366
375
|
missing_index = False
|
367
376
|
binary_file = False
|
368
377
|
current_hunk_header = ()
|
@@ -437,15 +446,6 @@ def fix_patch(patch_lines, original, remove_binary=False, fuzzy=False, add_newli
|
|
437
446
|
current_path = Path(current_file).absolute()
|
438
447
|
offset = 0
|
439
448
|
last_hunk = 0
|
440
|
-
if not Path.exists(current_path):
|
441
|
-
# this is meant to handle cases where the source file
|
442
|
-
# doesn't exist (e.g., when applying a patch that renames
|
443
|
-
# a file created earlier in the same patch)
|
444
|
-
# TODO: but really, does that ever happen???
|
445
|
-
fixed_lines.append(normalize_line(line))
|
446
|
-
look_for_rename = True
|
447
|
-
file_loaded = False
|
448
|
-
continue
|
449
449
|
if not current_path.is_file():
|
450
450
|
raise IsADirectoryError(f"Rename from header points to a directory, not a file: {current_file}")
|
451
451
|
if dir_mode or current_path == original_path:
|
@@ -462,7 +462,7 @@ def fix_patch(patch_lines, original, remove_binary=False, fuzzy=False, add_newli
|
|
462
462
|
last_index = i - 2
|
463
463
|
else:
|
464
464
|
raise NotImplementedError("Missing `rename from` header not yet supported.")
|
465
|
-
if not
|
465
|
+
if not file_loaded:
|
466
466
|
# if we're not looking for a rename but encounter "rename to",
|
467
467
|
# this indicates a malformed patch - log warning but continue
|
468
468
|
warnings.warn(
|
@@ -632,6 +632,8 @@ def fix_patch(patch_lines, original, remove_binary=False, fuzzy=False, add_newli
|
|
632
632
|
offset,
|
633
633
|
last_hunk
|
634
634
|
) = capture_hunk(current_hunk, original_lines, offset, last_hunk, current_hunk_header, fuzzy=fuzzy)
|
635
|
+
except EmptyHunk:
|
636
|
+
return fixed_lines
|
635
637
|
except (MissingHunkError, OutOfOrderHunk) as e:
|
636
638
|
e.add_file(current_file)
|
637
639
|
raise e
|
@@ -669,5 +671,4 @@ def main():
|
|
669
671
|
|
670
672
|
|
671
673
|
if __name__ == "__main__":
|
672
|
-
main()
|
673
|
-
|
674
|
+
main()
|
@@ -0,0 +1,10 @@
|
|
1
|
+
patch_fixer/__init__.py,sha256=n5DDMr4jbO3epK3ybBvjDyRddTWlWamN6ao5BC7xHFo,65
|
2
|
+
patch_fixer/cli.py,sha256=4zy02FsVrUrcQzsBwQ58PVfJXoG4OsOYKpk2JXGw1cY,3841
|
3
|
+
patch_fixer/patch_fixer.py,sha256=OuJkwhOq2Q9zcotxIRlT1kBZaD76JCxY5VCMrcSzWnA,28084
|
4
|
+
patch_fixer/split.py,sha256=l0rHM6-ZBuB9Iv6Ng6rxqZH5eKfvk2t87j__nDu67kM,3869
|
5
|
+
patch_fixer-0.4.1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
6
|
+
patch_fixer-0.4.1.dist-info/METADATA,sha256=4O0lHxiYNuta3IjGfLadnqwITFnJHD-gQVvb-lyXGos,4907
|
7
|
+
patch_fixer-0.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
8
|
+
patch_fixer-0.4.1.dist-info/entry_points.txt,sha256=ftc6dP6B1zJouSPeCCJLZtx-EEGVSrNEwy4YhtnEoxA,53
|
9
|
+
patch_fixer-0.4.1.dist-info/top_level.txt,sha256=yyp3KjFgExJsrFsS9ZBCnkhb05xg8hPYhB7ncdpTOv0,12
|
10
|
+
patch_fixer-0.4.1.dist-info/RECORD,,
|
@@ -1,10 +0,0 @@
|
|
1
|
-
patch_fixer/__init__.py,sha256=n5DDMr4jbO3epK3ybBvjDyRddTWlWamN6ao5BC7xHFo,65
|
2
|
-
patch_fixer/cli.py,sha256=4zy02FsVrUrcQzsBwQ58PVfJXoG4OsOYKpk2JXGw1cY,3841
|
3
|
-
patch_fixer/patch_fixer.py,sha256=YaArYeni8rFfFqILlytW7Bo9xz14SlsWGVjzHR_QqdI,28451
|
4
|
-
patch_fixer/split.py,sha256=l0rHM6-ZBuB9Iv6Ng6rxqZH5eKfvk2t87j__nDu67kM,3869
|
5
|
-
patch_fixer-0.4.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
6
|
-
patch_fixer-0.4.0.dist-info/METADATA,sha256=IAp5OWD110pKDSyzkDaj8rbLuX307Oo_CjJ93Ti-4-s,4907
|
7
|
-
patch_fixer-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
8
|
-
patch_fixer-0.4.0.dist-info/entry_points.txt,sha256=ftc6dP6B1zJouSPeCCJLZtx-EEGVSrNEwy4YhtnEoxA,53
|
9
|
-
patch_fixer-0.4.0.dist-info/top_level.txt,sha256=yyp3KjFgExJsrFsS9ZBCnkhb05xg8hPYhB7ncdpTOv0,12
|
10
|
-
patch_fixer-0.4.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|