Skip to content

Commit 3f2d9b9

Browse files
fix C++ parser heuristic, move repo switcher into chat header, add .env.local templates
1 parent 5b77e03 commit 3f2d9b9

11 files changed

Lines changed: 140 additions & 41 deletions

File tree

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# CodeGraph
2+
3+
CodeGraph lets you drop in a GitHub repository URL and explore the codebase visually. It parses every function in the repo, figures out what calls what, and draws that as an interactive graph. You can also ask plain English questions about the code and get answers back.
4+
5+
It was built because reading an unfamiliar codebase is slow. You either grep around hoping to find the right file, or you spend an hour tracing function calls manually. CodeGraph does that tracing for you and gives you something you can actually look at.
6+
7+
![CodeGraph dashboard showing the keystore C++ repo](demo/graph.png)
8+
9+
## What it does
10+
11+
When you submit a repo URL, the system clones it, walks through every source file, and extracts all the functions it finds. It then builds a graph where each node is a function and each edge is a call relationship. On top of that it generates vector embeddings for every function so you can search semantically.
12+
13+
The dashboard has three panels. The left panel is a file explorer. Click a file and you see all the functions in it. The center panel is the graph. You can view the full graph across the whole repo, or switch to file view to see just the functions in the file you selected along with anything they call into from other files. Click any node and a small panel appears showing which file it lives in, what line it starts on, and what it depends on. The right panel is a chat interface where you can ask things like "where is authentication handled" or "what happens after a user logs in" and get a real answer with the relevant functions highlighted.
14+
15+
## Languages supported
16+
17+
Python, JavaScript, TypeScript, Go, Rust, Java, C, C++, Kotlin.
18+
19+
## Running it
20+
21+
You need Docker and Docker Compose. That is the only requirement.
22+
23+
Copy `backend/.env.local` to `backend/.env` and fill in your Gemini API key. You can get one free at https://aistudio.google.com/app/apikey. Everything else in the file can stay as is.
24+
25+
Then run:
26+
27+
```
28+
docker compose up --build
29+
```
30+
31+
32+
The first time you submit a repo it will clone it, parse it, build the graph, and generate embeddings. Small repos take under a minute. Larger ones with hundreds of functions take a few minutes, mostly because of the embedding step which calls the Gemini API in parallel.
33+
34+
## Re-analyzing a repo
35+
36+
If the repo has been updated and you want to pull the latest changes, click the re-analyze button in the sidebar. It will go through the full pipeline again from scratch.
37+
38+
## Switching between repos
39+
40+
The dropdown in the top right of the chat panel shows all repos you have analyzed. Click one to switch to it. You can also paste a new URL directly into that dropdown without going back to the landing page.
41+
42+
## Stack
43+
44+
The backend is Django with Celery for async processing. PostgreSQL with the pgvector extension stores both the relational data and the vector embeddings. Redis is the task broker. The frontend is React with TypeScript. Everything runs in Docker.

backend/apps/parser/extractor.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,30 @@ def _extract_python(file_path):
111111
),
112112
],
113113
"cpp": [
114-
re.compile(r"^[ \t]*(?:[\w:*&<>\[\]]+\s+)+(?P<name>~?\w+)\s*\([^;]*\)\s*(?:const\s*)?\{"),
115-
re.compile(r"^[ \t]*(?:[\w:*&<>\[\]]+\s+)+(?P<name>~?\w+)\s*\([^;]*\)\s*(?:const\s*)?$"),
114+
# Matches: [qualifiers] return_type [ClassName::]funcName(params) [const] {
115+
# Handles: void Foo::bar(...) { / std::string baz(...) { / Foo::Foo(...) {
116+
re.compile(
117+
r"^[ \t]*"
118+
r"(?:(?:inline|static|virtual|explicit|constexpr|override|friend)\s+)*" # optional qualifiers
119+
r"(?:[\w:*&<>, \t]+?\s+)?" # optional return type (greedy-lazy)
120+
r"(?:[\w]+::)*" # optional ClassName:: prefix(es)
121+
r"(?P<name>~?[\w]+)\s*" # function name (with optional ~)
122+
r"\([^;{]*\)\s*" # params — no semicolons or braces inside
123+
r"(?:const\s*)?(?:noexcept\s*)?" # optional const/noexcept
124+
r"(?:override\s*)?(?:->\s*[\w:*& ]+\s*)?" # optional trailing return
125+
r"\{" # opening brace on same line
126+
),
127+
# Same but brace on next line (two-line match not possible with single regex,
128+
# so match the signature line alone and let _find_block_end handle it)
129+
re.compile(
130+
r"^[ \t]*"
131+
r"(?:(?:inline|static|virtual|explicit|constexpr|override|friend)\s+)*"
132+
r"(?:[\w:*&<>, \t]+?\s+)?"
133+
r"(?:[\w]+::)*"
134+
r"(?P<name>~?[\w]+)\s*"
135+
r"\([^;{]*\)\s*"
136+
r"(?:const\s*)?(?:noexcept\s*)?(?:override\s*)?$"
137+
),
116138
],
117139
"c": [
118140
re.compile(r"^(?:[\w*]+\s+)+(?P<name>\w+)\s*\([^;]*\)\s*\{"),

backend/env.local

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
DEBUG=True
2+
SECRET_KEY=your-secret-key-here
3+
4+
POSTGRES_DB=knowledge_base
5+
POSTGRES_USER=kbuser
6+
POSTGRES_PASSWORD=kbpass123
7+
POSTGRES_HOST=db
8+
POSTGRES_PORT=5432
9+
10+
REDIS_URL=redis://redis:6379/0
11+
12+
GOOGLE_API_KEY=your-gemini-api-key-here
13+
14+
REPOS_DIR=/repos
15+
16+
ALLOWED_HOSTS=*

demo/graph.png

328 KB
Loading

frontend/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ dist-ssr
2222
*.njsproj
2323
*.sln
2424
*.sw?
25+
26+
.env

frontend/src/App.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,11 @@ export default function App() {
2323
setView(r.status === 'ready' ? 'dashboard' : 'processing')
2424
}
2525

26-
return (
27-
<>
28-
{/* Persistent top-right switcher — visible on all views */}
29-
<div className="topbar">
30-
<RepoSwitcher current={repo} onSelect={handleSelect} onNew={handleSubmit} />
31-
</div>
32-
33-
{view === 'landing' && <Landing onSubmit={handleSubmit} />}
34-
{view === 'processing' && <Processing repo={repo!} onReady={handleReady} />}
35-
{view === 'dashboard' && <Dashboard repo={repo!} onReanalyze={() => setView('processing')} />}
36-
</>
26+
const switcher = (
27+
<RepoSwitcher current={repo} onSelect={handleSelect} onNew={handleSubmit} />
3728
)
29+
30+
if (view === 'landing') return <Landing onSubmit={handleSubmit} switcher={switcher} />
31+
if (view === 'processing') return <Processing repo={repo!} onReady={handleReady} switcher={switcher} />
32+
return <Dashboard repo={repo!} onReanalyze={() => setView('processing')} switcher={switcher} />
3833
}

frontend/src/components/ChatPanel.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,20 @@ const SUGGESTIONS = [
1111
interface Props {
1212
repoId: string
1313
onFocusFn: (fn: FileFn) => void
14+
switcher: React.ReactNode
1415
}
1516

16-
export function ChatPanel({ repoId, onFocusFn }: Props) {
17+
export function ChatPanel({ repoId, onFocusFn, switcher }: Props) {
1718
const [messages, setMessages] = useState<ChatMessage[]>([])
1819
const [input, setInput] = useState('')
1920
const [loading, setLoading] = useState(false)
2021
const bottomRef = useRef<HTMLDivElement>(null)
2122

23+
useEffect(() => {
24+
setMessages([])
25+
setInput('')
26+
}, [repoId])
27+
2228
useEffect(() => {
2329
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
2430
}, [messages])
@@ -40,7 +46,10 @@ export function ChatPanel({ repoId, onFocusFn }: Props) {
4046

4147
return (
4248
<div className="chat-panel">
43-
<div className="chat-header">Ask about the codebase</div>
49+
<div className="chat-header">
50+
<span className="chat-header-title">Ask about the codebase</span>
51+
{switcher}
52+
</div>
4453
<div className="chat-messages">
4554
{messages.length === 0 && (
4655
<div className="chat-empty">

frontend/src/components/Dashboard.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import { ChatPanel } from './ChatPanel'
88
interface Props {
99
repo: Repository
1010
onReanalyze: () => void
11+
switcher: React.ReactNode
1112
}
1213

13-
export function Dashboard({ repo, onReanalyze }: Props) {
14+
export function Dashboard({ repo, onReanalyze, switcher }: Props) {
1415
const [selectedFile, setSelectedFile] = useState<RepoFile | null>(null)
1516

1617
const handleReanalyze = async () => {
@@ -28,14 +29,8 @@ export function Dashboard({ repo, onReanalyze }: Props) {
2829
onSelectFn={() => {}}
2930
onReanalyze={handleReanalyze}
3031
/>
31-
<GraphPanel
32-
repoId={repo.id}
33-
selectedFile={selectedFile}
34-
/>
35-
<ChatPanel
36-
repoId={repo.id}
37-
onFocusFn={() => {}}
38-
/>
32+
<GraphPanel repoId={repo.id} selectedFile={selectedFile} />
33+
<ChatPanel repoId={repo.id} onFocusFn={() => {}} switcher={switcher} />
3934
</div>
4035
)
4136
}

frontend/src/components/Landing.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { api } from '../api'
44

55
interface Props {
66
onSubmit: (repo: Repository) => void
7+
switcher: React.ReactNode
78
}
89

9-
export function Landing({ onSubmit }: Props) {
10+
export function Landing({ onSubmit, switcher }: Props) {
1011
const [url, setUrl] = useState('')
1112
const [loading, setLoading] = useState(false)
1213
const [error, setError] = useState('')
@@ -27,6 +28,7 @@ export function Landing({ onSubmit }: Props) {
2728

2829
return (
2930
<div className="landing">
31+
<div className="landing-topbar">{switcher}</div>
3032
<h1>CodeGraph</h1>
3133
<p>Explore any GitHub repository as a function-level knowledge graph</p>
3234
<form className="url-form" onSubmit={handleSubmit}>

frontend/src/components/Processing.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ const STEPS = [
1313
interface Props {
1414
repo: Repository
1515
onReady: (repo: Repository) => void
16+
switcher: React.ReactNode
1617
}
1718

18-
export function Processing({ repo: initial, onReady }: Props) {
19+
export function Processing({ repo: initial, onReady, switcher }: Props) {
1920
const [repo, setRepo] = useState(initial)
2021
const timer = useRef<ReturnType<typeof setInterval> | null>(null)
2122

@@ -40,6 +41,7 @@ export function Processing({ repo: initial, onReady }: Props) {
4041

4142
return (
4243
<div className="processing">
44+
<div className="landing-topbar">{switcher}</div>
4345
<h2>Analyzing {repo.name}</h2>
4446
<div className="steps">
4547
{STEPS.map((step, i) => {

0 commit comments

Comments
 (0)