-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathrust_unique.py
More file actions
354 lines (280 loc) · 10.3 KB
/
Copy pathrust_unique.py
File metadata and controls
354 lines (280 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
import json
import os
import requests
from pathlib import Path
from collections import Counter
import time
# ---------------------------
# Config
# ---------------------------
GITHUB_ORG = "ScottyLabs"
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") # Set this: export GITHUB_TOKEN=your_token
REPOS_DIR = 'repos'
# ---------------------------
# GitHub API Functions
# ---------------------------
def get_github_headers():
headers = {"Accept": "application/vnd.github.v3+json"}
if GITHUB_TOKEN:
headers["Authorization"] = f"token {GITHUB_TOKEN}"
return headers
def search_cargo_files(repo_name):
"""Search for all Cargo.toml files in a repository using tree API"""
try:
# Get the default branch
url = f"https://api.github.com/repos/{GITHUB_ORG}/{repo_name}"
response = requests.get(url, headers=get_github_headers())
if response.status_code == 403:
print(f"\n⚠️ Rate limit hit! Waiting 60 seconds...")
time.sleep(60)
response = requests.get(url, headers=get_github_headers())
if response.status_code != 200:
print(f"[Error {response.status_code}]", end=" ")
return []
default_branch = response.json().get('default_branch', 'main')
# Get the tree recursively
tree_url = f"https://api.github.com/repos/{GITHUB_ORG}/{repo_name}/git/trees/{default_branch}?recursive=1"
tree_response = requests.get(tree_url, headers=get_github_headers())
if tree_response.status_code == 403:
print(f"\n⚠️ Rate limit hit! Waiting 60 seconds...")
time.sleep(60)
tree_response = requests.get(tree_url, headers=get_github_headers())
if tree_response.status_code != 200:
print(f"[Error {tree_response.status_code}]", end=" ")
return []
tree_data = tree_response.json()
cargo_files = []
# Find all Cargo.toml files
for item in tree_data.get('tree', []):
if item['path'].endswith('Cargo.toml'):
cargo_files.append(item['path'])
return cargo_files
except Exception as e:
print(f"[Exception: {e}]", end=" ")
return []
def get_file_content(repo_name, file_path):
"""Fetch any file content from a GitHub repo"""
url = f"https://api.github.com/repos/{GITHUB_ORG}/{repo_name}/contents/{file_path}"
response = requests.get(url, headers=get_github_headers())
if response.status_code == 200:
import base64
content = base64.b64decode(response.json()['content']).decode('utf-8')
return content
return None
def parse_cargo_dependencies(cargo_content):
"""Parse dependencies from Cargo.toml content"""
if not cargo_content:
return []
dependencies = []
in_dependencies = False
for line in cargo_content.split('\n'):
line = line.strip()
if line.startswith('[dependencies]'):
in_dependencies = True
continue
elif line.startswith('[') and in_dependencies:
break
elif in_dependencies and '=' in line:
dep_name = line.split('=')[0].strip()
if dep_name and not dep_name.startswith('#'):
dependencies.append(dep_name)
return dependencies
# ---------------------------
# Get all repos from GitHub
# ---------------------------
def get_all_repos():
"""Fetch all repositories from the organization"""
repos = []
page = 1
while True:
url = f"https://api.github.com/orgs/{GITHUB_ORG}/repos?per_page=100&page={page}"
response = requests.get(url, headers=get_github_headers())
if response.status_code != 200:
print(f"Error fetching repos: {response.status_code}")
break
data = response.json()
if not data:
break
repos.extend([repo['name'] for repo in data])
page += 1
return repos
# ---------------------------
# Main Analysis
# ---------------------------
print("Fetching all ScottyLabs repositories from GitHub...")
print("=" * 80)
all_repos = get_all_repos()
print(f"Found {len(all_repos)} total repositories\n")
# Load local JSON data for SLOC info
sloc_data = {}
for json_file in Path(REPOS_DIR).glob('*.json'):
try:
with open(json_file, 'r') as f:
data = json.load(f)
sloc_data[data['repo']] = {
'rust_sloc': data.get('rust_sloc', 0),
'proportion': data.get('rust_sloc_ratio', 0)
}
except Exception as e:
print(f"Error reading {json_file}: {e}")
rust_repos_local = []
for repo in all_repos:
if repo in sloc_data and sloc_data[repo]['rust_sloc'] > 0:
rust_repos_local.append({
'repo': repo,
'rust_sloc': sloc_data[repo]['rust_sloc'],
'proportion': sloc_data[repo]['proportion']
})
print(f"Repositories with Rust code: {len(rust_repos_local)}")
# Fetch actual dependencies from GitHub
all_crates = Counter()
repo_crates = {}
for repo_data in rust_repos_local:
repo_name = repo_data['repo']
print(f"Searching {repo_name}...", end=" ")
# Search for all Cargo.toml files in the repo
cargo_files = search_cargo_files(repo_name)
if cargo_files:
print(f"found {len(cargo_files)} Cargo.toml file(s)")
all_deps = set()
for cargo_path in cargo_files:
print(f" - {cargo_path}...", end=" ")
cargo_content = get_file_content(repo_name, cargo_path)
if cargo_content:
deps = parse_cargo_dependencies(cargo_content)
all_deps.update(deps)
print(f"({len(deps)} deps)")
else:
print("✗")
time.sleep(1) # Increased delay to avoid rate limits
repo_crates[repo_name] = list(all_deps)
for dep in all_deps:
all_crates[dep] += 1
else:
repo_crates[repo_name] = []
print("✗ (no Cargo.toml files)")
time.sleep(1.5) # Longer delay between repos to avoid rate limits
total_rust_repos = len([r for r in repo_crates.values() if r])
# Infrastructure crates to ignore (used everywhere, not indicative of purpose)
IGNORE_CRATES = {
# Async runtimes
'tokio', 'async-std', 'async-trait', 'futures',
# Serialization
'serde', 'serde_json', 'serde_derive',
# Error handling
'anyhow', 'thiserror',
# Logging
'log', 'env_logger', 'tracing',
# Environment
'dotenv', 'dotenvy', 'dotenv_codegen',
# Common utilities
'chrono', 'uuid', 'regex', 'lazy_static', 'once_cell',
# HTTP basics
'http', 'http-body-util',
# Encoding
'base64', 'urlencoding',
# Configuration
'config',
# Local dependencies (workspace crates)
'models', 'path', 'search', 'migration',
}
# Crate purpose mapping
crate_purposes = {
# Web frameworks
'axum': 'web server/API framework',
'actix-web': 'web server/API',
'rocket': 'web server/API',
'warp': 'web server/API',
'tower': 'middleware for network services',
'tower-http': 'HTTP middleware',
'tower-sessions': 'session management',
'tower-oauth2-resource-server': 'OAuth2 resource server',
# Database
'sea-orm': 'database ORM',
'diesel': 'database ORM',
'sqlx': 'database access',
# Bot frameworks
'poise': 'Discord bot',
'serenity': 'Discord bot',
'slack-morphism': 'Slack bot',
# Auth
'samael': 'SAML authentication',
'jsonwebtoken': 'JWT authentication',
# Search/ranking
'bm25': 'search ranking algorithm',
'meilisearch-sdk': 'search engine client',
# WebAssembly
'wasm-bindgen': 'WebAssembly bindings',
'wasm-bindgen-futures': 'WebAssembly async',
# File handling
'zip': 'ZIP file handling',
'git2': 'Git operations',
# HTTP
'reqwest': 'HTTP client',
'hyper': 'HTTP implementation',
# API documentation
'utoipa': 'OpenAPI documentation',
'utoipa-axum': 'OpenAPI for Axum',
'utoipa-swagger-ui': 'Swagger UI integration',
# Desktop apps
'tauri': 'desktop app framework',
'tauri-plugin-opener': 'Tauri file opener plugin',
'tauri-plugin-deep-link': 'Tauri deep linking',
# Storage
'minio': 'S3-compatible object storage',
# Caching
'moka': 'in-memory cache',
# Templating
'askama': 'type-safe templates',
# CLI
'clap': 'command-line argument parser',
'colored': 'terminal colors',
# Concurrency
'rayon': 'data parallelism',
'async-std': 'async runtime',
# Logging/tracing
'tracing-subscriber': 'structured logging',
# Data structures
'priority-queue': 'priority queue',
}
# Analysis
print("\n" + "=" * 80)
print("Rust Usage Analysis - ScottyLabs Repositories")
print("=" * 80)
rust_repos_local.sort(key=lambda x: x['proportion'], reverse=True)
for repo_data in rust_repos_local:
repo_name = repo_data['repo']
crates = repo_crates.get(repo_name, [])
# Filter out infrastructure crates and apply usage threshold
threshold = 2
filtered_crates = [c for c in crates if c not in IGNORE_CRATES]
truly_unique = [c for c in filtered_crates if all_crates[c] <= threshold]
print(f"\n{repo_name}")
print(f" Rust SLOC: {repo_data['rust_sloc']} ({repo_data['proportion']*100:.2f}%)")
print(f" Total dependencies: {len(crates)}")
print(f" After filtering infrastructure: {len(filtered_crates)}")
print(f" Truly unique (used in ≤{threshold} repos): {len(truly_unique)}")
if truly_unique:
purposes = []
unknown = []
for crate in truly_unique:
if crate in crate_purposes:
purposes.append(f"{crate} ({crate_purposes[crate]})")
else:
unknown.append(crate)
if purposes:
print(f" Purpose indicators:")
for p in purposes:
print(f" - {p}")
if unknown:
print(f" Other unique: {', '.join(unknown[:5])}")
else:
print(f" No unique dependencies (uses common crates)")
# Summary
print("\n" + "=" * 80)
print("\nAll Crates Used Across ScottyLabs (sorted by usage):")
print("-" * 80)
for crate, count in all_crates.most_common():
percentage = (count / total_rust_repos) * 100 if total_rust_repos > 0 else 0
purpose = f" - {crate_purposes[crate]}" if crate in crate_purposes else ""
print(f" {crate:<30} {count}/{total_rust_repos} repos ({percentage:>5.1f}%){purpose}")