Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 143 additions & 3 deletions src/kit/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

from __future__ import annotations

import logging
import subprocess
from typing import Dict, List
from urllib.parse import urlparse

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
Expand All @@ -11,9 +14,31 @@

from .registry import registry

# Set up logging
logger = logging.getLogger(__name__)

app = FastAPI(title="kit API", version="0.1.0")


def sanitize_url(url: str) -> str:
"""Remove credentials from URL for safe display in error messages."""
try:
parsed = urlparse(url)
if parsed.username or parsed.password:
# Reconstruct URL without credentials
sanitized = f"{parsed.scheme}://{parsed.hostname}"
if parsed.port:
sanitized += f":{parsed.port}"
sanitized += parsed.path
if parsed.query:
sanitized += f"?{parsed.query}"
return sanitized
return url
except Exception:
# If parsing fails, return a generic message
return "[sanitized repository URL]"


class RepoIn(BaseModel):
path_or_url: str
github_token: str | None = None
Expand All @@ -27,9 +52,124 @@ class FilePathsIn(BaseModel):
@app.post("/repository", status_code=201)
def open_repo(body: RepoIn):
"""Register a repository path/URL and return its deterministic ID."""
repo_id = registry.add(body.path_or_url, body.ref)
_ = registry.get_repo(repo_id)
return {"id": repo_id}
try:
repo_id = registry.add(body.path_or_url, body.ref)
_ = registry.get_repo(repo_id)
logger.info(f"Repository opened successfully: {sanitize_url(body.path_or_url)}")
return {"id": repo_id}
except subprocess.CalledProcessError as e:
# Git command failures (clone, checkout, etc.)
error_msg = str(e)

# Log full details for debugging (internal only)
logger.warning(
f"Git command failed: {error_msg}",
extra={
"repo_url": body.path_or_url,
"ref": body.ref,
"return_code": e.returncode,
"event_type": "git_command_failure",
},
)

# For git clone failures (exit code 128 is common for "not found")
if e.returncode == 128 and "clone" in error_msg:
# Extract URL from error message if possible, otherwise use generic message
if body.path_or_url.startswith(("http://", "https://")):
logger.info(
f"Repository not found: {sanitize_url(body.path_or_url)}",
extra={"event_type": "repository_not_found", "repo_url_sanitized": sanitize_url(body.path_or_url)},
)
raise HTTPException(status_code=404, detail=f"Repository not found: {sanitize_url(body.path_or_url)}")
else:
logger.info(
f"Local repository not found: {body.path_or_url}",
extra={"event_type": "local_repository_not_found"},
)
raise HTTPException(status_code=404, detail="Repository not found or inaccessible")
else:
raise HTTPException(status_code=500, detail=f"Git operation failed: {error_msg}")
except FileNotFoundError as e:
logger.warning(f"File not found: {body.path_or_url}", extra={"event_type": "file_not_found", "error": str(e)})
raise HTTPException(status_code=404, detail=f"Repository path not found: {e!s}")
except ValueError as e:
# Git ref errors, invalid paths, etc.
logger.warning(
f"Invalid repository configuration: {body.path_or_url}",
extra={"event_type": "invalid_configuration", "ref": body.ref, "error": str(e)},
)
raise HTTPException(status_code=400, detail=f"Invalid repository configuration: {e!s}")
except Exception as e:
# All other failures - keep it simple
error_msg = str(e)
error_lower = error_msg.lower()

# Log with appropriate level based on error type
if "permission denied" in error_lower or "access denied" in error_lower:
logger.warning(
"Access denied for repository",
extra={
"event_type": "access_denied",
"repo_url_sanitized": sanitize_url(body.path_or_url)
if body.path_or_url.startswith(("http://", "https://"))
else body.path_or_url,
"error": error_msg,
},
)
raise HTTPException(
status_code=403,
detail=f"Access denied: {sanitize_url(body.path_or_url) if body.path_or_url.startswith(('http://', 'https://')) else 'repository'}",
)
elif "authentication failed" in error_lower or "invalid credentials" in error_lower:
logger.warning(
"Authentication failed for repository",
extra={
"event_type": "authentication_failed",
"repo_url_sanitized": sanitize_url(body.path_or_url)
if body.path_or_url.startswith(("http://", "https://"))
else body.path_or_url,
"error": error_msg,
},
)
raise HTTPException(status_code=401, detail="Authentication failed")
elif "timeout" in error_lower or "network" in error_lower or "connection" in error_lower:
logger.warning(
"Network error for repository",
extra={
"event_type": "network_error",
"repo_url_sanitized": sanitize_url(body.path_or_url)
if body.path_or_url.startswith(("http://", "https://"))
else body.path_or_url,
"error": error_msg,
},
)
raise HTTPException(status_code=503, detail="Network error accessing repository")
elif "repository not found" in error_lower or "not found" in error_lower:
logger.info(
"Repository not found",
extra={
"event_type": "repository_not_found",
"repo_url_sanitized": sanitize_url(body.path_or_url)
if body.path_or_url.startswith(("http://", "https://"))
else body.path_or_url,
},
)
raise HTTPException(
status_code=404,
detail=f"Repository not found: {sanitize_url(body.path_or_url) if body.path_or_url.startswith(('http://', 'https://')) else body.path_or_url}",
)
else:
logger.error(
"Unexpected error initializing repository",
extra={
"event_type": "unexpected_error",
"repo_url_sanitized": sanitize_url(body.path_or_url)
if body.path_or_url.startswith(("http://", "https://"))
else body.path_or_url,
"error": error_msg,
},
)
raise HTTPException(status_code=500, detail="Failed to initialize repository")


@app.get("/repository/{repo_id}/file-tree")
Expand Down
Loading