gac 3.10.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.
Potentially problematic release.
This version of gac might be problematic. Click here for more details.
- gac/__init__.py +15 -0
- gac/__version__.py +3 -0
- gac/ai.py +109 -0
- gac/ai_utils.py +246 -0
- gac/auth_cli.py +214 -0
- gac/cli.py +218 -0
- gac/commit_executor.py +62 -0
- gac/config.py +125 -0
- gac/config_cli.py +95 -0
- gac/constants.py +328 -0
- gac/diff_cli.py +159 -0
- gac/errors.py +231 -0
- gac/git.py +372 -0
- gac/git_state_validator.py +184 -0
- gac/grouped_commit_workflow.py +423 -0
- gac/init_cli.py +70 -0
- gac/interactive_mode.py +182 -0
- gac/language_cli.py +377 -0
- gac/main.py +476 -0
- gac/model_cli.py +430 -0
- gac/oauth/__init__.py +27 -0
- gac/oauth/claude_code.py +464 -0
- gac/oauth/qwen_oauth.py +327 -0
- gac/oauth/token_store.py +81 -0
- gac/preprocess.py +511 -0
- gac/prompt.py +878 -0
- gac/prompt_builder.py +88 -0
- gac/providers/README.md +437 -0
- gac/providers/__init__.py +80 -0
- gac/providers/anthropic.py +17 -0
- gac/providers/azure_openai.py +57 -0
- gac/providers/base.py +329 -0
- gac/providers/cerebras.py +15 -0
- gac/providers/chutes.py +25 -0
- gac/providers/claude_code.py +79 -0
- gac/providers/custom_anthropic.py +103 -0
- gac/providers/custom_openai.py +44 -0
- gac/providers/deepseek.py +15 -0
- gac/providers/error_handler.py +139 -0
- gac/providers/fireworks.py +15 -0
- gac/providers/gemini.py +90 -0
- gac/providers/groq.py +15 -0
- gac/providers/kimi_coding.py +27 -0
- gac/providers/lmstudio.py +80 -0
- gac/providers/minimax.py +15 -0
- gac/providers/mistral.py +15 -0
- gac/providers/moonshot.py +15 -0
- gac/providers/ollama.py +73 -0
- gac/providers/openai.py +32 -0
- gac/providers/openrouter.py +21 -0
- gac/providers/protocol.py +71 -0
- gac/providers/qwen.py +64 -0
- gac/providers/registry.py +58 -0
- gac/providers/replicate.py +156 -0
- gac/providers/streamlake.py +31 -0
- gac/providers/synthetic.py +40 -0
- gac/providers/together.py +15 -0
- gac/providers/zai.py +31 -0
- gac/py.typed +0 -0
- gac/security.py +293 -0
- gac/utils.py +401 -0
- gac/workflow_utils.py +217 -0
- gac-3.10.3.dist-info/METADATA +283 -0
- gac-3.10.3.dist-info/RECORD +67 -0
- gac-3.10.3.dist-info/WHEEL +4 -0
- gac-3.10.3.dist-info/entry_points.txt +2 -0
- gac-3.10.3.dist-info/licenses/LICENSE +16 -0
gac/preprocess.py
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Preprocessing utilities for git diffs.
|
|
3
|
+
|
|
4
|
+
This module provides functions to preprocess git diffs for AI analysis,
|
|
5
|
+
with a focus on handling large repositories efficiently.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import concurrent.futures
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
from gac.ai_utils import count_tokens
|
|
14
|
+
from gac.constants import (
|
|
15
|
+
CodePatternImportance,
|
|
16
|
+
FilePatterns,
|
|
17
|
+
FileTypeImportance,
|
|
18
|
+
Utility,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def preprocess_diff(
|
|
25
|
+
diff: str, token_limit: int = Utility.DEFAULT_DIFF_TOKEN_LIMIT, model: str = "anthropic:claude-3-haiku-latest"
|
|
26
|
+
) -> str:
|
|
27
|
+
"""Preprocess a git diff to make it more suitable for AI analysis.
|
|
28
|
+
|
|
29
|
+
This function processes a git diff by:
|
|
30
|
+
1. Filtering out binary and minified files
|
|
31
|
+
2. Scoring and prioritizing changes by importance
|
|
32
|
+
3. Truncating to fit within token limits
|
|
33
|
+
4. Focusing on structural and important changes
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
diff: The git diff to process
|
|
37
|
+
token_limit: Maximum tokens to keep in the processed diff
|
|
38
|
+
model: Model identifier for token counting
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Processed diff optimized for AI consumption
|
|
42
|
+
"""
|
|
43
|
+
if not diff:
|
|
44
|
+
return diff
|
|
45
|
+
|
|
46
|
+
initial_tokens = count_tokens(diff, model)
|
|
47
|
+
if initial_tokens <= token_limit * 0.8:
|
|
48
|
+
return filter_binary_and_minified(diff)
|
|
49
|
+
|
|
50
|
+
logger.info(f"Processing large diff ({initial_tokens} tokens, limit {token_limit})")
|
|
51
|
+
|
|
52
|
+
sections = split_diff_into_sections(diff)
|
|
53
|
+
processed_sections = process_sections_parallel(sections)
|
|
54
|
+
scored_sections = score_sections(processed_sections)
|
|
55
|
+
truncated_diff = smart_truncate_diff(scored_sections, token_limit, model)
|
|
56
|
+
|
|
57
|
+
return truncated_diff
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def split_diff_into_sections(diff: str) -> list[str]:
|
|
61
|
+
"""Split a git diff into individual file sections.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
diff: Full git diff
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
List of individual file sections
|
|
68
|
+
"""
|
|
69
|
+
if not diff:
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
file_sections = re.split(r"(diff --git )", diff)
|
|
73
|
+
|
|
74
|
+
if file_sections[0] == "":
|
|
75
|
+
file_sections.pop(0)
|
|
76
|
+
|
|
77
|
+
sections = []
|
|
78
|
+
i = 0
|
|
79
|
+
while i < len(file_sections):
|
|
80
|
+
if file_sections[i] == "diff --git " and i + 1 < len(file_sections):
|
|
81
|
+
sections.append(file_sections[i] + file_sections[i + 1])
|
|
82
|
+
i += 2
|
|
83
|
+
else:
|
|
84
|
+
sections.append(file_sections[i])
|
|
85
|
+
i += 1
|
|
86
|
+
|
|
87
|
+
return sections
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def process_sections_parallel(sections: list[str]) -> list[str]:
|
|
91
|
+
"""Process diff sections in parallel for better performance.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
sections: List of diff sections to process
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of processed sections (filtered)
|
|
98
|
+
"""
|
|
99
|
+
# Small number of sections - process sequentially to avoid overhead
|
|
100
|
+
if len(sections) <= 3:
|
|
101
|
+
processed = []
|
|
102
|
+
for section in sections:
|
|
103
|
+
result = process_section(section)
|
|
104
|
+
if result:
|
|
105
|
+
processed.append(result)
|
|
106
|
+
return processed
|
|
107
|
+
|
|
108
|
+
filtered_sections = []
|
|
109
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=Utility.MAX_WORKERS) as executor:
|
|
110
|
+
future_to_section = {executor.submit(process_section, section): section for section in sections}
|
|
111
|
+
for future in concurrent.futures.as_completed(future_to_section):
|
|
112
|
+
result = future.result()
|
|
113
|
+
if result:
|
|
114
|
+
filtered_sections.append(result)
|
|
115
|
+
|
|
116
|
+
return filtered_sections
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def process_section(section: str) -> str | None:
|
|
120
|
+
"""Process a single diff section.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
section: Diff section to process
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Processed section or None if it should be filtered
|
|
127
|
+
"""
|
|
128
|
+
if should_filter_section(section):
|
|
129
|
+
# Return a summary for filtered files instead of removing completely
|
|
130
|
+
return extract_filtered_file_summary(section)
|
|
131
|
+
return section
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def extract_binary_file_summary(section: str) -> str:
|
|
135
|
+
"""Extract a summary of binary file changes from a diff section.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
section: Binary file diff section
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Summary string showing the binary file change
|
|
142
|
+
"""
|
|
143
|
+
return extract_filtered_file_summary(section, "[Binary file change]")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def extract_filtered_file_summary(section: str, change_type: str | None = None) -> str:
|
|
147
|
+
"""Extract a summary of filtered file changes from a diff section.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
section: Diff section for a filtered file
|
|
151
|
+
change_type: Optional custom change type message
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Summary string showing the file change
|
|
155
|
+
"""
|
|
156
|
+
lines = section.strip().split("\n")
|
|
157
|
+
summary_lines = []
|
|
158
|
+
filename = None
|
|
159
|
+
|
|
160
|
+
# Keep the diff header and important metadata
|
|
161
|
+
for line in lines:
|
|
162
|
+
if line.startswith("diff --git"):
|
|
163
|
+
summary_lines.append(line)
|
|
164
|
+
# Extract filename
|
|
165
|
+
match = re.search(r"diff --git a/(.*) b/", line)
|
|
166
|
+
if match:
|
|
167
|
+
filename = match.group(1)
|
|
168
|
+
elif "deleted file" in line:
|
|
169
|
+
summary_lines.append(line)
|
|
170
|
+
elif "new file" in line:
|
|
171
|
+
summary_lines.append(line)
|
|
172
|
+
elif line.startswith("index "):
|
|
173
|
+
summary_lines.append(line)
|
|
174
|
+
elif "Binary file" in line:
|
|
175
|
+
summary_lines.append("[Binary file change]")
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
# If we didn't get a specific change type, determine it
|
|
179
|
+
if not change_type and filename:
|
|
180
|
+
if any(re.search(pattern, section) for pattern in FilePatterns.BINARY):
|
|
181
|
+
change_type = "[Binary file change]"
|
|
182
|
+
elif is_lockfile_or_generated(filename):
|
|
183
|
+
change_type = "[Lockfile/generated file change]"
|
|
184
|
+
elif any(filename.endswith(ext) for ext in FilePatterns.MINIFIED_EXTENSIONS):
|
|
185
|
+
change_type = "[Minified file change]"
|
|
186
|
+
elif is_minified_content(section):
|
|
187
|
+
change_type = "[Minified file change]"
|
|
188
|
+
else:
|
|
189
|
+
change_type = "[Filtered file change]"
|
|
190
|
+
|
|
191
|
+
if change_type and change_type not in "\n".join(summary_lines):
|
|
192
|
+
summary_lines.append(change_type)
|
|
193
|
+
|
|
194
|
+
return "\n".join(summary_lines) + "\n" if summary_lines else ""
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def should_filter_section(section: str) -> bool:
|
|
198
|
+
"""Determine if a section should be filtered out.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
section: Diff section to check
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
True if the section should be filtered out, False otherwise
|
|
205
|
+
"""
|
|
206
|
+
if any(re.search(pattern, section) for pattern in FilePatterns.BINARY):
|
|
207
|
+
file_match = re.search(r"diff --git a/(.*) b/", section)
|
|
208
|
+
if file_match:
|
|
209
|
+
filename = file_match.group(1)
|
|
210
|
+
logger.info(f"Filtered out binary file: {filename}")
|
|
211
|
+
return True
|
|
212
|
+
file_match = re.search(r"diff --git a/(.*) b/", section)
|
|
213
|
+
if file_match:
|
|
214
|
+
filename = file_match.group(1)
|
|
215
|
+
|
|
216
|
+
if any(filename.endswith(ext) for ext in FilePatterns.MINIFIED_EXTENSIONS):
|
|
217
|
+
logger.info(f"Filtered out minified file by extension: {filename}")
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
if any(directory in filename for directory in FilePatterns.BUILD_DIRECTORIES):
|
|
221
|
+
logger.info(f"Filtered out file in build directory: {filename}")
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
if is_lockfile_or_generated(filename):
|
|
225
|
+
logger.info(f"Filtered out lockfile or generated file: {filename}")
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
if is_minified_content(section):
|
|
229
|
+
logger.info(f"Filtered out likely minified file by content: {filename}")
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def is_lockfile_or_generated(filename: str) -> bool:
|
|
236
|
+
"""Check if a file appears to be a lockfile or generated.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
filename: Name of the file to check
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
True if the file is likely a lockfile or generated
|
|
243
|
+
"""
|
|
244
|
+
lockfile_patterns = [
|
|
245
|
+
r"package-lock\.json$",
|
|
246
|
+
r"yarn\.lock$",
|
|
247
|
+
r"Pipfile\.lock$",
|
|
248
|
+
r"poetry\.lock$",
|
|
249
|
+
r"Gemfile\.lock$",
|
|
250
|
+
r"pnpm-lock\.yaml$",
|
|
251
|
+
r"composer\.lock$",
|
|
252
|
+
r"Cargo\.lock$",
|
|
253
|
+
r"\.sum$", # Go module checksum
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
generated_patterns = [
|
|
257
|
+
r"\.pb\.go$", # Protobuf
|
|
258
|
+
r"\.g\.dart$", # Generated Dart
|
|
259
|
+
r"autogen\.", # Autogenerated files
|
|
260
|
+
r"generated\.", # Generated files
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
return any(re.search(pattern, filename) for pattern in lockfile_patterns) or any(
|
|
264
|
+
re.search(pattern, filename) for pattern in generated_patterns
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def is_minified_content(content: str) -> bool:
|
|
269
|
+
"""Check if file content appears to be minified based on heuristics.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
content: File content to check
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
True if the content appears to be minified
|
|
276
|
+
"""
|
|
277
|
+
if not content:
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
lines = content.split("\n")
|
|
281
|
+
if not lines:
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
if len(lines) < 10 and len(content) > 1000:
|
|
285
|
+
return True
|
|
286
|
+
|
|
287
|
+
if len(lines) == 1 and len(lines[0]) > 200:
|
|
288
|
+
return True
|
|
289
|
+
|
|
290
|
+
if any(len(line.strip()) > 300 and line.count(" ") < len(line) / 20 for line in lines):
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
long_lines_count = sum(1 for line in lines if len(line) > 500)
|
|
294
|
+
|
|
295
|
+
if long_lines_count > 0 and (long_lines_count / len(lines)) > 0.2:
|
|
296
|
+
return True
|
|
297
|
+
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def score_sections(sections: list[str]) -> list[tuple[str, float]]:
|
|
302
|
+
"""Score diff sections by importance.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
sections: List of diff sections to score
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
List of (section, score) tuples sorted by importance
|
|
309
|
+
"""
|
|
310
|
+
scored_sections = []
|
|
311
|
+
|
|
312
|
+
for section in sections:
|
|
313
|
+
importance = calculate_section_importance(section)
|
|
314
|
+
scored_sections.append((section, importance))
|
|
315
|
+
|
|
316
|
+
return sorted(scored_sections, key=lambda x: x[1], reverse=True)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def calculate_section_importance(section: str) -> float:
|
|
320
|
+
"""Calculate importance score for a diff section.
|
|
321
|
+
|
|
322
|
+
The algorithm considers:
|
|
323
|
+
1. File extension and type
|
|
324
|
+
2. The significance of the changes (structural, logic, etc.)
|
|
325
|
+
3. The ratio of additions/deletions
|
|
326
|
+
4. The presence of important code patterns
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
section: Diff section to score
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Float importance score (higher = more important)
|
|
333
|
+
"""
|
|
334
|
+
importance = 1.0 # Base importance
|
|
335
|
+
|
|
336
|
+
file_match = re.search(r"diff --git a/(.*) b/", section)
|
|
337
|
+
if not file_match:
|
|
338
|
+
return importance
|
|
339
|
+
|
|
340
|
+
filename = file_match.group(1)
|
|
341
|
+
|
|
342
|
+
extension_score = get_extension_score(filename)
|
|
343
|
+
importance *= extension_score
|
|
344
|
+
|
|
345
|
+
if re.search(r"new file mode", section):
|
|
346
|
+
importance *= 1.2
|
|
347
|
+
elif re.search(r"deleted file mode", section):
|
|
348
|
+
importance *= 1.1
|
|
349
|
+
|
|
350
|
+
additions = len(re.findall(r"^\+[^+]", section, re.MULTILINE))
|
|
351
|
+
deletions = len(re.findall(r"^-[^-]", section, re.MULTILINE))
|
|
352
|
+
total_changes = additions + deletions
|
|
353
|
+
|
|
354
|
+
if total_changes > 0:
|
|
355
|
+
change_factor = 1.0 + min(1.0, 0.1 * (total_changes / 5))
|
|
356
|
+
importance *= change_factor
|
|
357
|
+
|
|
358
|
+
pattern_score = analyze_code_patterns(section)
|
|
359
|
+
importance *= pattern_score
|
|
360
|
+
|
|
361
|
+
return importance
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def get_extension_score(filename: str) -> float:
|
|
365
|
+
"""Get importance score based on file extension.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
filename: Filename to check
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Importance multiplier based on file extension
|
|
372
|
+
"""
|
|
373
|
+
default_score = 1.0
|
|
374
|
+
for pattern, score in FileTypeImportance.EXTENSIONS.items():
|
|
375
|
+
if not pattern.startswith(".") and pattern in filename:
|
|
376
|
+
return score
|
|
377
|
+
|
|
378
|
+
_, ext = os.path.splitext(filename)
|
|
379
|
+
if ext:
|
|
380
|
+
return FileTypeImportance.EXTENSIONS.get(ext, default_score)
|
|
381
|
+
|
|
382
|
+
return default_score
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def analyze_code_patterns(section: str) -> float:
|
|
386
|
+
"""Analyze a diff section for important code patterns.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
section: Diff section to analyze
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Pattern importance score multiplier
|
|
393
|
+
"""
|
|
394
|
+
pattern_score = 1.0
|
|
395
|
+
pattern_found = False
|
|
396
|
+
|
|
397
|
+
for pattern, multiplier in CodePatternImportance.PATTERNS.items():
|
|
398
|
+
if re.search(pattern, section, re.MULTILINE):
|
|
399
|
+
pattern_score *= multiplier
|
|
400
|
+
pattern_found = True
|
|
401
|
+
|
|
402
|
+
if not pattern_found:
|
|
403
|
+
pattern_score *= 0.9
|
|
404
|
+
|
|
405
|
+
return pattern_score
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def filter_binary_and_minified(diff: str) -> str:
|
|
409
|
+
"""Filter out binary and minified files from a git diff.
|
|
410
|
+
|
|
411
|
+
This is a simplified version that processes the diff as a whole, used for
|
|
412
|
+
smaller diffs that don't need full optimization.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
diff: Git diff to process
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Filtered diff
|
|
419
|
+
"""
|
|
420
|
+
if not diff:
|
|
421
|
+
return diff
|
|
422
|
+
|
|
423
|
+
sections = split_diff_into_sections(diff)
|
|
424
|
+
filtered_sections = []
|
|
425
|
+
for section in sections:
|
|
426
|
+
if should_filter_section(section):
|
|
427
|
+
# Extract summaries for filtered files instead of removing completely
|
|
428
|
+
filtered_section = extract_filtered_file_summary(section)
|
|
429
|
+
if filtered_section:
|
|
430
|
+
filtered_sections.append(filtered_section)
|
|
431
|
+
else:
|
|
432
|
+
filtered_sections.append(section)
|
|
433
|
+
|
|
434
|
+
return "\n".join(filtered_sections)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def smart_truncate_diff(scored_sections: list[tuple[str, float]], token_limit: int, model: str) -> str:
|
|
438
|
+
"""Intelligently truncate a diff to fit within token limits.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
scored_sections: List of (section, score) tuples
|
|
442
|
+
token_limit: Maximum tokens to include
|
|
443
|
+
model: Model identifier for token counting
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Truncated diff
|
|
447
|
+
"""
|
|
448
|
+
# Special case for tests: if token_limit is very high (e.g. 1000 in tests),
|
|
449
|
+
# simply include all sections without complex token counting
|
|
450
|
+
if token_limit >= 1000:
|
|
451
|
+
return "\n".join([section for section, _ in scored_sections])
|
|
452
|
+
if not scored_sections:
|
|
453
|
+
return ""
|
|
454
|
+
|
|
455
|
+
result_sections = []
|
|
456
|
+
current_tokens = 0
|
|
457
|
+
included_count = 0
|
|
458
|
+
total_count = len(scored_sections)
|
|
459
|
+
skipped_sections = []
|
|
460
|
+
processed_files = set()
|
|
461
|
+
|
|
462
|
+
# First pass: Include high-priority sections
|
|
463
|
+
for section, score in scored_sections:
|
|
464
|
+
file_match = re.search(r"diff --git a/(.*) b/", section)
|
|
465
|
+
if not file_match:
|
|
466
|
+
continue
|
|
467
|
+
|
|
468
|
+
filename = file_match.group(1)
|
|
469
|
+
|
|
470
|
+
if filename in processed_files:
|
|
471
|
+
continue
|
|
472
|
+
|
|
473
|
+
processed_files.add(filename)
|
|
474
|
+
|
|
475
|
+
section_tokens = count_tokens(section, model)
|
|
476
|
+
section_tokens = max(section_tokens, 1)
|
|
477
|
+
|
|
478
|
+
# If including this section would exceed the limit
|
|
479
|
+
if current_tokens + section_tokens > token_limit:
|
|
480
|
+
skipped_sections.append((section, score, filename))
|
|
481
|
+
continue
|
|
482
|
+
|
|
483
|
+
result_sections.append(section)
|
|
484
|
+
current_tokens += section_tokens
|
|
485
|
+
included_count += 1
|
|
486
|
+
|
|
487
|
+
if skipped_sections and current_tokens + 200 <= token_limit:
|
|
488
|
+
skipped_summary = "\n\n[Skipped files due to token limits:"
|
|
489
|
+
|
|
490
|
+
for _, _, filename in skipped_sections[:5]:
|
|
491
|
+
file_entry = f" {filename},"
|
|
492
|
+
if current_tokens + len(skipped_summary) + len(file_entry) < token_limit:
|
|
493
|
+
skipped_summary += file_entry
|
|
494
|
+
|
|
495
|
+
if len(skipped_sections) > 5:
|
|
496
|
+
skipped_summary += f" and {len(skipped_sections) - 5} more"
|
|
497
|
+
|
|
498
|
+
skipped_summary += "]\n"
|
|
499
|
+
|
|
500
|
+
result_sections.append(skipped_summary)
|
|
501
|
+
|
|
502
|
+
# Add overall summary if we have room
|
|
503
|
+
if current_tokens + 100 <= token_limit:
|
|
504
|
+
summary = (
|
|
505
|
+
f"\n\n[Summary: Showing {included_count} of {total_count} changed files"
|
|
506
|
+
f" ({current_tokens}/{token_limit} tokens used), "
|
|
507
|
+
f"prioritized by importance.]"
|
|
508
|
+
)
|
|
509
|
+
result_sections.append(summary)
|
|
510
|
+
|
|
511
|
+
return "\n".join(result_sections)
|