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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ backend/.env
backend/data/
__pycache__/
*.pyc
frontend/dist/
frontend/dist/
agents/
4 changes: 3 additions & 1 deletion backend/app/ai/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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]
]
}

Expand Down
43 changes: 25 additions & 18 deletions backend/app/analyzers/commit_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from datetime import datetime


def analyze_commits(commits: list):

Check failure on line 5 in backend/app/analyzers/commit_analyzer.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 18 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=kunal-yelgate_GithubAI2&issues=AaCgazz89xLTKYIi53sn&open=AaCgazz89xLTKYIi53sn&pullRequest=4

if not commits:
return {
Expand All @@ -10,34 +10,39 @@
"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),
Expand Down Expand Up @@ -65,5 +70,7 @@
commit_dates[-1]
if commit_dates
else None
)
),

"history": history
}
20 changes: 18 additions & 2 deletions backend/app/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,22 @@


@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(

Check failure on line 49 in backend/app/routes/auth.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Document this HTTPException with status code 400 in the "responses" parameter.

See more on https://sonarcloud.io/project/issues?id=kunal-yelgate_GithubAI2&issues=AaCgaz0Y9xLTKYIi53so&open=AaCgaz0Y9xLTKYIi53so&pullRequest=4
status_code=400,
detail=(error_description or error or "GitHub authentication failed")
)

if not code:
raise HTTPException(

Check failure on line 55 in backend/app/routes/auth.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Document this HTTPException with status code 400 in the "responses" parameter.

See more on https://sonarcloud.io/project/issues?id=kunal-yelgate_GithubAI2&issues=AaCgaz0Y9xLTKYIi53sp&open=AaCgaz0Y9xLTKYIi53sp&pullRequest=4
status_code=400,
detail="Missing GitHub OAuth code"
)

token_data = await exchange_code_for_token(code)

Expand All @@ -63,7 +78,8 @@
value=session,
httponly=True,
secure=False, # True in production HTTPS
samesite="lax"
samesite="lax",
path="/"
)

return response
42 changes: 29 additions & 13 deletions backend/app/services/github_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import httpx
import base64
from urllib.parse import quote

GITHUB_API_URL = "https://api.github.com"

Expand Down Expand Up @@ -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,
Expand Down
60 changes: 60 additions & 0 deletions backend/tests/test_auth_callback.py
Original file line number Diff line number Diff line change
@@ -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
35 changes: 27 additions & 8 deletions frontend/src/pages/Repository.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@
<strong>{analysis.architecture.backend || "Source"}</strong>
</div>
<p className="muted">
{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
</p>
{analysis.architecture.entry_points?.length > 0 && (
<div className="source-list">
Expand All @@ -113,12 +114,14 @@
)}
{analysis.architecture.module_graph?.length > 0 && (
<div className="source-list">
{analysis.architecture.module_graph.slice(0, 12).map((edge) => (
<div key={`${edge.from}-${edge.to}-${edge.import}`}>
<code>{edge.from}</code>
<span>→ {edge.to}</span>
</div>
))}
{analysis.architecture.module_graph
.slice(0, 12)
.map((edge) => (
<div key={`${edge.from}-${edge.to}-${edge.import}`}>
<code>{edge.from}</code>
<span>→ {edge.to}</span>
</div>
))}
</div>
)}
</article>
Expand Down Expand Up @@ -147,6 +150,22 @@
: "no setup detected"}
</p>
</article>
<article className="panel wide-panel">
<p className="eyebrow">Commit history</p>
<h2>{analysis.commits.total_analyzed} total commits</h2>
<div className="source-list">
{(analysis.commits.history || []).slice(0, 8).map((commit) => (
<div key={commit.sha || `${commit.author}-${commit.date}`}>
<code>{commit.message}</code>
<span>
{commit.author} ·{" "}
{new Date(commit.date).toLocaleDateString()}
{commit.date ? "" : ""}

Check warning on line 163 in frontend/src/pages/Repository.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This conditional operation returns the same value whether the condition is "true" or "false".

See more on https://sonarcloud.io/project/issues?id=kunal-yelgate_GithubAI2&issues=AaCgazw69xLTKYIi53sm&open=AaCgazw69xLTKYIi53sm&pullRequest=4
</span>
</div>
))}
</div>
</article>
<article className="panel wide-panel">
<p className="eyebrow">Important files</p>
<h2>
Expand Down
Loading