Skip to content
Open
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
25 changes: 19 additions & 6 deletions api/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,18 +426,22 @@ def delete_files(self, files: list[Path]) -> tuple[str, dict, list[int]]:

return None

def get_file(self, path: str, name: str, ext: str) -> Optional[Node]:
def get_file(self, path: str, name: str, ext: str) -> Optional[File]:
"""
Retrieves a File node from the graph database based on its path, name,
and extension.
Retrieves a File entity from the graph database based on its path, name, and extension.

Args:
path (str): The file path.
name (str): The file name.
ext (str): The file extension.

Returns:
Optional[Node]: The File node if found, otherwise None.
Optional[File]: The File object if found, otherwise None.

This method constructs and executes a query to find a file node in the graph
database with the specified path, name, and extension. If the file node is found,
it creates and returns a File object with its properties and ID. If no such node
is found, it returns None.

Example:
file = self.get_file('/path/to/file', 'filename', '.py')
Expand All @@ -448,10 +452,19 @@ def get_file(self, path: str, name: str, ext: str) -> Optional[Node]:
params = {'path': path, 'name': name, 'ext': ext}

res = self._query(q, params)
if len(res.result_set) == 0:
if(len(res.result_set) == 0):
return None

return res.result_set[0][0]
node = res.result_set[0][0]

ext = node.properties['ext']
path = node.properties['path']
name = node.properties['name']
file = File(path, name, ext)

file.id = node.id

return file

# set file code coverage
# if file coverage is 100% set every defined function coverage to 100% aswell
Expand Down
6 changes: 6 additions & 0 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export default function App() {
const [carouselApi, setCarouselApi] = useState<CarouselApi>()
const [zoomedNodes, setZoomedNodes] = useState<Node[]>([])
const [hasHiddenElements, setHasHiddenElements] = useState(false);
const [isFetchingGraph, setIsFetchingGraph] = useState(false)

useEffect(() => {
if (path?.start?.id && path?.end?.id) {
Expand Down Expand Up @@ -153,6 +154,7 @@ export default function App() {
}

async function onFetchGraph(graphName: string) {
setIsFetchingGraph(true)
try {
const result = await fetch(`/api/graph_entities?repo=${prepareArg(graphName)}`, {
method: 'GET',
Expand Down Expand Up @@ -186,6 +188,8 @@ export default function App() {
title: "Uh oh! Something went wrong.",
description: "Failed to load repository graph. Please try again.",
})
} finally {
setIsFetchingGraph(false)
}
}

Expand Down Expand Up @@ -527,6 +531,7 @@ export default function App() {
options={options}
setOptions={setOptions}
onFetchGraph={onFetchGraph}
isFetchingGraph={isFetchingGraph}
onFetchNode={onFetchNode}
setPath={setPath}
isShowPath={!!path}
Expand Down Expand Up @@ -654,6 +659,7 @@ export default function App() {
options={options}
setOptions={setOptions}
onFetchGraph={onFetchGraph}
isFetchingGraph={isFetchingGraph}
onFetchNode={onFetchNode}
setPath={setPath}
isShowPath={!!path}
Expand Down
23 changes: 16 additions & 7 deletions app/src/components/code-graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
import { Graph, GraphData, Node, Link } from "./model";
import { Toolbar } from "./toolbar";
import { Labels } from "./labels";
import { Download, GitFork, Search, X } from "lucide-react";
import { GitFork, Loader2, Search, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import ElementMenu from "./elementMenu";
import Combobox from "./combobox";
Expand All @@ -28,6 +28,7 @@ interface Props {
data: GraphData,
setData: Dispatch<SetStateAction<GraphData>>,
onFetchGraph: (graphName: string) => Promise<void>,
isFetchingGraph: boolean,
onFetchNode: (nodeIds: number[]) => Promise<GraphData>,
options: string[]
setOptions: Dispatch<SetStateAction<string[]>>
Expand Down Expand Up @@ -58,9 +59,8 @@ export function CodeGraph({
data,
setData,
onFetchGraph,
isFetchingGraph,
onFetchNode,
options,
setOptions,
isShowPath,
setPath,
canvasRef,
Expand Down Expand Up @@ -93,6 +93,7 @@ export function CodeGraph({
const [commitIndex, setCommitIndex] = useState<number>(0);
const [currentCommit, setCurrentCommit] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const [options, setOptions] = useState<string[]>([]);

useEffect(() => {
setData({ ...graph.Elements })
Expand Down Expand Up @@ -516,10 +517,18 @@ export function CodeGraph({
</div>
</div>
</div>
: <div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<GitFork className="md:w-24 md:h-24 w-16 h-16" />
<h1 className="md:text-4xl text-2xl text-center">Select a repo to show its graph here</h1>
</div>
: (
isFetchingGraph ?
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<Loader2 className="md:w-24 md:h-24 w-16 h-16 animate-spin" />
<h1 className="md:text-4xl text-2xl text-center">Fetching graph...</h1>
</div>
:
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<GitFork className="md:w-24 md:h-24 w-16 h-16" />
<h1 className="md:text-4xl text-2xl text-center">Select a repo to show its graph here</h1>
</div>
)
}
</main>
{/* {
Expand Down
73 changes: 47 additions & 26 deletions app/src/components/combobox.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { toast } from "@/components/ui/use-toast";
import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";

const AUTH_HEADERS: HeadersInit = import.meta.env.VITE_SECRET_TOKEN
Expand All @@ -17,58 +18,78 @@ interface Props {
export default function Combobox({ options, setOptions, selectedValue, onSelectedValue }: Props) {

const [open, setOpen] = useState(false)
const [lastOpened, setLastOpened] = useState<number>();
const [lastFetch, setLastFetch] = useState<number>();
const [isFetchingOptions, setIsFetchingOptions] = useState(false)

const fetchOptions = async () => {
const result = await fetch(`/api/list_repos`, {
method: 'GET',
headers: {
...AUTH_HEADERS,
},
})
setIsFetchingOptions(true)

if (!result.ok) {
toast({
variant: "destructive",
title: "Uh oh! Something went wrong.",
description: await result.text(),
try {
const result = await fetch(`/api/list_repos`, {
method: 'GET',
headers: {
...AUTH_HEADERS,
},
})
return
}

const json = await result.json()
setOptions(json.repositories)
if (!result.ok) {
toast({
variant: "destructive",
title: "Uh oh! Something went wrong.",
description: await result.text(),
})
return
}

const json = await result.json()
setOptions(json.repositories)
} finally {
setIsFetchingOptions(false)
}
}

useEffect(() => {
fetchOptions()
}, [])

//fetch options when the combobox is opened
useEffect(() => {
if (!open) return

const now = Date.now();

if (lastOpened && now - lastOpened < 30000) return;

setLastOpened(now);

//check if last fetch was less than 30 seconds ago
if (lastFetch && now - lastFetch < 30000) return;

setLastFetch(now);

fetchOptions()
}, [open])

return (
<Select open={open} onOpenChange={setOpen} value={selectedValue} onValueChange={onSelectedValue}>
<Select open={open} onOpenChange={setOpen} disabled={options.length === 0 && !isFetchingOptions} value={isFetchingOptions ? "Fetching options..." : options.length !== 0 ? selectedValue : "No options found"} onValueChange={onSelectedValue}>
<SelectTrigger className="z-10 md:z-0 rounded-md border border-border focus:ring-1 focus:ring-primary">
<SelectValue placeholder="Select a repo" />
</SelectTrigger>
<SelectContent>
{
options.length !== 0 &&
options.map((option) => (
<SelectItem key={option} value={option}>
{option}
isFetchingOptions ?
<SelectItem value="Fetching options...">
<div className="flex flex-row items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
<p>Fetching options...</p>
</div>
</SelectItem>
))
: options.length !== 0 ?
options.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))
:
<SelectItem value="No options found">
<p>No options found</p>
</SelectItem>
}
</SelectContent>
</Select>
Expand Down
48 changes: 20 additions & 28 deletions tests/test_c_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
import unittest
from pathlib import Path

from api import SourceAnalyzer, Graph

from api import SourceAnalyzer, File, Struct, Function, Graph

class Test_C_Analyzer(unittest.TestCase):
def test_analyzer(self):
Expand All @@ -25,44 +24,37 @@ def test_analyzer(self):
analyzer.analyze_local_folder(path, g)

f = g.get_file('', 'src.c', '.c')
self.assertIsNotNone(f)
self.assertEqual(f.properties['name'], 'src.c')
self.assertEqual(f.properties['ext'], '.c')
self.assertEqual(File('', 'src.c', '.c'), f)

s = g.get_struct_by_name('exp')
self.assertIsNotNone(s)
self.assertEqual(s.properties['name'], 'exp')
self.assertEqual(s.properties['path'], 'src.c')
self.assertEqual(s.properties['src_start'], 9)
self.assertEqual(s.properties['src_end'], 13)
self.assertEqual(s.properties['fields'], [['i', 'int'], ['f', 'float'], ['data', 'char[]']])
expected_s = Struct('src.c', 'exp', '', 9, 13)
expected_s.add_field('i', 'int')
expected_s.add_field('f', 'float')
expected_s.add_field('data', 'char[]')
self.assertEqual(expected_s, s)

add = g.get_function_by_name('add')
self.assertIsNotNone(add)
self.assertEqual(add.properties['name'], 'add')
self.assertEqual(add.properties['path'], 'src.c')
self.assertEqual(add.properties['ret_type'], 'int')
self.assertEqual(add.properties['src_start'], 0)
self.assertEqual(add.properties['src_end'], 7)
self.assertEqual(add.properties['args'], [['a', 'int'], ['b', 'int']])
self.assertIn('a + b', add.properties['src'])

expected_add = Function('src.c', 'add', '', 'int', '', 0, 7)
expected_add.add_argument('a', 'int')
expected_add.add_argument('b', 'int')
self.assertEqual(expected_add, add)
self.assertIn('a + b', add.src)

main = g.get_function_by_name('main')
self.assertIsNotNone(main)
self.assertEqual(main.properties['name'], 'main')
self.assertEqual(main.properties['path'], 'src.c')
self.assertEqual(main.properties['ret_type'], 'int')
self.assertEqual(main.properties['src_start'], 15)
self.assertEqual(main.properties['src_end'], 18)
self.assertEqual(main.properties['args'], [['argv', 'const char**'], ['argc', 'int']])
self.assertIn('x = add', main.properties['src'])

expected_main = Function('src.c', 'main', '', 'int', '', 15, 18)
expected_main.add_argument('argv', 'const char**')
expected_main.add_argument('argc', 'int')
self.assertEqual(expected_main, main)
self.assertIn('x = add', main.src)

callees = g.function_calls(main.id)
self.assertEqual(len(callees), 1)
self.assertEqual(callees[0], add)

callers = g.function_called_by(add.id)
callers = [caller.properties['name'] for caller in callers]
callers = [caller.name for caller in callers]

self.assertEqual(len(callers), 2)
self.assertIn('add', callers)
Expand Down
21 changes: 11 additions & 10 deletions tests/test_git_history.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import os
import unittest
import pygit2
from git import Repo
from api import (
Graph,
Project,
switch_commit
)
Expand Down Expand Up @@ -29,8 +30,8 @@ def setUpClass(cls):
repo_dir = os.path.join(current_dir, 'git_repo')

# Checkout HEAD commit
repo = pygit2.Repository(repo_dir)
repo.checkout_head()
repo = Repo(repo_dir)
repo.git.checkout("HEAD")

proj = Project.from_local_repository(repo_dir)
graph = proj.analyze_sources()
Expand All @@ -44,13 +45,13 @@ def assert_file_exists(self, path: str, name: str, ext: str) -> None:
f = graph.get_file(path, name, ext)

self.assertIsNotNone(f)
self.assertEqual(f.properties['ext'], ext)
self.assertEqual(f.properties['path'], path)
self.assertEqual(f.properties['name'], name)
self.assertEqual(f.ext, ext)
self.assertEqual(f.path, path)
self.assertEqual(f.name, name)

def test_git_graph_structure(self):
# validate git graph structure
c = repo.revparse_single("HEAD")
c = repo.commit("HEAD")

while True:
commits = git_graph.get_commits([c.short_id])
Expand All @@ -61,13 +62,13 @@ def test_git_graph_structure(self):
self.assertEqual(c.short_id, actual['hash'])
self.assertEqual(c.message, actual['message'])
self.assertEqual(c.author.name, actual['author'])
self.assertEqual(c.commit_time, actual['date'])
self.assertEqual(c.committed_date, actual['date'])

# Advance to previous commit
if len(c.parent_ids) == 0:
if len(c.parents) == 0:
break

c = repo.get(c.parent_ids[0])
c = c.parents[0]

def test_git_transitions(self):
# our test git repo:
Expand Down
Loading
Loading