Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -4,7 +4,7 @@ FastAPI server for GitHub App integration
|
|
| 4 |
Connects desktop app to GitHub securely
|
| 5 |
"""
|
| 6 |
|
| 7 |
-
from fastapi import FastAPI, HTTPException, Request
|
| 8 |
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
from pydantic import BaseModel
|
| 10 |
from typing import Optional, List, Dict
|
|
@@ -12,6 +12,12 @@ import json
|
|
| 12 |
import os
|
| 13 |
from pathlib import Path
|
| 14 |
from dotenv import load_dotenv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
from github_api import GitHubAppAuth, GitHubInsights, TokenManager
|
| 17 |
|
|
@@ -37,17 +43,30 @@ app.add_middleware(
|
|
| 37 |
|
| 38 |
# Load environment variables
|
| 39 |
APP_ID = os.getenv("GITHUB_APP_ID")
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
# Initialize GitHub App authentication
|
| 43 |
-
if APP_ID and
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
| 45 |
token_manager = TokenManager(github_auth)
|
| 46 |
else:
|
| 47 |
github_auth = None
|
| 48 |
token_manager = None
|
| 49 |
print("⚠️ WARNING: GitHub App credentials not configured")
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
# Simple JSON storage for installations
|
| 52 |
INSTALLATIONS_FILE = "installations.json"
|
| 53 |
|
|
@@ -65,6 +84,7 @@ class InstallationCallback(BaseModel):
|
|
| 65 |
|
| 66 |
class InsightsRequest(BaseModel):
|
| 67 |
"""Request for repository insights from desktop app"""
|
|
|
|
| 68 |
org: str
|
| 69 |
repo: str
|
| 70 |
|
|
@@ -74,6 +94,12 @@ class TokenRefreshRequest(BaseModel):
|
|
| 74 |
installation_id: int
|
| 75 |
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
# ============================================================================
|
| 78 |
# UTILITY FUNCTIONS
|
| 79 |
# ============================================================================
|
|
@@ -108,6 +134,224 @@ def get_installation_id(org: str) -> Optional[int]:
|
|
| 108 |
return None
|
| 109 |
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
# ============================================================================
|
| 112 |
# API ENDPOINTS
|
| 113 |
# ============================================================================
|
|
@@ -133,18 +377,25 @@ def health_check():
|
|
| 133 |
|
| 134 |
|
| 135 |
@app.get("/github/callback")
|
| 136 |
-
async def github_callback(request: Request):
|
| 137 |
"""
|
| 138 |
Handle GitHub App installation callback
|
| 139 |
|
| 140 |
-
After org installs Codesage, GitHub redirects here with installation_id
|
| 141 |
-
|
|
|
|
| 142 |
"""
|
| 143 |
# Get query parameters
|
| 144 |
query_params = dict(request.query_params)
|
| 145 |
|
| 146 |
installation_id = query_params.get("installation_id")
|
| 147 |
setup_action = query_params.get("setup_action")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
if not installation_id:
|
| 150 |
return {
|
|
@@ -184,11 +435,26 @@ async def github_callback(request: Request):
|
|
| 184 |
# Save with org name if we got it, otherwise use placeholder
|
| 185 |
if org_name:
|
| 186 |
save_installation(org_name, installation_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
return {
|
| 188 |
"success": True,
|
| 189 |
"message": f"✅ Successfully installed Codesage for {org_name}",
|
| 190 |
"org": org_name,
|
| 191 |
-
"installation_id": installation_id
|
|
|
|
| 192 |
}
|
| 193 |
else:
|
| 194 |
# Save with placeholder - user can update via /github/install
|
|
@@ -289,103 +555,287 @@ def list_org_repositories(org: str):
|
|
| 289 |
@app.post("/insights/commits")
|
| 290 |
def get_commits(data: InsightsRequest):
|
| 291 |
"""
|
| 292 |
-
Get commit history for a repository
|
| 293 |
-
|
| 294 |
"""
|
| 295 |
if not github_auth:
|
| 296 |
raise HTTPException(status_code=500, detail="GitHub App not configured")
|
|
|
|
|
|
|
| 297 |
|
| 298 |
installation_id = get_installation_id(data.org)
|
| 299 |
if not installation_id:
|
| 300 |
raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
|
| 301 |
|
| 302 |
try:
|
|
|
|
| 303 |
token = token_manager.get_token(installation_id)
|
| 304 |
insights = GitHubInsights(token)
|
| 305 |
-
commits = insights.get_commits(data.org, data.repo)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
|
| 307 |
return {
|
| 308 |
"org": data.org,
|
| 309 |
"repo": data.repo,
|
| 310 |
"count": len(commits),
|
|
|
|
| 311 |
"commits": commits
|
| 312 |
}
|
| 313 |
except Exception as e:
|
|
|
|
| 314 |
raise HTTPException(status_code=500, detail=f"Failed to fetch commits: {str(e)}")
|
| 315 |
|
| 316 |
|
| 317 |
@app.post("/insights/pull-requests")
|
| 318 |
def get_pull_requests(data: InsightsRequest):
|
| 319 |
-
"""Get pull requests for a repository
|
|
|
|
|
|
|
| 320 |
if not github_auth:
|
| 321 |
raise HTTPException(status_code=500, detail="GitHub App not configured")
|
|
|
|
|
|
|
| 322 |
|
| 323 |
installation_id = get_installation_id(data.org)
|
| 324 |
if not installation_id:
|
| 325 |
raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
|
| 326 |
|
| 327 |
try:
|
|
|
|
| 328 |
token = token_manager.get_token(installation_id)
|
| 329 |
insights = GitHubInsights(token)
|
| 330 |
-
prs = insights.get_pull_requests(data.org, data.repo)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
|
| 332 |
return {
|
| 333 |
"org": data.org,
|
| 334 |
"repo": data.repo,
|
| 335 |
"count": len(prs),
|
|
|
|
| 336 |
"pull_requests": prs
|
| 337 |
}
|
| 338 |
except Exception as e:
|
|
|
|
| 339 |
raise HTTPException(status_code=500, detail=f"Failed to fetch pull requests: {str(e)}")
|
| 340 |
|
| 341 |
|
| 342 |
@app.post("/insights/issues")
|
| 343 |
def get_issues(data: InsightsRequest):
|
| 344 |
-
"""Get issues for a repository
|
|
|
|
|
|
|
| 345 |
if not github_auth:
|
| 346 |
raise HTTPException(status_code=500, detail="GitHub App not configured")
|
|
|
|
|
|
|
| 347 |
|
| 348 |
installation_id = get_installation_id(data.org)
|
| 349 |
if not installation_id:
|
| 350 |
raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
|
| 351 |
|
| 352 |
try:
|
|
|
|
| 353 |
token = token_manager.get_token(installation_id)
|
| 354 |
insights = GitHubInsights(token)
|
| 355 |
-
issues = insights.get_issues(data.org, data.repo)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
|
| 357 |
return {
|
| 358 |
"org": data.org,
|
| 359 |
"repo": data.repo,
|
| 360 |
"count": len(issues),
|
|
|
|
| 361 |
"issues": issues
|
| 362 |
}
|
| 363 |
except Exception as e:
|
|
|
|
| 364 |
raise HTTPException(status_code=500, detail=f"Failed to fetch issues: {str(e)}")
|
| 365 |
|
| 366 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
@app.post("/insights/contributors")
|
| 368 |
def get_contributors(data: InsightsRequest):
|
| 369 |
-
"""Get contributors and their stats
|
|
|
|
|
|
|
| 370 |
if not github_auth:
|
| 371 |
raise HTTPException(status_code=500, detail="GitHub App not configured")
|
|
|
|
|
|
|
| 372 |
|
| 373 |
installation_id = get_installation_id(data.org)
|
| 374 |
if not installation_id:
|
| 375 |
raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
|
| 376 |
|
| 377 |
try:
|
|
|
|
| 378 |
token = token_manager.get_token(installation_id)
|
| 379 |
insights = GitHubInsights(token)
|
| 380 |
contributors = insights.get_contributors(data.org, data.repo)
|
| 381 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
return {
|
| 383 |
"org": data.org,
|
| 384 |
"repo": data.repo,
|
| 385 |
"count": len(contributors),
|
|
|
|
| 386 |
"contributors": contributors
|
| 387 |
}
|
| 388 |
except Exception as e:
|
|
|
|
| 389 |
raise HTTPException(status_code=500, detail=f"Failed to fetch contributors: {str(e)}")
|
| 390 |
|
| 391 |
|
|
@@ -445,11 +895,17 @@ def get_repository_activity(data: InsightsRequest):
|
|
| 445 |
raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
|
| 446 |
|
| 447 |
try:
|
|
|
|
| 448 |
token = token_manager.get_token(installation_id)
|
| 449 |
insights = GitHubInsights(token)
|
| 450 |
|
|
|
|
| 451 |
code_frequency = insights.get_code_frequency(data.org, data.repo)
|
|
|
|
|
|
|
|
|
|
| 452 |
commit_activity = insights.get_commit_activity(data.org, data.repo)
|
|
|
|
| 453 |
|
| 454 |
return {
|
| 455 |
"org": data.org,
|
|
@@ -458,12 +914,42 @@ def get_repository_activity(data: InsightsRequest):
|
|
| 458 |
"commit_activity": commit_activity
|
| 459 |
}
|
| 460 |
except Exception as e:
|
|
|
|
| 461 |
raise HTTPException(status_code=500, detail=f"Failed to fetch activity: {str(e)}")
|
| 462 |
|
| 463 |
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
|
| 468 |
|
| 469 |
|
|
|
|
| 4 |
Connects desktop app to GitHub securely
|
| 5 |
"""
|
| 6 |
|
| 7 |
+
from fastapi import FastAPI, HTTPException, Request, BackgroundTasks
|
| 8 |
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
from pydantic import BaseModel
|
| 10 |
from typing import Optional, List, Dict
|
|
|
|
| 12 |
import os
|
| 13 |
from pathlib import Path
|
| 14 |
from dotenv import load_dotenv
|
| 15 |
+
from supabase import create_client, Client
|
| 16 |
+
import logging
|
| 17 |
+
|
| 18 |
+
# Configure logging
|
| 19 |
+
logging.basicConfig(level=logging.INFO)
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
|
| 22 |
from github_api import GitHubAppAuth, GitHubInsights, TokenManager
|
| 23 |
|
|
|
|
| 43 |
|
| 44 |
# Load environment variables
|
| 45 |
APP_ID = os.getenv("GITHUB_APP_ID")
|
| 46 |
+
PRIVATE_KEY_PATH = os.getenv("GITHUB_PRIVATE_KEY_PATH", "private-key.pem")
|
| 47 |
+
SUPABASE_URL = os.getenv("SUPABASE_URL")
|
| 48 |
+
SUPABASE_SECRET_KEY = os.getenv("SUPABASE_SECRET_KEY")
|
| 49 |
+
DEFAULT_FIREBASE_ID = os.getenv("DEFAULT_FIREBASE_ID", "JDfZoVuGJhdLBEvR7rVCQKBG9r02")
|
| 50 |
|
| 51 |
# Initialize GitHub App authentication
|
| 52 |
+
if APP_ID and os.path.exists(PRIVATE_KEY_PATH):
|
| 53 |
+
with open(PRIVATE_KEY_PATH, "r") as f:
|
| 54 |
+
private_key = f.read()
|
| 55 |
+
|
| 56 |
+
github_auth = GitHubAppAuth(APP_ID, private_key)
|
| 57 |
token_manager = TokenManager(github_auth)
|
| 58 |
else:
|
| 59 |
github_auth = None
|
| 60 |
token_manager = None
|
| 61 |
print("⚠️ WARNING: GitHub App credentials not configured")
|
| 62 |
|
| 63 |
+
# Initialize Supabase client
|
| 64 |
+
if SUPABASE_URL and SUPABASE_SECRET_KEY:
|
| 65 |
+
supabase: Optional[Client] = create_client(SUPABASE_URL, SUPABASE_SECRET_KEY)
|
| 66 |
+
else:
|
| 67 |
+
supabase = None
|
| 68 |
+
print("⚠️ WARNING: Supabase credentials not configured")
|
| 69 |
+
|
| 70 |
# Simple JSON storage for installations
|
| 71 |
INSTALLATIONS_FILE = "installations.json"
|
| 72 |
|
|
|
|
| 84 |
|
| 85 |
class InsightsRequest(BaseModel):
|
| 86 |
"""Request for repository insights from desktop app"""
|
| 87 |
+
firebase_id: str # Now required
|
| 88 |
org: str
|
| 89 |
repo: str
|
| 90 |
|
|
|
|
| 94 |
installation_id: int
|
| 95 |
|
| 96 |
|
| 97 |
+
class GitHubSyncRequest(BaseModel):
|
| 98 |
+
"""Request to sync GitHub data into Supabase"""
|
| 99 |
+
firebase_id: str
|
| 100 |
+
org: str
|
| 101 |
+
|
| 102 |
+
|
| 103 |
# ============================================================================
|
| 104 |
# UTILITY FUNCTIONS
|
| 105 |
# ============================================================================
|
|
|
|
| 134 |
return None
|
| 135 |
|
| 136 |
|
| 137 |
+
def ensure_user_exists(firebase_id: str):
|
| 138 |
+
if not supabase:
|
| 139 |
+
raise HTTPException(status_code=500, detail="Supabase not configured")
|
| 140 |
+
|
| 141 |
+
result = supabase.table("Users").select("firebase_id").eq("firebase_id", firebase_id).limit(1).execute()
|
| 142 |
+
if not result.data:
|
| 143 |
+
raise HTTPException(status_code=404, detail="User not found for firebase_id")
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def build_repo_rollup(org: str, repo: str, insights: GitHubInsights) -> Dict:
|
| 147 |
+
repo_info = insights.get_repository_info(org, repo)
|
| 148 |
+
commits = insights.get_commits(org, repo, per_page=100)
|
| 149 |
+
pull_requests = insights.get_pull_requests(org, repo, state="all")
|
| 150 |
+
issues = insights.get_issues(org, repo, state="all")
|
| 151 |
+
contributors = insights.get_contributors(org, repo)
|
| 152 |
+
languages = insights.get_languages(org, repo)
|
| 153 |
+
|
| 154 |
+
return {
|
| 155 |
+
"owner": repo_info.get("owner", {}).get("login"),
|
| 156 |
+
"repo": repo_info.get("name"),
|
| 157 |
+
"full_name": repo_info.get("full_name"),
|
| 158 |
+
"private": repo_info.get("private"),
|
| 159 |
+
"default_branch": repo_info.get("default_branch"),
|
| 160 |
+
"language": repo_info.get("language"),
|
| 161 |
+
"html_url": repo_info.get("html_url"),
|
| 162 |
+
"updated_at": repo_info.get("updated_at"),
|
| 163 |
+
"pushed_at": repo_info.get("pushed_at"),
|
| 164 |
+
"description": repo_info.get("description"),
|
| 165 |
+
"stars": repo_info.get("stargazers_count", 0),
|
| 166 |
+
"forks": repo_info.get("forks_count", 0),
|
| 167 |
+
"open_issues": repo_info.get("open_issues_count", 0),
|
| 168 |
+
"created_at": repo_info.get("created_at"),
|
| 169 |
+
"size_kb": repo_info.get("size", 0),
|
| 170 |
+
"commit_count": len(commits),
|
| 171 |
+
"pr_count": len(pull_requests),
|
| 172 |
+
"issue_count": len(issues),
|
| 173 |
+
"contributor_count": len(contributors),
|
| 174 |
+
"languages_json": languages,
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def sync_installation_to_supabase(firebase_id: str, org: str, installation_id: int) -> int:
|
| 179 |
+
if not github_auth:
|
| 180 |
+
logger.error("GitHub App not configured")
|
| 181 |
+
raise HTTPException(status_code=500, detail="GitHub App not configured")
|
| 182 |
+
if not supabase:
|
| 183 |
+
logger.error("Supabase not configured")
|
| 184 |
+
raise HTTPException(status_code=500, detail="Supabase not configured")
|
| 185 |
+
|
| 186 |
+
try:
|
| 187 |
+
logger.info(f"Starting sync for firebase_id={firebase_id}, org={org}, installation_id={installation_id}")
|
| 188 |
+
|
| 189 |
+
# Check if user exists
|
| 190 |
+
logger.info(f"Checking if user exists with firebase_id={firebase_id}")
|
| 191 |
+
result = supabase.table("Users").select("firebase_id").eq("firebase_id", firebase_id).limit(1).execute()
|
| 192 |
+
if not result.data:
|
| 193 |
+
logger.error(f"User not found in Supabase for firebase_id={firebase_id}")
|
| 194 |
+
return 0
|
| 195 |
+
|
| 196 |
+
token = token_manager.get_token(installation_id)
|
| 197 |
+
repos = github_auth.get_installation_repositories(token)
|
| 198 |
+
logger.info(f"Found {len(repos)} repositories for {org}")
|
| 199 |
+
|
| 200 |
+
insights = GitHubInsights(token)
|
| 201 |
+
|
| 202 |
+
# Build repo rollups
|
| 203 |
+
rows = []
|
| 204 |
+
for i, repo in enumerate(repos):
|
| 205 |
+
try:
|
| 206 |
+
repo_name = repo.get("name")
|
| 207 |
+
logger.info(f"Processing repo {i+1}/{len(repos)}: {repo_name}")
|
| 208 |
+
rollup = build_repo_rollup(org, repo_name, insights)
|
| 209 |
+
rollup["firebase_id"] = firebase_id
|
| 210 |
+
rollup["installation_id"] = installation_id
|
| 211 |
+
rows.append(rollup)
|
| 212 |
+
except Exception as e:
|
| 213 |
+
logger.warning(f"Failed to process repo {repo_name}: {str(e)}")
|
| 214 |
+
continue
|
| 215 |
+
|
| 216 |
+
if rows:
|
| 217 |
+
logger.info(f"Upserting {len(rows)} repos into github table")
|
| 218 |
+
supabase.table("github").upsert(rows, on_conflict="firebase_id,full_name").execute()
|
| 219 |
+
logger.info(f"Successfully synced {len(rows)} repos to github table")
|
| 220 |
+
|
| 221 |
+
# Now sync detail data for each repo
|
| 222 |
+
for i, repo in enumerate(repos):
|
| 223 |
+
try:
|
| 224 |
+
repo_name = repo.get("name")
|
| 225 |
+
full_name = f"{org}/{repo_name}"
|
| 226 |
+
logger.info(f"Syncing details for repo {i+1}/{len(repos)}: {repo_name}")
|
| 227 |
+
|
| 228 |
+
# Get github_id
|
| 229 |
+
github_result = supabase.table("github").select("id").eq("firebase_id", firebase_id).eq("full_name", full_name).limit(1).execute()
|
| 230 |
+
if not github_result.data:
|
| 231 |
+
logger.warning(f"Could not find {full_name} in github table, skipping details")
|
| 232 |
+
continue
|
| 233 |
+
|
| 234 |
+
github_id = github_result.data[0]["id"]
|
| 235 |
+
|
| 236 |
+
# Sync commits
|
| 237 |
+
try:
|
| 238 |
+
commits = insights.get_commits(org, repo_name, per_page=100)
|
| 239 |
+
commit_rows = []
|
| 240 |
+
for commit in commits:
|
| 241 |
+
commit_data = commit.get("commit", {})
|
| 242 |
+
author_data = commit_data.get("author", {})
|
| 243 |
+
commit_rows.append({
|
| 244 |
+
"github_id": github_id,
|
| 245 |
+
"firebase_id": firebase_id,
|
| 246 |
+
"sha": commit.get("sha"),
|
| 247 |
+
"message": commit_data.get("message"),
|
| 248 |
+
"author": author_data.get("name"),
|
| 249 |
+
"author_email": author_data.get("email"),
|
| 250 |
+
"committed_date": author_data.get("date"),
|
| 251 |
+
})
|
| 252 |
+
if commit_rows:
|
| 253 |
+
supabase.table("commits").upsert(commit_rows, on_conflict="firebase_id,sha").execute()
|
| 254 |
+
logger.info(f" Stored {len(commit_rows)} commits")
|
| 255 |
+
except Exception as e:
|
| 256 |
+
logger.warning(f" Failed to sync commits: {str(e)}")
|
| 257 |
+
|
| 258 |
+
# Sync issues
|
| 259 |
+
try:
|
| 260 |
+
issues = insights.get_issues(org, repo_name, state="all")
|
| 261 |
+
issue_rows = []
|
| 262 |
+
for issue in issues:
|
| 263 |
+
if "pull_request" in issue:
|
| 264 |
+
continue
|
| 265 |
+
issue_rows.append({
|
| 266 |
+
"github_id": github_id,
|
| 267 |
+
"firebase_id": firebase_id,
|
| 268 |
+
"issue_id": issue.get("id"),
|
| 269 |
+
"issue_number": issue.get("number"),
|
| 270 |
+
"title": issue.get("title"),
|
| 271 |
+
"body": issue.get("body"),
|
| 272 |
+
"state": issue.get("state"),
|
| 273 |
+
"author": issue.get("user", {}).get("login"),
|
| 274 |
+
"created_at": issue.get("created_at"),
|
| 275 |
+
"updated_at": issue.get("updated_at"),
|
| 276 |
+
"url": issue.get("html_url"),
|
| 277 |
+
"labels": [label.get("name") for label in issue.get("labels", [])],
|
| 278 |
+
"comments": issue.get("comments", 0),
|
| 279 |
+
"reactions": issue.get("reactions", {}).get("total_count", 0),
|
| 280 |
+
})
|
| 281 |
+
if issue_rows:
|
| 282 |
+
supabase.table("issues").upsert(issue_rows, on_conflict="firebase_id,issue_id").execute()
|
| 283 |
+
logger.info(f" Stored {len(issue_rows)} issues")
|
| 284 |
+
except Exception as e:
|
| 285 |
+
logger.warning(f" Failed to sync issues: {str(e)}")
|
| 286 |
+
|
| 287 |
+
# Sync pull requests
|
| 288 |
+
try:
|
| 289 |
+
prs = insights.get_pull_requests(org, repo_name, state="all")
|
| 290 |
+
pr_rows = []
|
| 291 |
+
for pr in prs:
|
| 292 |
+
pr_rows.append({
|
| 293 |
+
"github_id": github_id,
|
| 294 |
+
"firebase_id": firebase_id,
|
| 295 |
+
"pr_id": pr.get("id"),
|
| 296 |
+
"pr_number": pr.get("number"),
|
| 297 |
+
"title": pr.get("title"),
|
| 298 |
+
"body": pr.get("body"),
|
| 299 |
+
"state": pr.get("state"),
|
| 300 |
+
"author": pr.get("user", {}).get("login"),
|
| 301 |
+
"created_at": pr.get("created_at"),
|
| 302 |
+
"updated_at": pr.get("updated_at"),
|
| 303 |
+
"merged_at": pr.get("merged_at"),
|
| 304 |
+
"closed_at": pr.get("closed_at"),
|
| 305 |
+
"url": pr.get("html_url"),
|
| 306 |
+
"head_branch": pr.get("head", {}).get("ref"),
|
| 307 |
+
"base_branch": pr.get("base", {}).get("ref"),
|
| 308 |
+
"draft": pr.get("draft", False),
|
| 309 |
+
"mergeable": pr.get("mergeable"),
|
| 310 |
+
"comments": pr.get("comments", 0),
|
| 311 |
+
"commits": pr.get("commits", 0),
|
| 312 |
+
"additions": pr.get("additions", 0),
|
| 313 |
+
"deletions": pr.get("deletions", 0),
|
| 314 |
+
"changed_files": pr.get("changed_files", 0),
|
| 315 |
+
})
|
| 316 |
+
if pr_rows:
|
| 317 |
+
supabase.table("pull_requests").upsert(pr_rows, on_conflict="firebase_id,pr_id").execute()
|
| 318 |
+
logger.info(f" Stored {len(pr_rows)} pull requests")
|
| 319 |
+
except Exception as e:
|
| 320 |
+
logger.warning(f" Failed to sync pull requests: {str(e)}")
|
| 321 |
+
|
| 322 |
+
# Sync contributors
|
| 323 |
+
try:
|
| 324 |
+
contributors = insights.get_contributors(org, repo_name)
|
| 325 |
+
contributor_rows = []
|
| 326 |
+
for contributor in contributors:
|
| 327 |
+
contributor_rows.append({
|
| 328 |
+
"github_id": github_id,
|
| 329 |
+
"firebase_id": firebase_id,
|
| 330 |
+
"username": contributor.get("login"),
|
| 331 |
+
"contributions": contributor.get("contributions", 0),
|
| 332 |
+
"avatar_url": contributor.get("avatar_url"),
|
| 333 |
+
"profile_url": contributor.get("html_url"),
|
| 334 |
+
})
|
| 335 |
+
if contributor_rows:
|
| 336 |
+
supabase.table("contributors").upsert(contributor_rows, on_conflict="firebase_id,github_id,username").execute()
|
| 337 |
+
logger.info(f" Stored {len(contributor_rows)} contributors")
|
| 338 |
+
except Exception as e:
|
| 339 |
+
logger.warning(f" Failed to sync contributors: {str(e)}")
|
| 340 |
+
|
| 341 |
+
except Exception as e:
|
| 342 |
+
logger.warning(f"Failed to sync details for {repo_name}: {str(e)}")
|
| 343 |
+
continue
|
| 344 |
+
|
| 345 |
+
logger.info(f"Completed full sync for {len(rows)} repos")
|
| 346 |
+
else:
|
| 347 |
+
logger.warning("No repos to sync")
|
| 348 |
+
|
| 349 |
+
return len(rows)
|
| 350 |
+
except Exception as e:
|
| 351 |
+
logger.error(f"Sync failed: {str(e)}", exc_info=True)
|
| 352 |
+
return 0
|
| 353 |
+
|
| 354 |
+
|
| 355 |
# ============================================================================
|
| 356 |
# API ENDPOINTS
|
| 357 |
# ============================================================================
|
|
|
|
| 377 |
|
| 378 |
|
| 379 |
@app.get("/github/callback")
|
| 380 |
+
async def github_callback(request: Request, background_tasks: BackgroundTasks):
|
| 381 |
"""
|
| 382 |
Handle GitHub App installation callback
|
| 383 |
|
| 384 |
+
After org installs Codesage, GitHub redirects here with installation_id and state (firebase_uid)
|
| 385 |
+
Frontend should redirect to: https://github.com/apps/{app_slug}/installations/new?state={firebase_uid}
|
| 386 |
+
GitHub redirects back with: /github/callback?installation_id=...&state={firebase_uid}
|
| 387 |
"""
|
| 388 |
# Get query parameters
|
| 389 |
query_params = dict(request.query_params)
|
| 390 |
|
| 391 |
installation_id = query_params.get("installation_id")
|
| 392 |
setup_action = query_params.get("setup_action")
|
| 393 |
+
firebase_id = query_params.get("state") # GitHub passes firebase_uid via state parameter
|
| 394 |
+
|
| 395 |
+
# Fallback to default if state not provided (for backward compatibility)
|
| 396 |
+
if not firebase_id:
|
| 397 |
+
logger.warning("No firebase_id in state parameter, using default")
|
| 398 |
+
firebase_id = DEFAULT_FIREBASE_ID
|
| 399 |
|
| 400 |
if not installation_id:
|
| 401 |
return {
|
|
|
|
| 435 |
# Save with org name if we got it, otherwise use placeholder
|
| 436 |
if org_name:
|
| 437 |
save_installation(org_name, installation_id)
|
| 438 |
+
sync_result = None
|
| 439 |
+
if supabase:
|
| 440 |
+
# Run heavy sync in background so callback returns immediately
|
| 441 |
+
background_tasks.add_task(
|
| 442 |
+
sync_installation_to_supabase,
|
| 443 |
+
firebase_id, # Use firebase_id from state parameter
|
| 444 |
+
org_name,
|
| 445 |
+
installation_id
|
| 446 |
+
)
|
| 447 |
+
sync_result = {
|
| 448 |
+
"queued": True,
|
| 449 |
+
"firebase_id": firebase_id
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
return {
|
| 453 |
"success": True,
|
| 454 |
"message": f"✅ Successfully installed Codesage for {org_name}",
|
| 455 |
"org": org_name,
|
| 456 |
+
"installation_id": installation_id,
|
| 457 |
+
"supabase_sync": sync_result
|
| 458 |
}
|
| 459 |
else:
|
| 460 |
# Save with placeholder - user can update via /github/install
|
|
|
|
| 555 |
@app.post("/insights/commits")
|
| 556 |
def get_commits(data: InsightsRequest):
|
| 557 |
"""
|
| 558 |
+
Get commit history for a repository and store in Supabase
|
| 559 |
+
firebase_id is required and will be passed in request body
|
| 560 |
"""
|
| 561 |
if not github_auth:
|
| 562 |
raise HTTPException(status_code=500, detail="GitHub App not configured")
|
| 563 |
+
if not supabase:
|
| 564 |
+
raise HTTPException(status_code=500, detail="Supabase not configured")
|
| 565 |
|
| 566 |
installation_id = get_installation_id(data.org)
|
| 567 |
if not installation_id:
|
| 568 |
raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
|
| 569 |
|
| 570 |
try:
|
| 571 |
+
# Fetch commits from GitHub
|
| 572 |
token = token_manager.get_token(installation_id)
|
| 573 |
insights = GitHubInsights(token)
|
| 574 |
+
commits = insights.get_commits(data.org, data.repo, per_page=100)
|
| 575 |
+
|
| 576 |
+
# Get github table id for this repo
|
| 577 |
+
full_name = f"{data.org}/{data.repo}"
|
| 578 |
+
github_result = supabase.table("github").select("id").eq("firebase_id", data.firebase_id).eq("full_name", full_name).limit(1).execute()
|
| 579 |
+
|
| 580 |
+
if not github_result.data:
|
| 581 |
+
raise HTTPException(status_code=404, detail=f"Repo {full_name} not found in github table for this user")
|
| 582 |
+
|
| 583 |
+
github_id = github_result.data[0]["id"]
|
| 584 |
+
|
| 585 |
+
# Transform and store commits
|
| 586 |
+
commit_rows = []
|
| 587 |
+
for commit in commits:
|
| 588 |
+
commit_data = commit.get("commit", {})
|
| 589 |
+
author_data = commit_data.get("author", {})
|
| 590 |
+
|
| 591 |
+
commit_rows.append({
|
| 592 |
+
"github_id": github_id,
|
| 593 |
+
"firebase_id": data.firebase_id,
|
| 594 |
+
"sha": commit.get("sha"),
|
| 595 |
+
"message": commit_data.get("message"),
|
| 596 |
+
"author": author_data.get("name"),
|
| 597 |
+
"author_email": author_data.get("email"),
|
| 598 |
+
"committed_date": author_data.get("date"),
|
| 599 |
+
})
|
| 600 |
+
|
| 601 |
+
if commit_rows:
|
| 602 |
+
supabase.table("commits").upsert(commit_rows, on_conflict="firebase_id,sha").execute()
|
| 603 |
+
logger.info(f"Stored {len(commit_rows)} commits for {full_name}")
|
| 604 |
|
| 605 |
return {
|
| 606 |
"org": data.org,
|
| 607 |
"repo": data.repo,
|
| 608 |
"count": len(commits),
|
| 609 |
+
"stored": len(commit_rows),
|
| 610 |
"commits": commits
|
| 611 |
}
|
| 612 |
except Exception as e:
|
| 613 |
+
logger.error(f"Failed to fetch/store commits: {str(e)}")
|
| 614 |
raise HTTPException(status_code=500, detail=f"Failed to fetch commits: {str(e)}")
|
| 615 |
|
| 616 |
|
| 617 |
@app.post("/insights/pull-requests")
|
| 618 |
def get_pull_requests(data: InsightsRequest):
|
| 619 |
+
"""Get pull requests for a repository and store in Supabase
|
| 620 |
+
firebase_id is required and will be passed in request body
|
| 621 |
+
"""
|
| 622 |
if not github_auth:
|
| 623 |
raise HTTPException(status_code=500, detail="GitHub App not configured")
|
| 624 |
+
if not supabase:
|
| 625 |
+
raise HTTPException(status_code=500, detail="Supabase not configured")
|
| 626 |
|
| 627 |
installation_id = get_installation_id(data.org)
|
| 628 |
if not installation_id:
|
| 629 |
raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
|
| 630 |
|
| 631 |
try:
|
| 632 |
+
# Fetch pull requests from GitHub
|
| 633 |
token = token_manager.get_token(installation_id)
|
| 634 |
insights = GitHubInsights(token)
|
| 635 |
+
prs = insights.get_pull_requests(data.org, data.repo, state="all")
|
| 636 |
+
|
| 637 |
+
# Get github table id for this repo
|
| 638 |
+
full_name = f"{data.org}/{data.repo}"
|
| 639 |
+
github_result = supabase.table("github").select("id").eq("firebase_id", data.firebase_id).eq("full_name", full_name).limit(1).execute()
|
| 640 |
+
|
| 641 |
+
if not github_result.data:
|
| 642 |
+
raise HTTPException(status_code=404, detail=f"Repo {full_name} not found in github table for this user")
|
| 643 |
+
|
| 644 |
+
github_id = github_result.data[0]["id"]
|
| 645 |
+
|
| 646 |
+
# Transform and store pull requests
|
| 647 |
+
pr_rows = []
|
| 648 |
+
for pr in prs:
|
| 649 |
+
pr_rows.append({
|
| 650 |
+
"github_id": github_id,
|
| 651 |
+
"firebase_id": data.firebase_id,
|
| 652 |
+
"pr_id": pr.get("id"),
|
| 653 |
+
"pr_number": pr.get("number"),
|
| 654 |
+
"title": pr.get("title"),
|
| 655 |
+
"body": pr.get("body"),
|
| 656 |
+
"state": pr.get("state"),
|
| 657 |
+
"author": pr.get("user", {}).get("login"),
|
| 658 |
+
"created_at": pr.get("created_at"),
|
| 659 |
+
"updated_at": pr.get("updated_at"),
|
| 660 |
+
"merged_at": pr.get("merged_at"),
|
| 661 |
+
"closed_at": pr.get("closed_at"),
|
| 662 |
+
"url": pr.get("html_url"),
|
| 663 |
+
"head_branch": pr.get("head", {}).get("ref"),
|
| 664 |
+
"base_branch": pr.get("base", {}).get("ref"),
|
| 665 |
+
"draft": pr.get("draft", False),
|
| 666 |
+
"mergeable": pr.get("mergeable"),
|
| 667 |
+
"comments": pr.get("comments", 0),
|
| 668 |
+
"commits": pr.get("commits", 0),
|
| 669 |
+
"additions": pr.get("additions", 0),
|
| 670 |
+
"deletions": pr.get("deletions", 0),
|
| 671 |
+
"changed_files": pr.get("changed_files", 0),
|
| 672 |
+
})
|
| 673 |
+
|
| 674 |
+
if pr_rows:
|
| 675 |
+
supabase.table("pull_requests").upsert(pr_rows, on_conflict="firebase_id,pr_id").execute()
|
| 676 |
+
logger.info(f"Stored {len(pr_rows)} pull requests for {full_name}")
|
| 677 |
|
| 678 |
return {
|
| 679 |
"org": data.org,
|
| 680 |
"repo": data.repo,
|
| 681 |
"count": len(prs),
|
| 682 |
+
"stored": len(pr_rows),
|
| 683 |
"pull_requests": prs
|
| 684 |
}
|
| 685 |
except Exception as e:
|
| 686 |
+
logger.error(f"Failed to fetch/store pull requests: {str(e)}")
|
| 687 |
raise HTTPException(status_code=500, detail=f"Failed to fetch pull requests: {str(e)}")
|
| 688 |
|
| 689 |
|
| 690 |
@app.post("/insights/issues")
|
| 691 |
def get_issues(data: InsightsRequest):
|
| 692 |
+
"""Get issues for a repository and store in Supabase
|
| 693 |
+
firebase_id is required and will be passed in request body
|
| 694 |
+
"""
|
| 695 |
if not github_auth:
|
| 696 |
raise HTTPException(status_code=500, detail="GitHub App not configured")
|
| 697 |
+
if not supabase:
|
| 698 |
+
raise HTTPException(status_code=500, detail="Supabase not configured")
|
| 699 |
|
| 700 |
installation_id = get_installation_id(data.org)
|
| 701 |
if not installation_id:
|
| 702 |
raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
|
| 703 |
|
| 704 |
try:
|
| 705 |
+
# Fetch issues from GitHub
|
| 706 |
token = token_manager.get_token(installation_id)
|
| 707 |
insights = GitHubInsights(token)
|
| 708 |
+
issues = insights.get_issues(data.org, data.repo, state="all")
|
| 709 |
+
|
| 710 |
+
# Get github table id for this repo
|
| 711 |
+
full_name = f"{data.org}/{data.repo}"
|
| 712 |
+
github_result = supabase.table("github").select("id").eq("firebase_id", data.firebase_id).eq("full_name", full_name).limit(1).execute()
|
| 713 |
+
|
| 714 |
+
if not github_result.data:
|
| 715 |
+
raise HTTPException(status_code=404, detail=f"Repo {full_name} not found in github table for this user")
|
| 716 |
+
|
| 717 |
+
github_id = github_result.data[0]["id"]
|
| 718 |
+
|
| 719 |
+
# Transform and store issues
|
| 720 |
+
issue_rows = []
|
| 721 |
+
for issue in issues:
|
| 722 |
+
# Skip pull requests (they appear in issues API too)
|
| 723 |
+
if "pull_request" in issue:
|
| 724 |
+
continue
|
| 725 |
+
|
| 726 |
+
issue_rows.append({
|
| 727 |
+
"github_id": github_id,
|
| 728 |
+
"firebase_id": data.firebase_id,
|
| 729 |
+
"issue_id": issue.get("id"),
|
| 730 |
+
"issue_number": issue.get("number"),
|
| 731 |
+
"title": issue.get("title"),
|
| 732 |
+
"body": issue.get("body"),
|
| 733 |
+
"state": issue.get("state"),
|
| 734 |
+
"author": issue.get("user", {}).get("login"),
|
| 735 |
+
"created_at": issue.get("created_at"),
|
| 736 |
+
"updated_at": issue.get("updated_at"),
|
| 737 |
+
"url": issue.get("html_url"),
|
| 738 |
+
"labels": [label.get("name") for label in issue.get("labels", [])],
|
| 739 |
+
"comments": issue.get("comments", 0),
|
| 740 |
+
"reactions": issue.get("reactions", {}).get("total_count", 0),
|
| 741 |
+
})
|
| 742 |
+
|
| 743 |
+
if issue_rows:
|
| 744 |
+
supabase.table("issues").upsert(issue_rows, on_conflict="firebase_id,issue_id").execute()
|
| 745 |
+
logger.info(f"Stored {len(issue_rows)} issues for {full_name}")
|
| 746 |
|
| 747 |
return {
|
| 748 |
"org": data.org,
|
| 749 |
"repo": data.repo,
|
| 750 |
"count": len(issues),
|
| 751 |
+
"stored": len(issue_rows),
|
| 752 |
"issues": issues
|
| 753 |
}
|
| 754 |
except Exception as e:
|
| 755 |
+
logger.error(f"Failed to fetch/store issues: {str(e)}")
|
| 756 |
raise HTTPException(status_code=500, detail=f"Failed to fetch issues: {str(e)}")
|
| 757 |
|
| 758 |
|
| 759 |
+
@app.post("/github/sync")
|
| 760 |
+
def sync_github_to_supabase(data: GitHubSyncRequest):
|
| 761 |
+
"""
|
| 762 |
+
Sync GitHub repo data and rollup stats into Supabase
|
| 763 |
+
Trigger this after user creation with firebase_id
|
| 764 |
+
"""
|
| 765 |
+
if not github_auth:
|
| 766 |
+
raise HTTPException(status_code=500, detail="GitHub App not configured")
|
| 767 |
+
if not supabase:
|
| 768 |
+
raise HTTPException(status_code=500, detail="Supabase not configured")
|
| 769 |
+
|
| 770 |
+
installation_id = get_installation_id(data.org)
|
| 771 |
+
if not installation_id:
|
| 772 |
+
raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
|
| 773 |
+
|
| 774 |
+
try:
|
| 775 |
+
synced = sync_installation_to_supabase(data.firebase_id, data.org, installation_id)
|
| 776 |
+
return {
|
| 777 |
+
"success": True,
|
| 778 |
+
"org": data.org,
|
| 779 |
+
"synced_repos": synced
|
| 780 |
+
}
|
| 781 |
+
except Exception as e:
|
| 782 |
+
raise HTTPException(status_code=500, detail=f"Failed to sync GitHub data: {str(e)}")
|
| 783 |
+
|
| 784 |
+
|
| 785 |
@app.post("/insights/contributors")
|
| 786 |
def get_contributors(data: InsightsRequest):
|
| 787 |
+
"""Get contributors and their stats, store in Supabase
|
| 788 |
+
firebase_id is required and will be passed in request body
|
| 789 |
+
"""
|
| 790 |
if not github_auth:
|
| 791 |
raise HTTPException(status_code=500, detail="GitHub App not configured")
|
| 792 |
+
if not supabase:
|
| 793 |
+
raise HTTPException(status_code=500, detail="Supabase not configured")
|
| 794 |
|
| 795 |
installation_id = get_installation_id(data.org)
|
| 796 |
if not installation_id:
|
| 797 |
raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
|
| 798 |
|
| 799 |
try:
|
| 800 |
+
# Fetch contributors from GitHub
|
| 801 |
token = token_manager.get_token(installation_id)
|
| 802 |
insights = GitHubInsights(token)
|
| 803 |
contributors = insights.get_contributors(data.org, data.repo)
|
| 804 |
|
| 805 |
+
# Get github table id for this repo
|
| 806 |
+
full_name = f"{data.org}/{data.repo}"
|
| 807 |
+
github_result = supabase.table("github").select("id").eq("firebase_id", data.firebase_id).eq("full_name", full_name).limit(1).execute()
|
| 808 |
+
|
| 809 |
+
if not github_result.data:
|
| 810 |
+
raise HTTPException(status_code=404, detail=f"Repo {full_name} not found in github table for this user")
|
| 811 |
+
|
| 812 |
+
github_id = github_result.data[0]["id"]
|
| 813 |
+
|
| 814 |
+
# Transform and store contributors
|
| 815 |
+
contributor_rows = []
|
| 816 |
+
for contributor in contributors:
|
| 817 |
+
contributor_rows.append({
|
| 818 |
+
"github_id": github_id,
|
| 819 |
+
"firebase_id": data.firebase_id,
|
| 820 |
+
"username": contributor.get("login"),
|
| 821 |
+
"contributions": contributor.get("contributions", 0),
|
| 822 |
+
"avatar_url": contributor.get("avatar_url"),
|
| 823 |
+
"profile_url": contributor.get("html_url"),
|
| 824 |
+
})
|
| 825 |
+
|
| 826 |
+
if contributor_rows:
|
| 827 |
+
supabase.table("contributors").upsert(contributor_rows, on_conflict="firebase_id,github_id,username").execute()
|
| 828 |
+
logger.info(f"Stored {len(contributor_rows)} contributors for {full_name}")
|
| 829 |
+
|
| 830 |
return {
|
| 831 |
"org": data.org,
|
| 832 |
"repo": data.repo,
|
| 833 |
"count": len(contributors),
|
| 834 |
+
"stored": len(contributor_rows),
|
| 835 |
"contributors": contributors
|
| 836 |
}
|
| 837 |
except Exception as e:
|
| 838 |
+
logger.error(f"Failed to fetch/store contributors: {str(e)}")
|
| 839 |
raise HTTPException(status_code=500, detail=f"Failed to fetch contributors: {str(e)}")
|
| 840 |
|
| 841 |
|
|
|
|
| 895 |
raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
|
| 896 |
|
| 897 |
try:
|
| 898 |
+
logger.info(f"Fetching activity for {data.org}/{data.repo}")
|
| 899 |
token = token_manager.get_token(installation_id)
|
| 900 |
insights = GitHubInsights(token)
|
| 901 |
|
| 902 |
+
logger.info("Getting code frequency...")
|
| 903 |
code_frequency = insights.get_code_frequency(data.org, data.repo)
|
| 904 |
+
logger.info(f"Code frequency returned: {len(code_frequency) if isinstance(code_frequency, list) else 'not a list'}")
|
| 905 |
+
|
| 906 |
+
logger.info("Getting commit activity...")
|
| 907 |
commit_activity = insights.get_commit_activity(data.org, data.repo)
|
| 908 |
+
logger.info(f"Commit activity returned: {len(commit_activity) if isinstance(commit_activity, list) else 'not a list'}")
|
| 909 |
|
| 910 |
return {
|
| 911 |
"org": data.org,
|
|
|
|
| 914 |
"commit_activity": commit_activity
|
| 915 |
}
|
| 916 |
except Exception as e:
|
| 917 |
+
logger.error(f"Failed to fetch activity: {str(e)}", exc_info=True)
|
| 918 |
raise HTTPException(status_code=500, detail=f"Failed to fetch activity: {str(e)}")
|
| 919 |
|
| 920 |
|
| 921 |
+
@app.post("/github/sync/test")
|
| 922 |
+
def test_sync(data: GitHubSyncRequest):
|
| 923 |
+
"""
|
| 924 |
+
Test endpoint to debug sync issues
|
| 925 |
+
Shows what's happening without background task
|
| 926 |
+
"""
|
| 927 |
+
logger.info(f"Test sync called for firebase_id={data.firebase_id}, org={data.org}")
|
| 928 |
+
|
| 929 |
+
installation_id = get_installation_id(data.org)
|
| 930 |
+
if not installation_id:
|
| 931 |
+
return {
|
| 932 |
+
"success": False,
|
| 933 |
+
"error": f"No installation found for {data.org}",
|
| 934 |
+
"available_orgs": list(load_installations().keys())
|
| 935 |
+
}
|
| 936 |
+
|
| 937 |
+
try:
|
| 938 |
+
synced = sync_installation_to_supabase(data.firebase_id, data.org, installation_id)
|
| 939 |
+
return {
|
| 940 |
+
"success": True,
|
| 941 |
+
"org": data.org,
|
| 942 |
+
"firebase_id": data.firebase_id,
|
| 943 |
+
"synced_repos": synced
|
| 944 |
+
}
|
| 945 |
+
except Exception as e:
|
| 946 |
+
logger.error(f"Test sync failed: {str(e)}", exc_info=True)
|
| 947 |
+
return {
|
| 948 |
+
"success": False,
|
| 949 |
+
"error": str(e),
|
| 950 |
+
"org": data.org,
|
| 951 |
+
"firebase_id": data.firebase_id
|
| 952 |
+
}
|
| 953 |
|
| 954 |
|
| 955 |
|