Spaces:
Sleeping
Sleeping
| """ | |
| Codesage Backend API | |
| FastAPI server for GitHub App integration | |
| Connects desktop app to GitHub securely | |
| """ | |
| from fastapi import FastAPI, HTTPException, Request, BackgroundTasks | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel | |
| from typing import Optional, List, Dict | |
| import json | |
| import os | |
| from pathlib import Path | |
| from dotenv import load_dotenv | |
| from supabase import create_client, Client | |
| import logging | |
| # Configure logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| from github_api import GitHubAppAuth, GitHubInsights, TokenManager | |
| from webhooks import GitHubWebhookHandler | |
| # Load environment variables from .env file | |
| load_dotenv() | |
| # Initialize FastAPI app | |
| app = FastAPI( | |
| title="Codesage Backend API", | |
| description="Enterprise-grade GitHub insights for development teams", | |
| version="1.0.0" | |
| ) | |
| # CORS configuration (allow desktop app to connect) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # In production, specify your desktop app origins | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Load environment variables | |
| APP_ID = os.getenv("GITHUB_APP_ID") | |
| PRIVATE_KEY_PATH = os.getenv("GITHUB_PRIVATE_KEY_PATH", "private-key.pem") | |
| PRIVATE_KEY_CONTENT = PRIVATE_KEY_PATH # Direct private key content | |
| SUPABASE_URL = os.getenv("SUPABASE_URL") | |
| SUPABASE_SECRET_KEY = os.getenv("SUPABASE_SECRET_KEY") | |
| GITHUB_WEBHOOK_SECRET = os.getenv("GITHUB_WEBHOOK_SECRET", "") | |
| DEFAULT_FIREBASE_ID = os.getenv("DEFAULT_FIREBASE_ID", "JDfZoVuGJhdLBEvR7rVCQKBG9r02") | |
| # Initialize GitHub App authentication | |
| if APP_ID: | |
| try: | |
| # Try to get private key from environment variable first (production) | |
| if PRIVATE_KEY_CONTENT: | |
| private_key = PRIVATE_KEY_CONTENT | |
| logger.info("Using GitHub private key from GITHUB_PRIVATE_KEY env var") | |
| # Fallback to file path (development) | |
| elif os.path.exists(PRIVATE_KEY_PATH): | |
| with open(PRIVATE_KEY_PATH, "r") as f: | |
| private_key = f.read() | |
| logger.info(f"Using GitHub private key from file: {PRIVATE_KEY_PATH}") | |
| else: | |
| raise FileNotFoundError(f"Private key not found at {PRIVATE_KEY_PATH} and GITHUB_PRIVATE_KEY env var not set") | |
| github_auth = GitHubAppAuth(APP_ID, private_key) | |
| token_manager = TokenManager(github_auth) | |
| logger.info("✅ GitHub App configured successfully") | |
| except Exception as e: | |
| github_auth = None | |
| token_manager = None | |
| logger.error(f"❌ GitHub App configuration failed: {str(e)}") | |
| else: | |
| github_auth = None | |
| token_manager = None | |
| logger.error("❌ GITHUB_APP_ID not configured") | |
| # Initialize Supabase client | |
| if SUPABASE_URL and SUPABASE_SECRET_KEY: | |
| supabase: Optional[Client] = create_client(SUPABASE_URL, SUPABASE_SECRET_KEY) | |
| else: | |
| supabase = None | |
| print("⚠️ WARNING: Supabase credentials not configured") | |
| # Initialize webhook handler | |
| webhook_handler = GitHubWebhookHandler(supabase, GITHUB_WEBHOOK_SECRET) if supabase else None | |
| # Simple JSON storage for installations | |
| INSTALLATIONS_FILE = "installations.json" | |
| # ============================================================================ | |
| # DATA MODELS | |
| # ============================================================================ | |
| class InstallationCallback(BaseModel): | |
| """Data received from GitHub installation callback""" | |
| installation_id: int | |
| org: str | |
| setup_action: Optional[str] = None | |
| class InsightsRequest(BaseModel): | |
| """Request for repository insights from desktop app""" | |
| firebase_id: str # Now required | |
| org: str | |
| repo: str | |
| class TokenRefreshRequest(BaseModel): | |
| """Request to refresh installation token""" | |
| installation_id: int | |
| class GitHubSyncRequest(BaseModel): | |
| """Request to sync GitHub data into Supabase""" | |
| firebase_id: str | |
| org: str | |
| # ============================================================================ | |
| # UTILITY FUNCTIONS | |
| # ============================================================================ | |
| def load_installations() -> Dict: | |
| """Load installations from JSON file""" | |
| if os.path.exists(INSTALLATIONS_FILE): | |
| with open(INSTALLATIONS_FILE, "r") as f: | |
| return json.load(f) | |
| return {} | |
| def save_installation(org: str, installation_id: int): | |
| """Save installation to JSON file""" | |
| installations = load_installations() | |
| installations[org] = { | |
| "installation_id": installation_id, | |
| "installed_at": None # Could add timestamp | |
| } | |
| with open(INSTALLATIONS_FILE, "w") as f: | |
| json.dump(installations, f, indent=2) | |
| def get_installation_id(org: str) -> Optional[int]: | |
| """Get installation ID for an organization (legacy from file system)""" | |
| installations = load_installations() | |
| org_data = installations.get(org) | |
| if org_data: | |
| return org_data.get("installation_id") | |
| return None | |
| def get_installation_id_for_user(firebase_id: str) -> Optional[int]: | |
| """Get installation ID for a user from Supabase Users table""" | |
| if not supabase: | |
| logger.error("Supabase not configured") | |
| return None | |
| try: | |
| result = supabase.table("Users").select("installation_id").eq("firebase_id", firebase_id).limit(1).execute() | |
| if result.data and result.data[0].get("installation_id"): | |
| return result.data[0]["installation_id"] | |
| except Exception as e: | |
| logger.error(f"Failed to get installation_id for user {firebase_id}: {str(e)}") | |
| return None | |
| def ensure_user_exists(firebase_id: str): | |
| if not supabase: | |
| raise HTTPException(status_code=500, detail="Supabase not configured") | |
| result = supabase.table("Users").select("firebase_id").eq("firebase_id", firebase_id).limit(1).execute() | |
| if not result.data: | |
| raise HTTPException(status_code=404, detail="User not found for firebase_id") | |
| def build_repo_rollup(org: str, repo: str, insights: GitHubInsights) -> Dict: | |
| repo_info = insights.get_repository_info(org, repo) | |
| commits = insights.get_commits(org, repo, per_page=100) | |
| pull_requests = insights.get_pull_requests(org, repo, state="all") | |
| issues = insights.get_issues(org, repo, state="all") | |
| contributors = insights.get_contributors(org, repo) | |
| languages = insights.get_languages(org, repo) | |
| return { | |
| "owner": repo_info.get("owner", {}).get("login"), | |
| "repo": repo_info.get("name"), | |
| "full_name": repo_info.get("full_name"), | |
| "private": repo_info.get("private"), | |
| "default_branch": repo_info.get("default_branch"), | |
| "language": repo_info.get("language"), | |
| "html_url": repo_info.get("html_url"), | |
| "updated_at": repo_info.get("updated_at"), | |
| "pushed_at": repo_info.get("pushed_at"), | |
| "description": repo_info.get("description"), | |
| "stars": repo_info.get("stargazers_count", 0), | |
| "forks": repo_info.get("forks_count", 0), | |
| "open_issues": repo_info.get("open_issues_count", 0), | |
| "created_at": repo_info.get("created_at"), | |
| "size_kb": repo_info.get("size", 0), | |
| "commit_count": len(commits), | |
| "pr_count": len(pull_requests), | |
| "issue_count": len(issues), | |
| "contributor_count": len(contributors), | |
| "languages_json": languages, | |
| } | |
| def sync_installation_to_supabase(firebase_id: str, org: str, installation_id: int) -> int: | |
| if not github_auth: | |
| logger.error("GitHub App not configured") | |
| raise HTTPException(status_code=500, detail="GitHub App not configured") | |
| if not supabase: | |
| logger.error("Supabase not configured") | |
| raise HTTPException(status_code=500, detail="Supabase not configured") | |
| try: | |
| logger.info(f"Starting sync for firebase_id={firebase_id}, org={org}, installation_id={installation_id}") | |
| # Check if user exists | |
| logger.info(f"Checking if user exists with firebase_id={firebase_id}") | |
| result = supabase.table("Users").select("firebase_id").eq("firebase_id", firebase_id).limit(1).execute() | |
| if not result.data: | |
| logger.error(f"User not found in Supabase for firebase_id={firebase_id}") | |
| return 0 | |
| token = token_manager.get_token(installation_id) | |
| repos = github_auth.get_installation_repositories(token) | |
| logger.info(f"Found {len(repos)} repositories for {org}") | |
| insights = GitHubInsights(token) | |
| # Build repo rollups | |
| rows = [] | |
| for i, repo in enumerate(repos): | |
| try: | |
| repo_name = repo.get("name") | |
| logger.info(f"Processing repo {i+1}/{len(repos)}: {repo_name}") | |
| rollup = build_repo_rollup(org, repo_name, insights) | |
| rollup["firebase_id"] = firebase_id | |
| rollup["installation_id"] = installation_id | |
| rows.append(rollup) | |
| except Exception as e: | |
| logger.warning(f"Failed to process repo {repo_name}: {str(e)}") | |
| continue | |
| if rows: | |
| logger.info(f"Upserting {len(rows)} repos into github table") | |
| supabase.table("github").upsert(rows, on_conflict="firebase_id,full_name").execute() | |
| logger.info(f"Successfully synced {len(rows)} repos to github table") | |
| # Now sync detail data for each repo | |
| for i, repo in enumerate(repos): | |
| try: | |
| repo_name = repo.get("name") | |
| full_name = f"{org}/{repo_name}" | |
| logger.info(f"Syncing details for repo {i+1}/{len(repos)}: {repo_name}") | |
| # Get github_id | |
| github_result = supabase.table("github").select("id").eq("firebase_id", firebase_id).eq("full_name", full_name).limit(1).execute() | |
| if not github_result.data: | |
| logger.warning(f"Could not find {full_name} in github table, skipping details") | |
| continue | |
| github_id = github_result.data[0]["id"] | |
| # Sync commits | |
| try: | |
| commits = insights.get_commits(org, repo_name, per_page=100) | |
| commit_rows = [] | |
| for commit in commits: | |
| commit_data = commit.get("commit", {}) | |
| author_data = commit_data.get("author", {}) | |
| commit_rows.append({ | |
| "github_id": github_id, | |
| "firebase_id": firebase_id, | |
| "sha": commit.get("sha"), | |
| "message": commit_data.get("message"), | |
| "author": author_data.get("name"), | |
| "author_email": author_data.get("email"), | |
| "committed_date": author_data.get("date"), | |
| }) | |
| if commit_rows: | |
| supabase.table("commits").upsert(commit_rows, on_conflict="firebase_id,sha").execute() | |
| logger.info(f" Stored {len(commit_rows)} commits") | |
| except Exception as e: | |
| logger.warning(f" Failed to sync commits: {str(e)}") | |
| # Sync issues | |
| try: | |
| issues = insights.get_issues(org, repo_name, state="all") | |
| issue_rows = [] | |
| for issue in issues: | |
| if "pull_request" in issue: | |
| continue | |
| issue_rows.append({ | |
| "github_id": github_id, | |
| "firebase_id": firebase_id, | |
| "issue_id": issue.get("id"), | |
| "issue_number": issue.get("number"), | |
| "title": issue.get("title"), | |
| "body": issue.get("body"), | |
| "state": issue.get("state"), | |
| "author": issue.get("user", {}).get("login"), | |
| "created_at": issue.get("created_at"), | |
| "updated_at": issue.get("updated_at"), | |
| "url": issue.get("html_url"), | |
| "labels": [label.get("name") for label in issue.get("labels", [])], | |
| "comments": issue.get("comments", 0), | |
| "reactions": issue.get("reactions", {}).get("total_count", 0), | |
| }) | |
| if issue_rows: | |
| supabase.table("issues").upsert(issue_rows, on_conflict="firebase_id,issue_id").execute() | |
| logger.info(f" Stored {len(issue_rows)} issues") | |
| except Exception as e: | |
| logger.warning(f" Failed to sync issues: {str(e)}") | |
| # Sync pull requests | |
| try: | |
| prs = insights.get_pull_requests(org, repo_name, state="all") | |
| pr_rows = [] | |
| for pr in prs: | |
| pr_rows.append({ | |
| "github_id": github_id, | |
| "firebase_id": firebase_id, | |
| "pr_id": pr.get("id"), | |
| "pr_number": pr.get("number"), | |
| "title": pr.get("title"), | |
| "body": pr.get("body"), | |
| "state": pr.get("state"), | |
| "author": pr.get("user", {}).get("login"), | |
| "created_at": pr.get("created_at"), | |
| "updated_at": pr.get("updated_at"), | |
| "merged_at": pr.get("merged_at"), | |
| "closed_at": pr.get("closed_at"), | |
| "url": pr.get("html_url"), | |
| "head_branch": pr.get("head", {}).get("ref"), | |
| "base_branch": pr.get("base", {}).get("ref"), | |
| "draft": pr.get("draft", False), | |
| "mergeable": pr.get("mergeable"), | |
| "comments": pr.get("comments", 0), | |
| "commits": pr.get("commits", 0), | |
| "additions": pr.get("additions", 0), | |
| "deletions": pr.get("deletions", 0), | |
| "changed_files": pr.get("changed_files", 0), | |
| }) | |
| if pr_rows: | |
| supabase.table("pull_requests").upsert(pr_rows, on_conflict="firebase_id,pr_id").execute() | |
| logger.info(f" Stored {len(pr_rows)} pull requests") | |
| except Exception as e: | |
| logger.warning(f" Failed to sync pull requests: {str(e)}") | |
| # Sync contributors | |
| try: | |
| contributors = insights.get_contributors(org, repo_name) | |
| contributor_rows = [] | |
| for contributor in contributors: | |
| contributor_rows.append({ | |
| "github_id": github_id, | |
| "firebase_id": firebase_id, | |
| "username": contributor.get("login"), | |
| "contributions": contributor.get("contributions", 0), | |
| "avatar_url": contributor.get("avatar_url"), | |
| "profile_url": contributor.get("html_url"), | |
| }) | |
| if contributor_rows: | |
| supabase.table("contributors").upsert(contributor_rows, on_conflict="firebase_id,github_id,username").execute() | |
| logger.info(f" Stored {len(contributor_rows)} contributors") | |
| except Exception as e: | |
| logger.warning(f" Failed to sync contributors: {str(e)}") | |
| except Exception as e: | |
| logger.warning(f"Failed to sync details for {repo_name}: {str(e)}") | |
| continue | |
| logger.info(f"Completed full sync for {len(rows)} repos") | |
| else: | |
| logger.warning("No repos to sync") | |
| return len(rows) | |
| except Exception as e: | |
| logger.error(f"Sync failed: {str(e)}", exc_info=True) | |
| return 0 | |
| # ============================================================================ | |
| # API ENDPOINTS | |
| # ============================================================================ | |
| def root(): | |
| """Health check endpoint""" | |
| return { | |
| "service": "Codesage Backend API", | |
| "status": "operational", | |
| "configured": github_auth is not None | |
| } | |
| def health_check(): | |
| """Detailed health check""" | |
| return { | |
| "status": "healthy", | |
| "github_app_configured": github_auth is not None, | |
| "installations_count": len(load_installations()) | |
| } | |
| async def github_callback(request: Request, background_tasks: BackgroundTasks): | |
| """ | |
| Handle GitHub App installation callback | |
| After org installs Codesage, GitHub redirects here with installation_id and state (firebase_uid) | |
| Frontend should redirect to: https://github.com/apps/{app_slug}/installations/new?state={firebase_uid} | |
| GitHub redirects back with: /github/callback?installation_id=...&state={firebase_uid} | |
| """ | |
| # Get query parameters | |
| query_params = dict(request.query_params) | |
| installation_id = query_params.get("installation_id") | |
| setup_action = query_params.get("setup_action") | |
| firebase_id = query_params.get("state") # GitHub passes firebase_uid via state parameter | |
| # Fallback to default if state not provided (for backward compatibility) | |
| if not firebase_id: | |
| logger.warning("No firebase_id in state parameter, using default") | |
| firebase_id = DEFAULT_FIREBASE_ID | |
| if not installation_id: | |
| return { | |
| "success": False, | |
| "message": "Missing installation_id parameter", | |
| "received_params": query_params | |
| } | |
| try: | |
| installation_id = int(installation_id) | |
| except ValueError: | |
| return { | |
| "success": False, | |
| "message": "Invalid installation_id format", | |
| "installation_id": installation_id | |
| } | |
| # Try to get org name if credentials are configured | |
| org_name = None | |
| if github_auth: | |
| try: | |
| # Get installation token to verify and fetch org info | |
| token_data = github_auth.get_installation_token(installation_id) | |
| token = token_data["token"] | |
| # Get installation repositories to extract org name | |
| repos = github_auth.get_installation_repositories(token) | |
| if repos: | |
| org_name = repos[0]["owner"]["login"] | |
| except Exception as e: | |
| print(f"⚠️ Could not authenticate with GitHub: {str(e)}") | |
| print(f" Installation ID {installation_id} captured but org name unknown") | |
| # Save with org name if we got it, otherwise use placeholder | |
| if org_name: | |
| save_installation(org_name, installation_id) | |
| sync_result = None | |
| if supabase: | |
| # Save installation_id to Users table | |
| try: | |
| supabase.table("Users").update({"installation_id": installation_id}).eq("firebase_id", firebase_id).execute() | |
| logger.info(f"Saved installation_id {installation_id} for user {firebase_id}") | |
| except Exception as e: | |
| logger.error(f"Failed to save installation_id to Users table: {str(e)}") | |
| # Run heavy sync in background so callback returns immediately | |
| background_tasks.add_task( | |
| sync_installation_to_supabase, | |
| firebase_id, # Use firebase_id from state parameter | |
| org_name, | |
| installation_id | |
| ) | |
| sync_result = { | |
| "queued": True, | |
| "firebase_id": firebase_id | |
| } | |
| return { | |
| "success": True, | |
| "message": f"✅ Successfully installed Codesage for {org_name}", | |
| "org": org_name, | |
| "installation_id": installation_id, | |
| "supabase_sync": sync_result | |
| } | |
| else: | |
| # Save with placeholder - user can update via /github/install | |
| placeholder = f"installation_{installation_id}" | |
| save_installation(placeholder, installation_id) | |
| return { | |
| "success": True, | |
| "message": "✅ Installation ID captured! Use /github/install to register with org name", | |
| "installation_id": installation_id, | |
| "note": f"Saved as '{placeholder}'. Update your .env file with correct private key path and use POST /github/install to register with org name" | |
| } | |
| def register_installation(data: InstallationCallback): | |
| """ | |
| Manually register an installation (alternative to callback) | |
| Use this during development or for manual setup | |
| """ | |
| save_installation(data.org, data.installation_id) | |
| return { | |
| "success": True, | |
| "message": f"Installation registered for {data.org}", | |
| "installation_id": data.installation_id | |
| } | |
| async def handle_github_webhook(request: Request): | |
| """ | |
| Handle GitHub webhook events in real-time | |
| GitHub sends POST requests here when repository events occur: | |
| - push: New commits pushed | |
| - pull_request: PR created/updated/merged | |
| - issues: Issue created/updated/closed | |
| - repository: Repository created/deleted/renamed | |
| Verify signature header: X-Hub-Signature-256 | |
| Event type header: X-GitHub-Event | |
| """ | |
| if not webhook_handler: | |
| return { | |
| "success": False, | |
| "message": "Webhook handler not configured (Supabase not available)" | |
| } | |
| try: | |
| # Get headers | |
| signature = request.headers.get("X-Hub-Signature-256") | |
| event_type = request.headers.get("X-GitHub-Event") | |
| if not signature or not event_type: | |
| return { | |
| "success": False, | |
| "message": "Missing required headers (X-Hub-Signature-256, X-GitHub-Event)" | |
| } | |
| # Get raw body for signature verification | |
| body = await request.body() | |
| # Verify signature | |
| if not webhook_handler.verify_signature(body, signature): | |
| logger.warning(f"Invalid webhook signature for {event_type}") | |
| return { | |
| "success": False, | |
| "message": "Invalid signature" | |
| } | |
| # Parse payload | |
| payload = await request.json() | |
| # Route to handler | |
| result = webhook_handler.handle_webhook(event_type, payload) | |
| return result | |
| except Exception as e: | |
| logger.error(f"Failed to process webhook: {str(e)}", exc_info=True) | |
| return { | |
| "success": False, | |
| "message": f"Failed to process webhook: {str(e)}" | |
| } | |
| def list_installations(): | |
| """ | |
| List all registered installations | |
| Shows which orgs have installed Codesage | |
| """ | |
| installations = load_installations() | |
| return { | |
| "count": len(installations), | |
| "installations": installations | |
| } | |
| def get_org_installation(org: str): | |
| """Get installation details for a specific org""" | |
| installation_id = get_installation_id(org) | |
| if not installation_id: | |
| raise HTTPException(status_code=404, detail=f"No installation found for {org}") | |
| return { | |
| "org": org, | |
| "installation_id": installation_id | |
| } | |
| def list_org_repositories(org: str): | |
| """ | |
| List all repositories accessible for an org | |
| This is what the desktop app calls first | |
| """ | |
| if not github_auth: | |
| raise HTTPException(status_code=500, detail="GitHub App not configured") | |
| installation_id = get_installation_id(org) | |
| if not installation_id: | |
| raise HTTPException(status_code=404, detail=f"No installation found for {org}") | |
| try: | |
| # Get fresh token | |
| token = token_manager.get_token(installation_id) | |
| # Fetch repositories | |
| repos = github_auth.get_installation_repositories(token) | |
| # Return simplified list | |
| return { | |
| "org": org, | |
| "count": len(repos), | |
| "repositories": [ | |
| { | |
| "name": repo["name"], | |
| "full_name": repo["full_name"], | |
| "private": repo["private"], | |
| "default_branch": repo.get("default_branch"), | |
| "language": repo.get("language"), | |
| "updated_at": repo.get("updated_at") | |
| } | |
| for repo in repos | |
| ] | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Failed to fetch repositories: {str(e)}") | |
| def get_commits(data: InsightsRequest): | |
| """ | |
| Get commit history for a repository and store in Supabase | |
| firebase_id is required and will be passed in request body | |
| """ | |
| if not github_auth: | |
| raise HTTPException(status_code=500, detail="GitHub App not configured") | |
| if not supabase: | |
| raise HTTPException(status_code=500, detail="Supabase not configured") | |
| installation_id = get_installation_id_for_user(data.firebase_id) | |
| if not installation_id: | |
| raise HTTPException(status_code=404, detail=f"No GitHub installation found for user {data.firebase_id}") | |
| try: | |
| # Fetch commits from GitHub | |
| token = token_manager.get_token(installation_id) | |
| insights = GitHubInsights(token) | |
| commits = insights.get_commits(data.org, data.repo, per_page=100) | |
| # Get github table id for this repo | |
| full_name = f"{data.org}/{data.repo}" | |
| github_result = supabase.table("github").select("id").eq("firebase_id", data.firebase_id).eq("full_name", full_name).limit(1).execute() | |
| if not github_result.data: | |
| raise HTTPException(status_code=404, detail=f"Repo {full_name} not found in github table for this user") | |
| github_id = github_result.data[0]["id"] | |
| # Transform and store commits | |
| commit_rows = [] | |
| for commit in commits: | |
| commit_data = commit.get("commit", {}) | |
| author_data = commit_data.get("author", {}) | |
| commit_rows.append({ | |
| "github_id": github_id, | |
| "firebase_id": data.firebase_id, | |
| "sha": commit.get("sha"), | |
| "message": commit_data.get("message"), | |
| "author": author_data.get("name"), | |
| "author_email": author_data.get("email"), | |
| "committed_date": author_data.get("date"), | |
| }) | |
| if commit_rows: | |
| supabase.table("commits").upsert(commit_rows, on_conflict="firebase_id,sha").execute() | |
| logger.info(f"Stored {len(commit_rows)} commits for {full_name}") | |
| return { | |
| "org": data.org, | |
| "repo": data.repo, | |
| "count": len(commits), | |
| "stored": len(commit_rows), | |
| "commits": commits | |
| } | |
| except Exception as e: | |
| logger.error(f"Failed to fetch/store commits: {str(e)}") | |
| raise HTTPException(status_code=500, detail=f"Failed to fetch commits: {str(e)}") | |
| def get_pull_requests(data: InsightsRequest): | |
| """Get pull requests for a repository and store in Supabase | |
| firebase_id is required and will be passed in request body | |
| """ | |
| if not github_auth: | |
| raise HTTPException(status_code=500, detail="GitHub App not configured") | |
| if not supabase: | |
| raise HTTPException(status_code=500, detail="Supabase not configured") | |
| installation_id = get_installation_id_for_user(data.firebase_id) | |
| if not installation_id: | |
| raise HTTPException(status_code=404, detail=f"No GitHub installation found for user {data.firebase_id}") | |
| try: | |
| # Fetch pull requests from GitHub | |
| token = token_manager.get_token(installation_id) | |
| insights = GitHubInsights(token) | |
| prs = insights.get_pull_requests(data.org, data.repo, state="all") | |
| # Get github table id for this repo | |
| full_name = f"{data.org}/{data.repo}" | |
| github_result = supabase.table("github").select("id").eq("firebase_id", data.firebase_id).eq("full_name", full_name).limit(1).execute() | |
| if not github_result.data: | |
| raise HTTPException(status_code=404, detail=f"Repo {full_name} not found in github table for this user") | |
| github_id = github_result.data[0]["id"] | |
| # Transform and store pull requests | |
| pr_rows = [] | |
| for pr in prs: | |
| pr_rows.append({ | |
| "github_id": github_id, | |
| "firebase_id": data.firebase_id, | |
| "pr_id": pr.get("id"), | |
| "pr_number": pr.get("number"), | |
| "title": pr.get("title"), | |
| "body": pr.get("body"), | |
| "state": pr.get("state"), | |
| "author": pr.get("user", {}).get("login"), | |
| "created_at": pr.get("created_at"), | |
| "updated_at": pr.get("updated_at"), | |
| "merged_at": pr.get("merged_at"), | |
| "closed_at": pr.get("closed_at"), | |
| "url": pr.get("html_url"), | |
| "head_branch": pr.get("head", {}).get("ref"), | |
| "base_branch": pr.get("base", {}).get("ref"), | |
| "draft": pr.get("draft", False), | |
| "mergeable": pr.get("mergeable"), | |
| "comments": pr.get("comments", 0), | |
| "commits": pr.get("commits", 0), | |
| "additions": pr.get("additions", 0), | |
| "deletions": pr.get("deletions", 0), | |
| "changed_files": pr.get("changed_files", 0), | |
| }) | |
| if pr_rows: | |
| supabase.table("pull_requests").upsert(pr_rows, on_conflict="firebase_id,pr_id").execute() | |
| logger.info(f"Stored {len(pr_rows)} pull requests for {full_name}") | |
| return { | |
| "org": data.org, | |
| "repo": data.repo, | |
| "count": len(prs), | |
| "stored": len(pr_rows), | |
| "pull_requests": prs | |
| } | |
| except Exception as e: | |
| logger.error(f"Failed to fetch/store pull requests: {str(e)}") | |
| raise HTTPException(status_code=500, detail=f"Failed to fetch pull requests: {str(e)}") | |
| def get_issues(data: InsightsRequest): | |
| """Get issues for a repository and store in Supabase | |
| firebase_id is required and will be passed in request body | |
| """ | |
| if not github_auth: | |
| raise HTTPException(status_code=500, detail="GitHub App not configured") | |
| if not supabase: | |
| raise HTTPException(status_code=500, detail="Supabase not configured") | |
| installation_id = get_installation_id_for_user(data.firebase_id) | |
| if not installation_id: | |
| raise HTTPException(status_code=404, detail=f"No GitHub installation found for user {data.firebase_id}") | |
| try: | |
| # Fetch issues from GitHub | |
| token = token_manager.get_token(installation_id) | |
| insights = GitHubInsights(token) | |
| issues = insights.get_issues(data.org, data.repo, state="all") | |
| # Get github table id for this repo | |
| full_name = f"{data.org}/{data.repo}" | |
| github_result = supabase.table("github").select("id").eq("firebase_id", data.firebase_id).eq("full_name", full_name).limit(1).execute() | |
| if not github_result.data: | |
| raise HTTPException(status_code=404, detail=f"Repo {full_name} not found in github table for this user") | |
| github_id = github_result.data[0]["id"] | |
| # Transform and store issues | |
| issue_rows = [] | |
| for issue in issues: | |
| # Skip pull requests (they appear in issues API too) | |
| if "pull_request" in issue: | |
| continue | |
| issue_rows.append({ | |
| "github_id": github_id, | |
| "firebase_id": data.firebase_id, | |
| "issue_id": issue.get("id"), | |
| "issue_number": issue.get("number"), | |
| "title": issue.get("title"), | |
| "body": issue.get("body"), | |
| "state": issue.get("state"), | |
| "author": issue.get("user", {}).get("login"), | |
| "created_at": issue.get("created_at"), | |
| "updated_at": issue.get("updated_at"), | |
| "url": issue.get("html_url"), | |
| "labels": [label.get("name") for label in issue.get("labels", [])], | |
| "comments": issue.get("comments", 0), | |
| "reactions": issue.get("reactions", {}).get("total_count", 0), | |
| }) | |
| if issue_rows: | |
| supabase.table("issues").upsert(issue_rows, on_conflict="firebase_id,issue_id").execute() | |
| logger.info(f"Stored {len(issue_rows)} issues for {full_name}") | |
| return { | |
| "org": data.org, | |
| "repo": data.repo, | |
| "count": len(issues), | |
| "stored": len(issue_rows), | |
| "issues": issues | |
| } | |
| except Exception as e: | |
| logger.error(f"Failed to fetch/store issues: {str(e)}") | |
| raise HTTPException(status_code=500, detail=f"Failed to fetch issues: {str(e)}") | |
| def sync_github_to_supabase(data: GitHubSyncRequest): | |
| """ | |
| Sync GitHub repo data and rollup stats into Supabase | |
| Trigger this after user creation with firebase_id | |
| """ | |
| if not github_auth: | |
| raise HTTPException(status_code=500, detail="GitHub App not configured") | |
| if not supabase: | |
| raise HTTPException(status_code=500, detail="Supabase not configured") | |
| installation_id = get_installation_id_for_user(data.firebase_id) | |
| if not installation_id: | |
| raise HTTPException(status_code=404, detail=f"No GitHub installation found for user {data.firebase_id}") | |
| try: | |
| synced = sync_installation_to_supabase(data.firebase_id, data.org, installation_id) | |
| return { | |
| "success": True, | |
| "org": data.org, | |
| "synced_repos": synced | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Failed to sync GitHub data: {str(e)}") | |
| def get_contributors(data: InsightsRequest): | |
| """Get contributors and their stats, store in Supabase | |
| firebase_id is required and will be passed in request body | |
| """ | |
| if not github_auth: | |
| raise HTTPException(status_code=500, detail="GitHub App not configured") | |
| if not supabase: | |
| raise HTTPException(status_code=500, detail="Supabase not configured") | |
| installation_id = get_installation_id_for_user(data.firebase_id) | |
| if not installation_id: | |
| raise HTTPException(status_code=404, detail=f"No GitHub installation found for user {data.firebase_id}") | |
| try: | |
| # Fetch contributors from GitHub | |
| token = token_manager.get_token(installation_id) | |
| insights = GitHubInsights(token) | |
| contributors = insights.get_contributors(data.org, data.repo) | |
| # Get github table id for this repo | |
| full_name = f"{data.org}/{data.repo}" | |
| github_result = supabase.table("github").select("id").eq("firebase_id", data.firebase_id).eq("full_name", full_name).limit(1).execute() | |
| if not github_result.data: | |
| raise HTTPException(status_code=404, detail=f"Repo {full_name} not found in github table for this user") | |
| github_id = github_result.data[0]["id"] | |
| # Transform and store contributors | |
| contributor_rows = [] | |
| for contributor in contributors: | |
| contributor_rows.append({ | |
| "github_id": github_id, | |
| "firebase_id": data.firebase_id, | |
| "username": contributor.get("login"), | |
| "contributions": contributor.get("contributions", 0), | |
| "avatar_url": contributor.get("avatar_url"), | |
| "profile_url": contributor.get("html_url"), | |
| }) | |
| if contributor_rows: | |
| supabase.table("contributors").upsert(contributor_rows, on_conflict="firebase_id,github_id,username").execute() | |
| logger.info(f"Stored {len(contributor_rows)} contributors for {full_name}") | |
| return { | |
| "org": data.org, | |
| "repo": data.repo, | |
| "count": len(contributors), | |
| "stored": len(contributor_rows), | |
| "contributors": contributors | |
| } | |
| except Exception as e: | |
| logger.error(f"Failed to fetch/store contributors: {str(e)}") | |
| raise HTTPException(status_code=500, detail=f"Failed to fetch contributors: {str(e)}") | |
| def get_repository_overview(data: InsightsRequest): | |
| """ | |
| Get comprehensive repository overview | |
| Combines multiple insights into one powerful response | |
| """ | |
| if not github_auth: | |
| raise HTTPException(status_code=500, detail="GitHub App not configured") | |
| installation_id = get_installation_id_for_user(data.firebase_id) | |
| if not installation_id: | |
| raise HTTPException(status_code=404, detail=f"No GitHub installation found for user {data.firebase_id}") | |
| try: | |
| token = token_manager.get_token(installation_id) | |
| insights = GitHubInsights(token) | |
| # Fetch multiple insights | |
| repo_info = insights.get_repository_info(data.org, data.repo) | |
| languages = insights.get_languages(data.org, data.repo) | |
| contributors = insights.get_contributors(data.org, data.repo) | |
| recent_commits = insights.get_commits(data.org, data.repo, per_page=10) | |
| return { | |
| "org": data.org, | |
| "repo": data.repo, | |
| "overview": { | |
| "name": repo_info.get("name"), | |
| "description": repo_info.get("description"), | |
| "stars": repo_info.get("stargazers_count"), | |
| "forks": repo_info.get("forks_count"), | |
| "open_issues": repo_info.get("open_issues_count"), | |
| "default_branch": repo_info.get("default_branch"), | |
| "created_at": repo_info.get("created_at"), | |
| "updated_at": repo_info.get("updated_at"), | |
| "size": repo_info.get("size"), | |
| "languages": languages, | |
| "contributors_count": len(contributors), | |
| "recent_commits_count": len(recent_commits) | |
| } | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Failed to fetch overview: {str(e)}") | |
| def get_repository_activity(data: InsightsRequest): | |
| """Get repository activity stats (code frequency, commit activity)""" | |
| if not github_auth: | |
| raise HTTPException(status_code=500, detail="GitHub App not configured") | |
| installation_id = get_installation_id_for_user(data.firebase_id) | |
| if not installation_id: | |
| raise HTTPException(status_code=404, detail=f"No GitHub installation found for user {data.firebase_id}") | |
| try: | |
| logger.info(f"Fetching activity for {data.org}/{data.repo}") | |
| token = token_manager.get_token(installation_id) | |
| insights = GitHubInsights(token) | |
| logger.info("Getting code frequency...") | |
| code_frequency = insights.get_code_frequency(data.org, data.repo) | |
| logger.info(f"Code frequency returned: {len(code_frequency) if isinstance(code_frequency, list) else 'not a list'}") | |
| logger.info("Getting commit activity...") | |
| commit_activity = insights.get_commit_activity(data.org, data.repo) | |
| logger.info(f"Commit activity returned: {len(commit_activity) if isinstance(commit_activity, list) else 'not a list'}") | |
| return { | |
| "org": data.org, | |
| "repo": data.repo, | |
| "code_frequency": code_frequency, | |
| "commit_activity": commit_activity | |
| } | |
| except Exception as e: | |
| logger.error(f"Failed to fetch activity: {str(e)}", exc_info=True) | |
| raise HTTPException(status_code=500, detail=f"Failed to fetch activity: {str(e)}") | |
| def test_sync(data: GitHubSyncRequest): | |
| """ | |
| Test endpoint to debug sync issues | |
| Shows what's happening without background task | |
| """ | |
| logger.info(f"Test sync called for firebase_id={data.firebase_id}, org={data.org}") | |
| installation_id = get_installation_id_for_user(data.firebase_id) | |
| if not installation_id: | |
| return { | |
| "success": False, | |
| "error": f"No GitHub installation found for user {data.firebase_id}", | |
| "available_orgs": list(load_installations().keys()) | |
| } | |
| try: | |
| synced = sync_installation_to_supabase(data.firebase_id, data.org, installation_id) | |
| return { | |
| "success": True, | |
| "org": data.org, | |
| "firebase_id": data.firebase_id, | |
| "synced_repos": synced | |
| } | |
| except Exception as e: | |
| logger.error(f"Test sync failed: {str(e)}", exc_info=True) | |
| return { | |
| "success": False, | |
| "error": str(e), | |
| "org": data.org, | |
| "firebase_id": data.firebase_id | |
| } | |