diff --git a/.gitignore b/.gitignore index 91f2e5c..d77bfa8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ backend/.env backend/data/ __pycache__/ *.pyc -frontend/dist/ \ No newline at end of file +frontend/dist/ +agents/ \ No newline at end of file diff --git a/backend/app/ai/context.py b/backend/app/ai/context.py index 511944a..aee2ff3 100644 --- a/backend/app/ai/context.py +++ b/backend/app/ai/context.py @@ -5,6 +5,8 @@ SYSTEM_PROMPT = """You are GitHub Atlas, a precise repository intelligence assistant. Answer only from the supplied GitHub context. Never invent files, metrics, technologies, or code. +Repository descriptions are weak evidence and are never enough by themselves to claim a stack or framework. To support a claim, use explicit evidence from the repository metadata, dependency lists, technology detection, README content, source file analysis, and retrieved code snippets. +If the repository context does not show direct evidence, say that the repository does not provide enough evidence to confirm it. When citing evidence, use repository-relative file paths in backticks. Explain uncertainty when context is incomplete. For statistics and profile questions, calculate from the supplied structured data. For code questions, distinguish static-analysis findings from direct source excerpts. @@ -51,7 +53,7 @@ def repository_context(analysis: dict, chunks: list[dict]): "chunk_index": chunk["chunk_index"], "content": chunk["content"][:2400] } - for chunk in chunks[:2] + for chunk in chunks[:4] ] } diff --git a/backend/app/analyzers/commit_analyzer.py b/backend/app/analyzers/commit_analyzer.py index b853841..acfed54 100644 --- a/backend/app/analyzers/commit_analyzer.py +++ b/backend/app/analyzers/commit_analyzer.py @@ -10,34 +10,39 @@ def analyze_commits(commits: list): "contributors": [], "commits_by_contributor": {}, "first_commit": None, - "latest_commit": None + "latest_commit": None, + "history": [] } contributor_counter = Counter() - commit_dates = [] + history = [] for commit in commits: + author = commit.get("author") or {} + username = author.get("login") or commit.get("commit", {}).get("author", {}).get("name") - author = commit.get("author") - - if author: - username = author.get("login") - - if username: - contributor_counter[username] += 1 + if username: + contributor_counter[username] += 1 commit_info = commit.get("commit", {}) - author_info = commit_info.get("author") - - if author_info: - - date = author_info.get("date") - - if date: - commit_dates.append(date) + author_info = commit_info.get("author") or {} + committer_info = commit_info.get("committer") or {} + date = author_info.get("date") or committer_info.get("date") + if date: + commit_dates.append(date) + + history.append({ + "sha": commit.get("sha"), + "message": (commit_info.get("message") or "").strip().split("\n")[0], + "author": username or commit_info.get("author", {}).get("name") or "Unknown", + "date": date, + "url": commit.get("html_url") or commit.get("url"), + "committer": (committer_info.get("name") or "Unknown") + }) commit_dates.sort() + history.sort(key=lambda item: item["date"] or "", reverse=True) return { "total_analyzed": len(commits), @@ -65,5 +70,7 @@ def analyze_commits(commits: list): commit_dates[-1] if commit_dates else None - ) + ), + + "history": history } \ No newline at end of file diff --git a/backend/app/routes/auth.py b/backend/app/routes/auth.py index 96b97f7..57ad4f9 100644 --- a/backend/app/routes/auth.py +++ b/backend/app/routes/auth.py @@ -40,7 +40,22 @@ async def logout(): @router.get("/github/callback") -async def github_callback(code: str): +async def github_callback( + code: str | None = None, + error: str | None = None, + error_description: str | None = None, +): + if error: + raise HTTPException( + status_code=400, + detail=(error_description or error or "GitHub authentication failed") + ) + + if not code: + raise HTTPException( + status_code=400, + detail="Missing GitHub OAuth code" + ) token_data = await exchange_code_for_token(code) @@ -63,7 +78,8 @@ async def github_callback(code: str): value=session, httponly=True, secure=False, # True in production HTTPS - samesite="lax" + samesite="lax", + path="/" ) return response \ No newline at end of file diff --git a/backend/app/services/github_api.py b/backend/app/services/github_api.py index ef740bc..0a2e9ed 100644 --- a/backend/app/services/github_api.py +++ b/backend/app/services/github_api.py @@ -1,6 +1,7 @@ import asyncio import httpx import base64 +from urllib.parse import quote GITHUB_API_URL = "https://api.github.com" @@ -170,20 +171,35 @@ async def get_repository_tree( "X-GitHub-Api-Version": "2022-11-28" } - async with httpx.AsyncClient() as client: - - response = await client.get( - f"{GITHUB_API_URL}/repos/" - f"{owner}/{repo}/git/trees/{branch}", - headers=headers, - params={ - "recursive": "1" - } - ) - - response.raise_for_status() + branches_to_try = [branch, "main", "master"] + seen = set() - return response.json() + async with httpx.AsyncClient() as client: + for candidate in branches_to_try: + if not candidate or candidate in seen: + continue + seen.add(candidate) + + encoded_candidate = quote(candidate, safe="") + + try: + response = await client.get( + f"{GITHUB_API_URL}/repos/" + f"{owner}/{repo}/git/trees/{encoded_candidate}", + headers=headers, + params={ + "recursive": "1" + } + ) + + if response.status_code == 404: + continue + response.raise_for_status() + return response.json() + except httpx.HTTPStatusError: + continue + + return {"tree": []} async def get_repository_file( access_token: str, diff --git a/backend/tests/test_auth_callback.py b/backend/tests/test_auth_callback.py new file mode 100644 index 0000000..4da8883 --- /dev/null +++ b/backend/tests/test_auth_callback.py @@ -0,0 +1,60 @@ +from fastapi.testclient import TestClient + +from app.ai.context import SYSTEM_PROMPT +from app.analyzers.commit_analyzer import analyze_commits +from app.main import app + + +client = TestClient(app) + + +def test_github_callback_requires_code(monkeypatch): + monkeypatch.setattr( + "app.routes.auth.exchange_code_for_token", + lambda code: {"access_token": "fake-token"}, + ) + monkeypatch.setattr( + "app.routes.auth.create_session", + lambda access_token: "signed-session", + ) + + response = client.get("/auth/github/callback") + + assert response.status_code == 400 + assert response.json()["detail"] == "Missing GitHub OAuth code" + + +def test_analyze_commits_includes_history_and_authors(): + commits = [ + { + "sha": "abc123", + "author": {"login": "alice"}, + "commit": { + "message": "Fix login flow\n\nDetails", + "author": {"name": "Alice", "date": "2024-01-02T00:00:00Z"} + }, + "html_url": "https://example.com/commit/abc123" + }, + { + "sha": "def456", + "author": {"login": "bob"}, + "commit": { + "message": "Add dashboard stats", + "author": {"name": "Bob", "date": "2024-01-01T00:00:00Z"} + }, + "html_url": "https://example.com/commit/def456" + } + ] + + analysis = analyze_commits(commits) + + assert analysis["total_analyzed"] == 2 + assert analysis["history"][0]["author"] == "alice" + assert analysis["history"][0]["message"] == "Fix login flow" + assert analysis["history"][1]["author"] == "bob" + + +def test_system_prompt_requires_repository_evidence(): + assert "Repository descriptions are weak evidence" in SYSTEM_PROMPT + assert "source file analysis" in SYSTEM_PROMPT + assert "retrieved code snippets" in SYSTEM_PROMPT diff --git a/frontend/src/pages/Repository.jsx b/frontend/src/pages/Repository.jsx index 453c926..bab682d 100644 --- a/frontend/src/pages/Repository.jsx +++ b/frontend/src/pages/Repository.jsx @@ -98,8 +98,9 @@ function Repository({ repository, onBack }) { {analysis.architecture.backend || "Source"}

- {analysis.architecture.analyzed_source_files || 0} source files ·{" "} - {analysis.architecture.module_relationships || 0} detected module relationships + {analysis.architecture.analyzed_source_files || 0} source files + · {analysis.architecture.module_relationships || 0} detected + module relationships

{analysis.architecture.entry_points?.length > 0 && (
@@ -113,12 +114,14 @@ function Repository({ repository, onBack }) { )} {analysis.architecture.module_graph?.length > 0 && (
- {analysis.architecture.module_graph.slice(0, 12).map((edge) => ( -
- {edge.from} - → {edge.to} -
- ))} + {analysis.architecture.module_graph + .slice(0, 12) + .map((edge) => ( +
+ {edge.from} + → {edge.to} +
+ ))}
)} @@ -147,6 +150,22 @@ function Repository({ repository, onBack }) { : "no setup detected"}

+
+

Commit history

+

{analysis.commits.total_analyzed} total commits

+
+ {(analysis.commits.history || []).slice(0, 8).map((commit) => ( +
+ {commit.message} + + {commit.author} ·{" "} + {new Date(commit.date).toLocaleDateString()} + {commit.date ? "" : ""} + +
+ ))} +
+

Important files