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