@pilllesss/yorn 1.0.182 โ 1.0.183
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 @pilllesss/yorn might be problematic. Click here for more details.
- package/README.md +1 -1
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/skills/code-review/LICENSE +21 -0
- package/dist/skills/code-review/SKILL.md +233 -0
- package/dist/skills/code-review/assets/pr-review-template.md +137 -0
- package/dist/skills/code-review/assets/review-checklist.md +123 -0
- package/dist/skills/code-review/reference/angular.md +768 -0
- package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
- package/dist/skills/code-review/reference/c.md +890 -0
- package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
- package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
- package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
- package/dist/skills/code-review/reference/cpp.md +893 -0
- package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
- package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
- package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
- package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
- package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
- package/dist/skills/code-review/reference/csharp.md +525 -0
- package/dist/skills/code-review/reference/css-less-sass.md +661 -0
- package/dist/skills/code-review/reference/dart.md +670 -0
- package/dist/skills/code-review/reference/django.md +985 -0
- package/dist/skills/code-review/reference/fastapi.md +580 -0
- package/dist/skills/code-review/reference/go.md +993 -0
- package/dist/skills/code-review/reference/java.md +409 -0
- package/dist/skills/code-review/reference/java8.md +586 -0
- package/dist/skills/code-review/reference/kotlin.md +1018 -0
- package/dist/skills/code-review/reference/nestjs.md +593 -0
- package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
- package/dist/skills/code-review/reference/php.md +684 -0
- package/dist/skills/code-review/reference/python.md +1073 -0
- package/dist/skills/code-review/reference/qt.md +757 -0
- package/dist/skills/code-review/reference/react.md +871 -0
- package/dist/skills/code-review/reference/ruby.md +964 -0
- package/dist/skills/code-review/reference/rust.md +846 -0
- package/dist/skills/code-review/reference/security-review-guide.md +494 -0
- package/dist/skills/code-review/reference/svelte.md +1064 -0
- package/dist/skills/code-review/reference/swift.md +936 -0
- package/dist/skills/code-review/reference/typescript.md +1016 -0
- package/dist/skills/code-review/reference/vue.md +924 -0
- package/dist/skills/code-review/reference/zig.md +440 -0
- package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
- package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
- package/dist/yorn.cjs +628 -628
- package/package.json +2 -2
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PR Analyzer - Analyze PR complexity and suggest review approach.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python pr-analyzer.py [--diff-file FILE] [--stats]
|
|
7
|
+
|
|
8
|
+
Or pipe diff directly:
|
|
9
|
+
git diff main...HEAD | python pr-analyzer.py
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import re
|
|
15
|
+
import argparse
|
|
16
|
+
from collections import defaultdict
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import List, Dict, Optional
|
|
19
|
+
|
|
20
|
+
RISK_NO_TESTS = "NO_TEST_CHANGES"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class FileStats:
|
|
25
|
+
"""Statistics for a single file."""
|
|
26
|
+
filename: str
|
|
27
|
+
additions: int = 0
|
|
28
|
+
deletions: int = 0
|
|
29
|
+
is_test: bool = False
|
|
30
|
+
is_config: bool = False
|
|
31
|
+
language: str = "unknown"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class PRAnalysis:
|
|
36
|
+
"""Complete PR analysis results."""
|
|
37
|
+
total_files: int
|
|
38
|
+
total_additions: int
|
|
39
|
+
total_deletions: int
|
|
40
|
+
files: List[FileStats]
|
|
41
|
+
complexity_score: float
|
|
42
|
+
size_category: str
|
|
43
|
+
estimated_review_time: int
|
|
44
|
+
risk_factors: List[str]
|
|
45
|
+
suggestions: List[str]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def detect_language(filename: str) -> str:
|
|
49
|
+
"""Detect programming language from filename."""
|
|
50
|
+
_, ext = os.path.splitext(filename)
|
|
51
|
+
extensions = {
|
|
52
|
+
'.py': 'Python',
|
|
53
|
+
'.js': 'JavaScript',
|
|
54
|
+
'.ts': 'TypeScript',
|
|
55
|
+
'.tsx': 'TypeScript/React',
|
|
56
|
+
'.jsx': 'JavaScript/React',
|
|
57
|
+
'.rs': 'Rust',
|
|
58
|
+
'.go': 'Go',
|
|
59
|
+
'.c': 'C',
|
|
60
|
+
'.h': 'C/C++',
|
|
61
|
+
'.cpp': 'C++',
|
|
62
|
+
'.hpp': 'C++',
|
|
63
|
+
'.cc': 'C++',
|
|
64
|
+
'.cxx': 'C++',
|
|
65
|
+
'.hh': 'C++',
|
|
66
|
+
'.hxx': 'C++',
|
|
67
|
+
'.java': 'Java',
|
|
68
|
+
'.kt': 'Kotlin',
|
|
69
|
+
'.swift': 'Swift',
|
|
70
|
+
'.rb': 'Ruby',
|
|
71
|
+
'.php': 'PHP',
|
|
72
|
+
'.cs': 'C#',
|
|
73
|
+
'.vue': 'Vue',
|
|
74
|
+
'.svelte': 'Svelte',
|
|
75
|
+
'.sql': 'SQL',
|
|
76
|
+
'.md': 'Markdown',
|
|
77
|
+
'.json': 'JSON',
|
|
78
|
+
'.yaml': 'YAML',
|
|
79
|
+
'.yml': 'YAML',
|
|
80
|
+
'.toml': 'TOML',
|
|
81
|
+
'.css': 'CSS',
|
|
82
|
+
'.scss': 'SCSS',
|
|
83
|
+
'.less': 'Less',
|
|
84
|
+
'.html': 'HTML',
|
|
85
|
+
'.zig': 'Zig',
|
|
86
|
+
'.ex': 'Elixir',
|
|
87
|
+
'.exs': 'Elixir',
|
|
88
|
+
'.erl': 'Erlang',
|
|
89
|
+
'.scala': 'Scala',
|
|
90
|
+
'.lua': 'Lua',
|
|
91
|
+
}
|
|
92
|
+
return extensions.get(ext.lower(), 'unknown')
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def is_test_file(filename: str) -> bool:
|
|
96
|
+
"""Check if file is a test file."""
|
|
97
|
+
test_patterns = [
|
|
98
|
+
r'(?:^|/)test_[^/]+\.py$', # Python: test_handler.py (anchored to path segment)
|
|
99
|
+
r'[^/]+_test\.', # *_test.<ext> โ Rust, Go, etc.
|
|
100
|
+
r'[^/]+\.test\.(js|ts|tsx|jsx)$', # JS/TS: handler.test.ts
|
|
101
|
+
r'[^/]+\.spec\.(js|ts|tsx|jsx)$', # JS/TS: handler.spec.ts
|
|
102
|
+
r'(?:^|/)tests?/', # tests/ or test/ directory
|
|
103
|
+
r'(?:^|/)__tests__/', # __tests__/ directory
|
|
104
|
+
r'(?:^|/)spec/', # spec/ directory (Ruby, etc.)
|
|
105
|
+
]
|
|
106
|
+
return any(re.search(p, filename) for p in test_patterns)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_config_file(filename: str) -> bool:
|
|
110
|
+
"""Check if file is a configuration file.
|
|
111
|
+
|
|
112
|
+
Uses explicit known-name matching to avoid flagging data files
|
|
113
|
+
like data.json, openapi.yaml, or swagger.json as config.
|
|
114
|
+
"""
|
|
115
|
+
basename = os.path.basename(filename)
|
|
116
|
+
|
|
117
|
+
# Env files (.env, .env.local, .env.production, โฆ)
|
|
118
|
+
if basename.startswith('.env'):
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
# Known config filenames (exact match)
|
|
122
|
+
known_config_names = {
|
|
123
|
+
'package.json', 'package-lock.json',
|
|
124
|
+
'tsconfig.json', 'jsconfig.json',
|
|
125
|
+
'babel.config.json', 'babel.config.js',
|
|
126
|
+
'webpack.config.js', 'webpack.config.ts',
|
|
127
|
+
'rollup.config.js', 'vite.config.ts', 'vite.config.js',
|
|
128
|
+
'.eslintrc.json', '.eslintrc.js', '.eslintrc.yml',
|
|
129
|
+
'.prettierrc.json', '.prettierrc.yml', '.prettierrc.js',
|
|
130
|
+
'jest.config.js', 'jest.config.ts',
|
|
131
|
+
'vitest.config.ts', 'vitest.config.js',
|
|
132
|
+
'tailwind.config.js', 'tailwind.config.ts',
|
|
133
|
+
'postcss.config.js',
|
|
134
|
+
'docker-compose.yml', 'docker-compose.yaml',
|
|
135
|
+
'Dockerfile',
|
|
136
|
+
'Makefile', 'CMakeLists.txt',
|
|
137
|
+
'pyproject.toml', 'poetry.toml', 'Pipfile',
|
|
138
|
+
'setup.cfg', 'setup.py', 'tox.ini',
|
|
139
|
+
'Cargo.toml', 'Cargo.lock',
|
|
140
|
+
'go.mod', 'go.sum',
|
|
141
|
+
'Gemfile', 'Gemfile.lock',
|
|
142
|
+
'composer.json', 'composer.lock',
|
|
143
|
+
'Podfile', 'Package.swift',
|
|
144
|
+
'.gitignore', '.gitattributes',
|
|
145
|
+
'gradle.properties', 'build.gradle', 'build.gradle.kts',
|
|
146
|
+
'settings.gradle', 'settings.gradle.kts',
|
|
147
|
+
}
|
|
148
|
+
if basename in known_config_names:
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
# Known config path patterns
|
|
152
|
+
config_path_patterns = [
|
|
153
|
+
r'\.github/workflows/[^/]+\.ya?ml$',
|
|
154
|
+
r'\.vscode/',
|
|
155
|
+
r'\.idea/',
|
|
156
|
+
]
|
|
157
|
+
if any(re.search(p, filename) for p in config_path_patterns):
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
# Files with "config" in the name (e.g. app.config.ts, database_config.yml)
|
|
161
|
+
if re.search(r'config\.', basename):
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
# Files under a config/ directory
|
|
165
|
+
if re.search(r'(?:^|/)config/', filename):
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def parse_diff(diff_content: str) -> List[FileStats]:
|
|
172
|
+
"""Parse git diff output and extract file statistics."""
|
|
173
|
+
files = []
|
|
174
|
+
current_file = None
|
|
175
|
+
|
|
176
|
+
for line in diff_content.split('\n'):
|
|
177
|
+
# New file header
|
|
178
|
+
if line.startswith('diff --git'):
|
|
179
|
+
if current_file:
|
|
180
|
+
files.append(current_file)
|
|
181
|
+
# "diff --git a/<path> b/<path>" โ match the b/ side via a
|
|
182
|
+
# backreference so a literal "b/" inside paths like lib/, web/ or
|
|
183
|
+
# db/ can't be mistaken for the prefix. Renames have differing
|
|
184
|
+
# paths, so fall back to the b/ side after the separating space.
|
|
185
|
+
match = re.match(r'diff --git a/(.+?) b/\1', line)
|
|
186
|
+
if not match:
|
|
187
|
+
match = re.search(r' b/(.+)$', line)
|
|
188
|
+
if match:
|
|
189
|
+
filename = match.group(1)
|
|
190
|
+
current_file = FileStats(
|
|
191
|
+
filename=filename,
|
|
192
|
+
language=detect_language(filename),
|
|
193
|
+
is_test=is_test_file(filename),
|
|
194
|
+
is_config=is_config_file(filename),
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
current_file = None
|
|
198
|
+
elif current_file:
|
|
199
|
+
if line.startswith('+') and not line.startswith('+++'):
|
|
200
|
+
current_file.additions += 1
|
|
201
|
+
elif line.startswith('-') and not line.startswith('---'):
|
|
202
|
+
current_file.deletions += 1
|
|
203
|
+
|
|
204
|
+
if current_file:
|
|
205
|
+
files.append(current_file)
|
|
206
|
+
|
|
207
|
+
return files
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def calculate_complexity(files: List[FileStats]) -> float:
|
|
211
|
+
"""Calculate complexity score (0-1 scale)."""
|
|
212
|
+
if not files:
|
|
213
|
+
return 0.0
|
|
214
|
+
|
|
215
|
+
total_changes = sum(f.additions + f.deletions for f in files)
|
|
216
|
+
|
|
217
|
+
# Base complexity from size
|
|
218
|
+
size_factor = min(total_changes / 1000, 1.0)
|
|
219
|
+
|
|
220
|
+
# Factor for number of files
|
|
221
|
+
file_factor = min(len(files) / 20, 1.0)
|
|
222
|
+
|
|
223
|
+
# Factor for non-test code ratio
|
|
224
|
+
test_lines = sum(f.additions + f.deletions for f in files if f.is_test)
|
|
225
|
+
non_test_ratio = 1 - (test_lines / max(total_changes, 1))
|
|
226
|
+
|
|
227
|
+
# Factor for language diversity
|
|
228
|
+
languages = set(f.language for f in files if f.language != 'unknown')
|
|
229
|
+
lang_factor = min(len(languages) / 5, 1.0)
|
|
230
|
+
|
|
231
|
+
complexity = (
|
|
232
|
+
size_factor * 0.4 +
|
|
233
|
+
file_factor * 0.2 +
|
|
234
|
+
non_test_ratio * 0.2 +
|
|
235
|
+
lang_factor * 0.2
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return round(complexity, 2)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def categorize_size(total_changes: int) -> str:
|
|
242
|
+
"""Categorize PR size."""
|
|
243
|
+
if total_changes < 50:
|
|
244
|
+
return "XS (Extra Small)"
|
|
245
|
+
elif total_changes < 200:
|
|
246
|
+
return "S (Small)"
|
|
247
|
+
elif total_changes < 400:
|
|
248
|
+
return "M (Medium)"
|
|
249
|
+
elif total_changes < 800:
|
|
250
|
+
return "L (Large)"
|
|
251
|
+
else:
|
|
252
|
+
return "XL (Extra Large) - Consider splitting"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def estimate_review_time(files: List[FileStats], complexity: float) -> int:
|
|
256
|
+
"""Estimate review time in minutes."""
|
|
257
|
+
total_changes = sum(f.additions + f.deletions for f in files)
|
|
258
|
+
|
|
259
|
+
# Base time: ~1 minute per 20 lines
|
|
260
|
+
base_time = total_changes / 20
|
|
261
|
+
|
|
262
|
+
# Adjust for complexity
|
|
263
|
+
adjusted_time = base_time * (1 + complexity)
|
|
264
|
+
|
|
265
|
+
# Minimum 5 minutes, maximum 120 minutes
|
|
266
|
+
return max(5, min(120, int(adjusted_time)))
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def identify_risk_factors(files: List[FileStats]) -> List[str]:
|
|
270
|
+
"""Identify potential risk factors in the PR."""
|
|
271
|
+
risks = []
|
|
272
|
+
|
|
273
|
+
total_changes = sum(f.additions + f.deletions for f in files)
|
|
274
|
+
test_changes = sum(f.additions + f.deletions for f in files if f.is_test)
|
|
275
|
+
|
|
276
|
+
if total_changes > 400:
|
|
277
|
+
risks.append("Large PR (>400 lines) - harder to review thoroughly")
|
|
278
|
+
|
|
279
|
+
if test_changes == 0 and total_changes > 50:
|
|
280
|
+
risks.append(f"{RISK_NO_TESTS}: No test changes - verify test coverage")
|
|
281
|
+
|
|
282
|
+
if total_changes > 100 and test_changes / max(total_changes, 1) < 0.2:
|
|
283
|
+
risks.append("Low test ratio (<20%) - consider adding more tests")
|
|
284
|
+
|
|
285
|
+
# Security-sensitive files
|
|
286
|
+
security_patterns = ['.env', 'auth', 'security', 'password', 'token', 'secret']
|
|
287
|
+
for f in files:
|
|
288
|
+
if any(p in f.filename.lower() for p in security_patterns):
|
|
289
|
+
risks.append(f"Security-sensitive file: {f.filename}")
|
|
290
|
+
break
|
|
291
|
+
|
|
292
|
+
# Database changes
|
|
293
|
+
for f in files:
|
|
294
|
+
if 'migration' in f.filename.lower() or f.language == 'SQL':
|
|
295
|
+
risks.append("Database changes detected - review carefully")
|
|
296
|
+
break
|
|
297
|
+
|
|
298
|
+
# Config changes
|
|
299
|
+
config_files = [f for f in files if f.is_config]
|
|
300
|
+
if config_files:
|
|
301
|
+
risks.append(f"Configuration changes in {len(config_files)} file(s)")
|
|
302
|
+
|
|
303
|
+
return risks
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def generate_suggestions(files: List[FileStats], complexity: float, risks: List[str]) -> List[str]:
|
|
307
|
+
"""Generate review suggestions."""
|
|
308
|
+
suggestions = []
|
|
309
|
+
|
|
310
|
+
total_changes = sum(f.additions + f.deletions for f in files)
|
|
311
|
+
|
|
312
|
+
if total_changes > 800:
|
|
313
|
+
suggestions.append("Consider splitting this PR into smaller, focused changes")
|
|
314
|
+
|
|
315
|
+
if complexity > 0.7:
|
|
316
|
+
suggestions.append("High complexity - allocate extra review time")
|
|
317
|
+
suggestions.append("Consider pair reviewing for critical sections")
|
|
318
|
+
|
|
319
|
+
if any(RISK_NO_TESTS in r for r in risks):
|
|
320
|
+
suggestions.append("Request test additions before approval")
|
|
321
|
+
|
|
322
|
+
# Language-specific suggestions
|
|
323
|
+
languages = set(f.language for f in files)
|
|
324
|
+
if 'TypeScript' in languages or 'TypeScript/React' in languages:
|
|
325
|
+
suggestions.append("Check for proper type usage (avoid 'any')")
|
|
326
|
+
if 'Rust' in languages:
|
|
327
|
+
suggestions.append("Check for unwrap() usage and error handling")
|
|
328
|
+
if 'C' in languages or 'C++' in languages or 'C/C++' in languages:
|
|
329
|
+
suggestions.append("Check for memory safety, bounds checks, and UB risks")
|
|
330
|
+
if 'SQL' in languages:
|
|
331
|
+
suggestions.append("Review for SQL injection and query performance")
|
|
332
|
+
|
|
333
|
+
if not suggestions:
|
|
334
|
+
suggestions.append("Standard review process should suffice")
|
|
335
|
+
|
|
336
|
+
return suggestions
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def analyze_pr(diff_content: str) -> PRAnalysis:
|
|
340
|
+
"""Perform complete PR analysis."""
|
|
341
|
+
files = parse_diff(diff_content)
|
|
342
|
+
|
|
343
|
+
total_additions = sum(f.additions for f in files)
|
|
344
|
+
total_deletions = sum(f.deletions for f in files)
|
|
345
|
+
total_changes = total_additions + total_deletions
|
|
346
|
+
|
|
347
|
+
complexity = calculate_complexity(files)
|
|
348
|
+
risks = identify_risk_factors(files)
|
|
349
|
+
suggestions = generate_suggestions(files, complexity, risks)
|
|
350
|
+
|
|
351
|
+
return PRAnalysis(
|
|
352
|
+
total_files=len(files),
|
|
353
|
+
total_additions=total_additions,
|
|
354
|
+
total_deletions=total_deletions,
|
|
355
|
+
files=files,
|
|
356
|
+
complexity_score=complexity,
|
|
357
|
+
size_category=categorize_size(total_changes),
|
|
358
|
+
estimated_review_time=estimate_review_time(files, complexity),
|
|
359
|
+
risk_factors=risks,
|
|
360
|
+
suggestions=suggestions,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def print_analysis(analysis: PRAnalysis, show_files: bool = False):
|
|
365
|
+
"""Print analysis results."""
|
|
366
|
+
print("\n" + "=" * 60)
|
|
367
|
+
print("PR ANALYSIS REPORT")
|
|
368
|
+
print("=" * 60)
|
|
369
|
+
|
|
370
|
+
print(f"\n๐ SUMMARY")
|
|
371
|
+
print(f" Files changed: {analysis.total_files}")
|
|
372
|
+
print(f" Additions: +{analysis.total_additions}")
|
|
373
|
+
print(f" Deletions: -{analysis.total_deletions}")
|
|
374
|
+
print(f" Total changes: {analysis.total_additions + analysis.total_deletions}")
|
|
375
|
+
|
|
376
|
+
print(f"\n๐ SIZE: {analysis.size_category}")
|
|
377
|
+
print(f" Complexity score: {analysis.complexity_score}/1.0")
|
|
378
|
+
print(f" Estimated review time: ~{analysis.estimated_review_time} minutes")
|
|
379
|
+
|
|
380
|
+
if analysis.risk_factors:
|
|
381
|
+
print(f"\nโ ๏ธ RISK FACTORS:")
|
|
382
|
+
for risk in analysis.risk_factors:
|
|
383
|
+
print(f" โข {risk}")
|
|
384
|
+
|
|
385
|
+
print(f"\n๐ก SUGGESTIONS:")
|
|
386
|
+
for suggestion in analysis.suggestions:
|
|
387
|
+
print(f" โข {suggestion}")
|
|
388
|
+
|
|
389
|
+
if show_files:
|
|
390
|
+
print(f"\n๐ FILES:")
|
|
391
|
+
# Group by language
|
|
392
|
+
by_lang: Dict[str, List[FileStats]] = defaultdict(list)
|
|
393
|
+
for f in analysis.files:
|
|
394
|
+
by_lang[f.language].append(f)
|
|
395
|
+
|
|
396
|
+
for lang, lang_files in sorted(by_lang.items()):
|
|
397
|
+
print(f"\n [{lang}]")
|
|
398
|
+
for f in lang_files:
|
|
399
|
+
prefix = "๐งช" if f.is_test else "โ๏ธ" if f.is_config else "๐"
|
|
400
|
+
print(f" {prefix} {f.filename} (+{f.additions}/-{f.deletions})")
|
|
401
|
+
|
|
402
|
+
print("\n" + "=" * 60)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def main():
|
|
406
|
+
parser = argparse.ArgumentParser(description='Analyze PR complexity')
|
|
407
|
+
parser.add_argument('--diff-file', '-f', help='Path to diff file')
|
|
408
|
+
parser.add_argument('--stats', '-s', action='store_true', help='Show file details')
|
|
409
|
+
args = parser.parse_args()
|
|
410
|
+
|
|
411
|
+
# Read diff from file or stdin
|
|
412
|
+
try:
|
|
413
|
+
if args.diff_file:
|
|
414
|
+
with open(args.diff_file, 'r', encoding='utf-8', errors='replace') as f:
|
|
415
|
+
diff_content = f.read()
|
|
416
|
+
elif not sys.stdin.isatty():
|
|
417
|
+
diff_content = sys.stdin.buffer.read().decode('utf-8', errors='replace')
|
|
418
|
+
else:
|
|
419
|
+
print("Usage: git diff main...HEAD | python pr-analyzer.py")
|
|
420
|
+
print(" python pr-analyzer.py -f diff.txt")
|
|
421
|
+
sys.exit(1)
|
|
422
|
+
except OSError as e:
|
|
423
|
+
print(f"Error reading diff input: {e}", file=sys.stderr)
|
|
424
|
+
sys.exit(1)
|
|
425
|
+
|
|
426
|
+
if not diff_content.strip():
|
|
427
|
+
print("No diff content provided")
|
|
428
|
+
sys.exit(1)
|
|
429
|
+
|
|
430
|
+
analysis = analyze_pr(diff_content)
|
|
431
|
+
print_analysis(analysis, show_files=args.stats)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
if __name__ == '__main__':
|
|
435
|
+
main()
|