codetool-shell 0.1.1__py3-none-win_amd64.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.
- codetool_shell/__init__.py +11 -0
- codetool_shell/api.py +59 -0
- codetool_shell/bin/windows-x86_64/codetool-shell-rust.exe +0 -0
- codetool_shell/filters/__init__.py +14 -0
- codetool_shell/filters/build_compiler/__init__.py +7 -0
- codetool_shell/filters/build_compiler/detector.py +412 -0
- codetool_shell/filters/build_compiler/reducer.py +166 -0
- codetool_shell/filters/build_compiler/summary.py +617 -0
- codetool_shell/filters/ci_job_log/__init__.py +7 -0
- codetool_shell/filters/ci_job_log/detector.py +64 -0
- codetool_shell/filters/ci_job_log/reducer.py +99 -0
- codetool_shell/filters/ci_job_log/summary.py +243 -0
- codetool_shell/filters/diff/__init__.py +7 -0
- codetool_shell/filters/diff/detector.py +136 -0
- codetool_shell/filters/diff/reducer.py +308 -0
- codetool_shell/filters/generic_log/__init__.py +7 -0
- codetool_shell/filters/generic_log/detector.py +175 -0
- codetool_shell/filters/generic_log/reducer.py +99 -0
- codetool_shell/filters/generic_log/summary.py +161 -0
- codetool_shell/filters/git.py +514 -0
- codetool_shell/filters/html_cleanup/__init__.py +7 -0
- codetool_shell/filters/html_cleanup/detector.py +136 -0
- codetool_shell/filters/html_cleanup/reducer.py +27 -0
- codetool_shell/filters/html_cleanup/summary.py +422 -0
- codetool_shell/filters/json_payload/__init__.py +7 -0
- codetool_shell/filters/json_payload/detector.py +62 -0
- codetool_shell/filters/json_payload/reducer.py +81 -0
- codetool_shell/filters/json_payload/summary.py +233 -0
- codetool_shell/filters/listing/__init__.py +7 -0
- codetool_shell/filters/listing/detector.py +294 -0
- codetool_shell/filters/listing/reducer.py +30 -0
- codetool_shell/filters/log_template/__init__.py +7 -0
- codetool_shell/filters/log_template/constants.py +76 -0
- codetool_shell/filters/log_template/detector.py +331 -0
- codetool_shell/filters/log_template/reducer.py +78 -0
- codetool_shell/filters/log_template/template.py +280 -0
- codetool_shell/filters/log_template/types.py +21 -0
- codetool_shell/filters/opaque_payload/__init__.py +7 -0
- codetool_shell/filters/opaque_payload/detector.py +563 -0
- codetool_shell/filters/opaque_payload/reducer.py +142 -0
- codetool_shell/filters/opaque_payload/summary.py +61 -0
- codetool_shell/filters/package_manager/__init__.py +7 -0
- codetool_shell/filters/package_manager/detector.py +220 -0
- codetool_shell/filters/package_manager/reducer.py +110 -0
- codetool_shell/filters/package_manager/summary.py +172 -0
- codetool_shell/filters/pipeline.py +65 -0
- codetool_shell/filters/rg.py +250 -0
- codetool_shell/filters/system_output/__init__.py +7 -0
- codetool_shell/filters/system_output/detector.py +600 -0
- codetool_shell/filters/system_output/reducer.py +331 -0
- codetool_shell/filters/system_output/summary.py +164 -0
- codetool_shell/filters/table/__init__.py +7 -0
- codetool_shell/filters/table/detector.py +244 -0
- codetool_shell/filters/table/reducer.py +57 -0
- codetool_shell/filters/table/summary.py +37 -0
- codetool_shell/filters/test_runner/__init__.py +7 -0
- codetool_shell/filters/test_runner/ansi.py +80 -0
- codetool_shell/filters/test_runner/detector.py +409 -0
- codetool_shell/filters/test_runner/reducer.py +288 -0
- codetool_shell/filters/test_runner/summary.py +449 -0
- codetool_shell/filters/text.py +38 -0
- codetool_shell/filters/traceback/__init__.py +7 -0
- codetool_shell/filters/traceback/detector.py +209 -0
- codetool_shell/filters/traceback/reducer.py +141 -0
- codetool_shell/filters/traceback/summary.py +122 -0
- codetool_shell/filters/tree.py +59 -0
- codetool_shell/py.typed +0 -0
- codetool_shell/python_backend.py +38 -0
- codetool_shell/rust_backend.py +254 -0
- codetool_shell-0.1.1.dist-info/METADATA +152 -0
- codetool_shell-0.1.1.dist-info/RECORD +72 -0
- codetool_shell-0.1.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Conservative reducer for CI/job log output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..text import join_preserving_final_newline, score, split_preserving_final_newline
|
|
6
|
+
from .detector import CiLogSignal, detect_ci_job_log
|
|
7
|
+
from .summary import (
|
|
8
|
+
extract_group_step,
|
|
9
|
+
is_group_end,
|
|
10
|
+
is_important_line,
|
|
11
|
+
normalize_selected_line,
|
|
12
|
+
parse_ci_line,
|
|
13
|
+
step_label_for_line,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def compress_ci_job_log_output(text: str) -> str:
|
|
18
|
+
"""Compress GitHub Actions / gh-run style job logs."""
|
|
19
|
+
|
|
20
|
+
lines, final_newline = split_preserving_final_newline(text)
|
|
21
|
+
signal = detect_ci_job_log(lines)
|
|
22
|
+
if signal is None:
|
|
23
|
+
return text
|
|
24
|
+
|
|
25
|
+
candidate_lines = _reduce_lines(lines, signal)
|
|
26
|
+
if len(candidate_lines) < 2:
|
|
27
|
+
return text
|
|
28
|
+
|
|
29
|
+
candidate = join_preserving_final_newline(candidate_lines, final_newline)
|
|
30
|
+
if score(candidate) < score(text):
|
|
31
|
+
return candidate
|
|
32
|
+
return text
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _reduce_lines(lines: list[str], signal: CiLogSignal) -> list[str]:
|
|
36
|
+
outcome = "failure" if signal.failed else "warning" if signal.warning else "success"
|
|
37
|
+
selected = [f"ci log: github-actions {outcome}"]
|
|
38
|
+
current_step: str | None = None
|
|
39
|
+
in_group = False
|
|
40
|
+
omitted = 0
|
|
41
|
+
|
|
42
|
+
def flush_omitted() -> None:
|
|
43
|
+
nonlocal omitted
|
|
44
|
+
if omitted:
|
|
45
|
+
plural = "" if omitted == 1 else "s"
|
|
46
|
+
selected.append(f"… {omitted} noisy line{plural} omitted")
|
|
47
|
+
omitted = 0
|
|
48
|
+
|
|
49
|
+
def start_step(step: str) -> None:
|
|
50
|
+
nonlocal current_step
|
|
51
|
+
clean_step = step.strip()
|
|
52
|
+
if not clean_step or clean_step == current_step:
|
|
53
|
+
return
|
|
54
|
+
flush_omitted()
|
|
55
|
+
selected.append(f"step: {clean_step}")
|
|
56
|
+
current_step = clean_step
|
|
57
|
+
|
|
58
|
+
for raw_line in lines:
|
|
59
|
+
parsed = parse_ci_line(raw_line)
|
|
60
|
+
message = parsed.message.strip()
|
|
61
|
+
if not message:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
group_step = extract_group_step(message)
|
|
65
|
+
if group_step is not None:
|
|
66
|
+
start_step(group_step)
|
|
67
|
+
in_group = True
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
prefixed_step = step_label_for_line(parsed)
|
|
71
|
+
if prefixed_step is not None and not in_group:
|
|
72
|
+
start_step(prefixed_step)
|
|
73
|
+
if prefixed_step == message:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
if is_group_end(message):
|
|
77
|
+
in_group = False
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
if is_important_line(message):
|
|
81
|
+
flush_omitted()
|
|
82
|
+
selected.append(normalize_selected_line(message))
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
omitted += 1
|
|
86
|
+
|
|
87
|
+
flush_omitted()
|
|
88
|
+
return _drop_blank_and_adjacent_duplicates(selected)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _drop_blank_and_adjacent_duplicates(lines: list[str]) -> list[str]:
|
|
92
|
+
output: list[str] = []
|
|
93
|
+
for line in lines:
|
|
94
|
+
if not line:
|
|
95
|
+
continue
|
|
96
|
+
if output and output[-1] == line:
|
|
97
|
+
continue
|
|
98
|
+
output.append(line)
|
|
99
|
+
return output
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Line parsing and classification for CI/job logs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class CiLine:
|
|
11
|
+
"""A parsed CI log line with noisy runner prefix removed."""
|
|
12
|
+
|
|
13
|
+
message: str
|
|
14
|
+
job: str | None
|
|
15
|
+
step: str | None
|
|
16
|
+
had_ci_prefix: bool
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_ci_line(line: str) -> CiLine:
|
|
20
|
+
"""Return CI prefix metadata and the visible log message."""
|
|
21
|
+
|
|
22
|
+
tab_parts = line.split("\t", 3)
|
|
23
|
+
if len(tab_parts) == 4 and _is_github_timestamp(tab_parts[2].strip()):
|
|
24
|
+
return CiLine(
|
|
25
|
+
message=tab_parts[3].strip(),
|
|
26
|
+
job=tab_parts[0].strip() or None,
|
|
27
|
+
step=tab_parts[1].strip() or None,
|
|
28
|
+
had_ci_prefix=True,
|
|
29
|
+
)
|
|
30
|
+
if len(tab_parts) == 3:
|
|
31
|
+
timestamp, separator, rest = tab_parts[2].strip().partition(" ")
|
|
32
|
+
if separator and _is_github_timestamp(timestamp):
|
|
33
|
+
return CiLine(
|
|
34
|
+
message=rest.strip(),
|
|
35
|
+
job=tab_parts[0].strip() or None,
|
|
36
|
+
step=tab_parts[1].strip() or None,
|
|
37
|
+
had_ci_prefix=True,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
stripped = line.lstrip()
|
|
41
|
+
first, separator, rest = stripped.partition(" ")
|
|
42
|
+
if separator and _is_github_timestamp(first):
|
|
43
|
+
return CiLine(
|
|
44
|
+
message=rest.strip(),
|
|
45
|
+
job=None,
|
|
46
|
+
step=None,
|
|
47
|
+
had_ci_prefix=True,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return CiLine(message=line.strip(), job=None, step=None, had_ci_prefix=False)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def extract_group_step(message: str) -> str | None:
|
|
54
|
+
"""Return a GitHub Actions group title if ``message`` opens a group."""
|
|
55
|
+
|
|
56
|
+
stripped = message.strip()
|
|
57
|
+
for prefix in ("##[group]", "::group::"):
|
|
58
|
+
if stripped.startswith(prefix):
|
|
59
|
+
title = stripped[len(prefix) :].strip()
|
|
60
|
+
return title or None
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_group_end(message: str) -> bool:
|
|
65
|
+
"""Return true for a GitHub Actions group terminator."""
|
|
66
|
+
|
|
67
|
+
stripped = message.strip()
|
|
68
|
+
return stripped.startswith(("##[endgroup]", "::endgroup::"))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def step_label_for_line(parsed: CiLine) -> str | None:
|
|
72
|
+
"""Return a compact step label from parsed prefix data or a run marker."""
|
|
73
|
+
|
|
74
|
+
if parsed.step:
|
|
75
|
+
if parsed.job and parsed.job != parsed.step:
|
|
76
|
+
return f"{parsed.job} / {parsed.step}"
|
|
77
|
+
return parsed.step
|
|
78
|
+
|
|
79
|
+
message = parsed.message.strip()
|
|
80
|
+
if _is_step_start_message(message):
|
|
81
|
+
return message
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def is_actions_marker(message: str) -> bool:
|
|
86
|
+
"""Return true for GitHub Actions control/annotation markers."""
|
|
87
|
+
|
|
88
|
+
stripped = message.strip()
|
|
89
|
+
return stripped.startswith("##[") or stripped.startswith(
|
|
90
|
+
(
|
|
91
|
+
"::group::",
|
|
92
|
+
"::endgroup::",
|
|
93
|
+
"::error",
|
|
94
|
+
"::warning",
|
|
95
|
+
"::notice",
|
|
96
|
+
"::debug",
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def is_error_annotation(message: str) -> bool:
|
|
102
|
+
"""Return true for GitHub Actions error annotations."""
|
|
103
|
+
|
|
104
|
+
stripped = message.strip()
|
|
105
|
+
return stripped.startswith(("::error", "##[error]"))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def is_warning_annotation(message: str) -> bool:
|
|
109
|
+
"""Return true for GitHub Actions warning annotations."""
|
|
110
|
+
|
|
111
|
+
stripped = message.strip()
|
|
112
|
+
return stripped.startswith(("::warning", "##[warning]"))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def is_warning_line(message: str) -> bool:
|
|
116
|
+
"""Return true for warning lines worth preserving."""
|
|
117
|
+
|
|
118
|
+
stripped = message.strip()
|
|
119
|
+
return is_warning_annotation(stripped) or stripped.startswith(("Warning:", "WARNING:"))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def is_failure_line(message: str) -> bool:
|
|
123
|
+
"""Return true for failure/exit lines worth preserving."""
|
|
124
|
+
|
|
125
|
+
stripped = message.strip()
|
|
126
|
+
lower = stripped.lower()
|
|
127
|
+
return (
|
|
128
|
+
is_error_annotation(stripped)
|
|
129
|
+
or stripped.startswith(("Error:", "ERROR:", "Failed", "FAILED", "fatal:"))
|
|
130
|
+
or "process completed with exit code" in lower
|
|
131
|
+
or "failed with exit code" in lower
|
|
132
|
+
or "exit code 1" in lower
|
|
133
|
+
or "status: failure" in lower
|
|
134
|
+
or "result: failure" in lower
|
|
135
|
+
or "conclusion: failure" in lower
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def is_conclusion_line(message: str) -> bool:
|
|
140
|
+
"""Return true for final job/workflow status lines."""
|
|
141
|
+
|
|
142
|
+
lower = message.strip().lower()
|
|
143
|
+
return (
|
|
144
|
+
"workflow completed" in lower
|
|
145
|
+
or "job completed" in lower
|
|
146
|
+
or "completed with status" in lower
|
|
147
|
+
or "completed with result" in lower
|
|
148
|
+
or "completed successfully" in lower
|
|
149
|
+
or "conclusion:" in lower
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def is_reference_line(message: str) -> bool:
|
|
154
|
+
"""Return true for URLs and artifact/path references."""
|
|
155
|
+
|
|
156
|
+
stripped = message.strip()
|
|
157
|
+
lower = stripped.lower()
|
|
158
|
+
if "https://" in lower or "http://" in lower:
|
|
159
|
+
return True
|
|
160
|
+
if "artifact" in lower:
|
|
161
|
+
return True
|
|
162
|
+
if any(token in lower for token in _REFERENCE_TOKENS):
|
|
163
|
+
return True
|
|
164
|
+
return _looks_like_path_reference(stripped)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def is_path_location_line(message: str) -> bool:
|
|
168
|
+
"""Return true for source file locations like ``src/app.ts:12:3``."""
|
|
169
|
+
|
|
170
|
+
stripped = message.strip().removeprefix("at ").strip()
|
|
171
|
+
parts = stripped.split(":")
|
|
172
|
+
if len(parts) < 2:
|
|
173
|
+
return False
|
|
174
|
+
for index in range(1, len(parts)):
|
|
175
|
+
if parts[index].isdigit():
|
|
176
|
+
path = ":".join(parts[:index])
|
|
177
|
+
return _looks_like_path(path)
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def is_important_line(message: str) -> bool:
|
|
182
|
+
"""Return true when a CI log line must be preserved."""
|
|
183
|
+
|
|
184
|
+
stripped = message.strip()
|
|
185
|
+
return (
|
|
186
|
+
is_warning_line(stripped)
|
|
187
|
+
or is_failure_line(stripped)
|
|
188
|
+
or is_conclusion_line(stripped)
|
|
189
|
+
or is_reference_line(stripped)
|
|
190
|
+
or is_path_location_line(stripped)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def normalize_selected_line(message: str) -> str:
|
|
195
|
+
"""Normalize a selected CI log message."""
|
|
196
|
+
|
|
197
|
+
return message.strip()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _is_step_start_message(message: str) -> bool:
|
|
201
|
+
return message.startswith(("Run ", "Post ", "Complete job", "Set up job", "Prepare "))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _is_github_timestamp(value: str) -> bool:
|
|
205
|
+
return _GITHUB_TIMESTAMP_RE.match(value) is not None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _looks_like_path_reference(line: str) -> bool:
|
|
209
|
+
lower = line.lower()
|
|
210
|
+
if not any(suffix in lower for suffix in _REFERENCE_SUFFIXES):
|
|
211
|
+
return False
|
|
212
|
+
return "/" in line or "\\" in line
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _looks_like_path(path: str) -> bool:
|
|
216
|
+
if not path or path[0].isspace() or "://" in path:
|
|
217
|
+
return False
|
|
218
|
+
return "/" in path or "\\" in path or "." in path
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
_GITHUB_TIMESTAMP_RE = re.compile(
|
|
222
|
+
r"^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z$"
|
|
223
|
+
)
|
|
224
|
+
_REFERENCE_TOKENS = (
|
|
225
|
+
"test-results/",
|
|
226
|
+
"playwright-report/",
|
|
227
|
+
"coverage/",
|
|
228
|
+
"dist/",
|
|
229
|
+
"build/",
|
|
230
|
+
"logs/",
|
|
231
|
+
)
|
|
232
|
+
_REFERENCE_SUFFIXES = (
|
|
233
|
+
".zip",
|
|
234
|
+
".tar.gz",
|
|
235
|
+
".log",
|
|
236
|
+
".txt",
|
|
237
|
+
".html",
|
|
238
|
+
".xml",
|
|
239
|
+
".json",
|
|
240
|
+
".png",
|
|
241
|
+
".jpg",
|
|
242
|
+
".jpeg",
|
|
243
|
+
)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Conservative unified-diff block detection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class DiffBlock:
|
|
11
|
+
"""Line range for one high-confidence diff block."""
|
|
12
|
+
|
|
13
|
+
start: int
|
|
14
|
+
end: int
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_NORMAL_HUNK_RE = re.compile(
|
|
18
|
+
r"^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@(?: .*)?$"
|
|
19
|
+
)
|
|
20
|
+
_COMBINED_HUNK_RE = re.compile(
|
|
21
|
+
r"^(?P<ats>@{3,}) (?:-\d+(?:,\d+)? )+\+\d+(?:,\d+)? (?P=ats)(?: .*)?$"
|
|
22
|
+
)
|
|
23
|
+
_DIFF_HEADER_PREFIXES = ("diff --git ", "diff --cc ", "diff --combined ")
|
|
24
|
+
_DIFF_METADATA_PREFIXES = (
|
|
25
|
+
"new file mode ",
|
|
26
|
+
"deleted file mode ",
|
|
27
|
+
"old mode ",
|
|
28
|
+
"new mode ",
|
|
29
|
+
"similarity index ",
|
|
30
|
+
"dissimilarity index ",
|
|
31
|
+
"rename from ",
|
|
32
|
+
"rename to ",
|
|
33
|
+
"copy from ",
|
|
34
|
+
"copy to ",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def find_diff_blocks(lines: list[str]) -> tuple[DiffBlock, ...]:
|
|
39
|
+
"""Return high-confidence diff block ranges."""
|
|
40
|
+
|
|
41
|
+
blocks: list[DiffBlock] = []
|
|
42
|
+
i = 0
|
|
43
|
+
while i < len(lines):
|
|
44
|
+
kind = _block_start_kind(lines, i)
|
|
45
|
+
if kind is None:
|
|
46
|
+
i += 1
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
end = _find_block_end(lines, i, kind)
|
|
50
|
+
block = lines[i:end]
|
|
51
|
+
if _is_valid_diff_block(block, kind):
|
|
52
|
+
blocks.append(DiffBlock(i, end))
|
|
53
|
+
i = end
|
|
54
|
+
else:
|
|
55
|
+
i += 1
|
|
56
|
+
|
|
57
|
+
return tuple(blocks)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_hunk_header(line: str) -> bool:
|
|
61
|
+
"""Return whether ``line`` is a normal or combined hunk header."""
|
|
62
|
+
|
|
63
|
+
return _NORMAL_HUNK_RE.match(line) is not None or _COMBINED_HUNK_RE.match(line) is not None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def hunk_prefix_columns(header: str) -> int:
|
|
67
|
+
"""Return body prefix column count for a hunk header."""
|
|
68
|
+
|
|
69
|
+
if header.startswith("@@@"):
|
|
70
|
+
at_count = len(header) - len(header.lstrip("@"))
|
|
71
|
+
return max(2, at_count - 1)
|
|
72
|
+
return 1
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _block_start_kind(lines: list[str], index: int) -> str | None:
|
|
76
|
+
line = lines[index]
|
|
77
|
+
if line.startswith(_DIFF_HEADER_PREFIXES):
|
|
78
|
+
return "diff"
|
|
79
|
+
if line.startswith("Index: "):
|
|
80
|
+
return "svn"
|
|
81
|
+
if _has_unified_file_header_pair_at(lines, index):
|
|
82
|
+
return "standalone"
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _find_block_end(lines: list[str], start: int, kind: str) -> int:
|
|
87
|
+
i = start + 1
|
|
88
|
+
while i < len(lines):
|
|
89
|
+
if lines[i].startswith(_DIFF_HEADER_PREFIXES) or lines[i].startswith("Index: "):
|
|
90
|
+
return i
|
|
91
|
+
if kind == "diff" and _is_git_patch_preamble_line(lines[i]):
|
|
92
|
+
return i
|
|
93
|
+
if kind == "standalone" and _has_unified_file_header_pair_at(lines, i):
|
|
94
|
+
return i
|
|
95
|
+
i += 1
|
|
96
|
+
return len(lines)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _has_unified_file_header_pair_at(lines: list[str], index: int) -> bool:
|
|
100
|
+
return (
|
|
101
|
+
index + 2 < len(lines)
|
|
102
|
+
and lines[index].startswith("--- ")
|
|
103
|
+
and lines[index + 1].startswith("+++ ")
|
|
104
|
+
and is_hunk_header(lines[index + 2])
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _is_valid_diff_block(block: list[str], kind: str) -> bool:
|
|
109
|
+
has_hunk = any(is_hunk_header(line) for line in block)
|
|
110
|
+
has_file_headers = any(
|
|
111
|
+
block[index].startswith("--- ") and block[index + 1].startswith("+++ ")
|
|
112
|
+
for index in range(max(0, len(block) - 1))
|
|
113
|
+
)
|
|
114
|
+
has_binary = any(
|
|
115
|
+
line.startswith(("GIT binary patch", "Binary files ", "literal ", "delta "))
|
|
116
|
+
for line in block
|
|
117
|
+
)
|
|
118
|
+
has_metadata = any(line.startswith(_DIFF_METADATA_PREFIXES) for line in block)
|
|
119
|
+
|
|
120
|
+
if kind == "diff":
|
|
121
|
+
return has_hunk or has_binary or has_file_headers or has_metadata
|
|
122
|
+
return has_hunk and has_file_headers
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _is_git_patch_preamble_line(line: str) -> bool:
|
|
126
|
+
if line.startswith("commit "):
|
|
127
|
+
token = line.removeprefix("commit ").split(maxsplit=1)[0]
|
|
128
|
+
return _is_hex_prefix(token)
|
|
129
|
+
if line.startswith("From "):
|
|
130
|
+
token = line.removeprefix("From ").split(maxsplit=1)[0]
|
|
131
|
+
return _is_hex_prefix(token)
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _is_hex_prefix(token: str) -> bool:
|
|
136
|
+
return len(token) >= 7 and all(char in "0123456789abcdefABCDEF" for char in token)
|