Download app/utils/github_client.py from UNI12345/ARCHIVISE: direct link, hf CLI and curl.
- Browser
- Download file 6.46 kB
-
https://huggingface.co/spaces/UNI12345/ARCHIVISE/resolve/main/app/utils/github_client.py
- Command line
-
hf download hf://spaces/UNI12345/ARCHIVISE/app/utils/github_client.py
-
curl -L -o github_client.py https://huggingface.co/spaces/UNI12345/ARCHIVISE/resolve/main/app/utils/github_client.py
6.46 kB
| import base64 | |
| import hashlib | |
| import re | |
| from typing import List, Dict, Any | |
| import httpx | |
| from github import Github | |
| from cryptography.fernet import Fernet | |
| from app.config import settings | |
| from loguru import logger | |
| # Initialize encryption cipher using a key derived from SECRET_KEY | |
| def _get_fernet_cipher() -> Fernet: | |
| # Fernet requires a 32-byte base64-encoded key | |
| key_hash = hashlib.sha256(settings.SECRET_KEY.encode()).digest() | |
| fernet_key = base64.urlsafe_b64encode(key_hash) | |
| return Fernet(fernet_key) | |
| def encrypt_token(token: str) -> str: | |
| cipher = _get_fernet_cipher() | |
| return cipher.encrypt(token.encode()).decode() | |
| def decrypt_token(encrypted_token: str) -> str: | |
| cipher = _get_fernet_cipher() | |
| return cipher.decrypt(encrypted_token.encode()).decode() | |
| def get_github_auth_url(state: str) -> str: | |
| redirect_uri = f"{settings.FRONTEND_URL}/api/auth/callback" | |
| return ( | |
| f"https://github.com/login/oauth/authorize" | |
| f"?client_id={settings.GITHUB_CLIENT_ID}" | |
| f"&redirect_uri={redirect_uri}" | |
| f"&state={state}" | |
| f"&scope=repo,read:org" | |
| ) | |
| async def exchange_code_for_token(code: str) -> dict: | |
| url = "https://github.com/login/oauth/access_token" | |
| headers = {"Accept": "application/json"} | |
| data = { | |
| "client_id": settings.GITHUB_CLIENT_ID, | |
| "client_secret": settings.GITHUB_CLIENT_SECRET, | |
| "code": code | |
| } | |
| async with httpx.AsyncClient() as client: | |
| response = await client.post(url, headers=headers, data=data) | |
| response.raise_for_status() | |
| return response.json() | |
| async def get_github_user_info(access_token: str) -> dict: | |
| url = "https://api.github.com/user" | |
| headers = { | |
| "Authorization": f"token {access_token}", | |
| "Accept": "application/vnd.github.v3+json" | |
| } | |
| async with httpx.AsyncClient() as client: | |
| response = await client.get(url, headers=headers) | |
| response.raise_for_status() | |
| return response.json() | |
| def get_user_repos(access_token: str) -> List[Dict[str, Any]]: | |
| try: | |
| g = Github(access_token) | |
| repos = [] | |
| # Get repos sorted by updated date | |
| for repo in g.get_user().get_repos(sort="updated", direction="desc"): | |
| repos.append({ | |
| "id": repo.id, | |
| "name": repo.name, | |
| "full_name": repo.full_name, | |
| "html_url": repo.html_url, | |
| "description": repo.description, | |
| "language": repo.language, | |
| "updated_at": repo.updated_at.isoformat() if repo.updated_at else "", | |
| "private": repo.private | |
| }) | |
| return repos | |
| except Exception as e: | |
| logger.error(f"Failed to fetch repos from GitHub: {e}") | |
| return [] | |
| def get_repo_branches(access_token: str, repo_full_name: str) -> List[str]: | |
| try: | |
| g = Github(access_token) | |
| repo = g.get_repo(repo_full_name) | |
| return [b.name for b in repo.get_branches()] | |
| except Exception as e: | |
| logger.error(f"Failed to fetch branches for {repo_full_name}: {e}") | |
| return [] | |
| # File Extensions Whitelist | |
| ALLOWED_EXTENSIONS = { | |
| ".py", ".js", ".ts", ".jsx", ".tsx", ".json", ".yaml", ".yml", | |
| ".toml", ".html", ".css", ".sql", ".go", ".java", ".rb" | |
| } | |
| def get_repo_files(access_token: str, repo_full_name: str, branch: str) -> List[Dict[str, str]]: | |
| try: | |
| g = Github(access_token) | |
| repo = g.get_repo(repo_full_name) | |
| # Get files recursively up to limits | |
| files = [] | |
| total_size = 0 | |
| max_size = 50 * 1024 * 1024 # 50MB limit | |
| # Get git tree recursively | |
| git_ref = repo.get_git_ref(f"heads/{branch}") | |
| tree = repo.get_git_tree(git_ref.object.sha, recursive=True) | |
| for element in tree.tree: | |
| if element.type == "blob": | |
| # Check extension | |
| import os | |
| _, ext = os.path.splitext(element.path) | |
| if ext.lower() in ALLOWED_EXTENSIONS: | |
| # Ignore standard vendor/lock folders to stay within context limits | |
| if any(p in element.path.split("/") for p in ["node_modules", "vendor", "dist", "build", ".git", "venv", ".venv"]): | |
| continue | |
| # Fetch blob content | |
| blob = repo.get_git_blob(element.sha) | |
| content = base64.b64decode(blob.content).decode("utf-8", errors="ignore") | |
| # Redact secrets | |
| content = redact_env_secrets(content, element.path) | |
| files.append({ | |
| "filename": element.path, | |
| "content": content | |
| }) | |
| total_size += len(content) | |
| if len(files) >= 20 or total_size >= max_size: | |
| break | |
| return files | |
| except Exception as e: | |
| logger.error(f"Failed to fetch files from repo {repo_full_name}: {e}") | |
| return [] | |
| def redact_env_secrets(content: str, filename: str) -> str: | |
| # Redact common secrets patterns | |
| lines = content.splitlines() | |
| redacted_lines = [] | |
| # Check if this is an env file | |
| is_env = filename.endswith(".env") or filename.endswith(".env.production") or filename.endswith(".env.local") | |
| # Generic credential keys regex | |
| secret_key_re = re.compile( | |
| r"(password|secret|key|token|auth|pwd|credential|private_key|api_key|access_key)\s*[:=]\s*['\"]?([a-zA-Z0-9_\-\.\=\+]{8,})['\"]?", | |
| re.IGNORECASE | |
| ) | |
| for line in lines: | |
| if is_env: | |
| # For env files, redact everything after '=' | |
| if "=" in line and not line.strip().startswith("#"): | |
| key, val = line.split("=", 1) | |
| redacted_lines.append(f"{key}=[REDACTED]") | |
| else: | |
| redacted_lines.append(line) | |
| else: | |
| # Inline check for code files | |
| match = secret_key_re.search(line) | |
| if match: | |
| # Replace matching group containing key value | |
| val = match.group(2) | |
| redacted_lines.append(line.replace(val, "[REDACTED]")) | |
| else: | |
| redacted_lines.append(line) | |
| return "\n".join(redacted_lines) | |