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
9 changes: 7 additions & 2 deletions api/analyzers/source_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,21 +208,26 @@ def analyze_local_folder(self, path: str, g: Graph, ignore: Optional[list[str]]

logging.info("Done analyzing path")

def analyze_local_repository(self, path: str, ignore: Optional[list[str]] = None) -> Graph:
def analyze_local_repository(self, path: str, ignore: Optional[list[str]] = None, branch: Optional[str] = None) -> Graph:
"""
Analyze a local Git repository.

Args:
path (str): Path to a local git repository
ignore (List(str)): List of paths to skip
branch (Optional[str]): Branch name. Auto-detected from the
checkout when ``None``.
"""
if ignore is None:
ignore = []

from pygit2.repository import Repository
from ..project import detect_branch

proj_name = Path(path).name
graph = Graph(proj_name)
if branch is None:
branch = detect_branch(Path(path))
graph = Graph(proj_name, branch=branch)
self.analyze_local_folder(path, graph, ignore)

# Save processed commit hash to the DB
Expand Down
10 changes: 6 additions & 4 deletions api/auto_complete.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from typing import Optional

from .graph import Graph, AsyncGraphQuery


def prefix_search(repo: str, prefix: str) -> str:
def prefix_search(repo: str, prefix: str, branch: Optional[str] = None) -> str:
""" Returns a list of all entities in the repository that start with the given prefix. """
g = Graph(repo)
g = Graph(repo, branch=branch)
return g.prefix_search(prefix)


async def async_prefix_search(repo: str, prefix: str) -> list:
async def async_prefix_search(repo: str, prefix: str, branch: Optional[str] = None) -> list:
"""Async version of prefix_search using AsyncGraphQuery."""
g = AsyncGraphQuery(repo)
g = AsyncGraphQuery(repo, branch=branch)
try:
return await g.prefix_search(prefix)
finally:
Expand Down
86 changes: 70 additions & 16 deletions api/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ def index(
repo: Optional[str] = typer.Option(
None, "--repo", help="Graph name (defaults to folder name)"
),
branch: Optional[str] = typer.Option(
None, "--branch", help="Branch to associate with this index (auto-detected from git checkout when omitted; '_default' for non-git paths)"
),
) -> None:
"""Index a local folder into the knowledge graph."""
from .project import Project
Expand Down Expand Up @@ -204,14 +207,14 @@ def index(

_stderr(f"Indexing {folder} as '{name}'…")
try:
project = Project(name, folder, url)
project = Project(name, folder, url, branch=branch)
graph = project.analyze_sources(ignore=list(ignore) if ignore else [])
stats = graph.stats()
except Exception as e:
_json_error(str(e))

_stderr(f"Done — {stats['node_count']} nodes, {stats['edge_count']} edges")
_json_out({"status": "ok", "repo": name, **stats})
_stderr(f"Done — {stats['node_count']} nodes, {stats['edge_count']} edges (branch={project.branch})")
_json_out({"status": "ok", "repo": name, "branch": project.branch, **stats})


# ── index-repo ─────────────────────────────────────────────────────────
Expand All @@ -223,6 +226,9 @@ def index_repo(
ignore: Optional[List[str]] = typer.Option(
None, "--ignore", help="Directories to ignore (repeatable)"
),
branch: Optional[str] = typer.Option(
None, "--branch", help="Branch to associate with this index (auto-detected from the cloned checkout when omitted)"
),
) -> None:
"""Clone a git repository and index it into the knowledge graph."""
from .project import Project
Expand All @@ -233,22 +239,22 @@ def index_repo(
import io
import contextlib
with contextlib.redirect_stdout(io.StringIO()):
project = Project.from_git_repository(url)
project = Project.from_git_repository(url, branch=branch)
graph = project.analyze_sources(ignore=list(ignore) if ignore else [])
stats = graph.stats()
except Exception as e:
_json_error(str(e))

_stderr(f"Done — {stats['node_count']} nodes, {stats['edge_count']} edges")
_json_out({"status": "ok", "repo": project.name, **stats})
_stderr(f"Done — {stats['node_count']} nodes, {stats['edge_count']} edges (branch={project.branch})")
_json_out({"status": "ok", "repo": project.name, "branch": project.branch, **stats})


# ── list ───────────────────────────────────────────────────────────────


@app.command("list")
def list_repos() -> None:
"""List all indexed repositories."""
"""List all indexed (project, branch) pairs."""
from .graph import get_repos

try:
Expand All @@ -259,6 +265,30 @@ def list_repos() -> None:
_json_out({"repos": repos})


# ── migrate ────────────────────────────────────────────────────────────


@app.command("migrate")
def migrate(
dry_run: bool = typer.Option(False, "--dry-run", help="Print actions without performing them"),
) -> None:
"""Promote legacy (pre-T17) graphs and Redis keys into the per-branch namespace.

Renames each legacy ``<project>`` graph to ``code:<project>:_default``,
each ``{project}_info`` Redis key to ``{project}:_default_info``, and
each ``{project}_git`` graph to ``{project}:_default_git``. Idempotent.
"""

from .migrations.per_branch import run_migration

try:
result = run_migration(dry_run=dry_run)
except Exception as e:
_json_error(str(e))

_json_out(result)


# ── search ─────────────────────────────────────────────────────────────


Expand All @@ -268,18 +298,24 @@ def search(
repo: Optional[str] = typer.Option(
None, "--repo", help="Repository name (defaults to CWD name)"
),
branch: Optional[str] = typer.Option(
None, "--branch", help="Branch (auto-detected from CWD; '_default' for non-git paths)"
),
) -> None:
"""Search for entities by prefix (full-text search)."""
from .graph import Graph
from .project import detect_branch

name = _default_repo(repo)
if branch is None:
branch = detect_branch(Path.cwd())
try:
g = Graph(name)
g = Graph(name, branch=branch)
results = g.prefix_search(query)
except Exception as e:
_json_error(str(e))

_json_out({"repo": name, "results": results})
_json_out({"repo": name, "branch": branch, "results": results})


# ── neighbors ──────────────────────────────────────────────────────────
Expand All @@ -297,18 +333,24 @@ def neighbors(
label: Optional[str] = typer.Option(
None, "--label", help="Filter by destination label (e.g. Function, Class)"
),
branch: Optional[str] = typer.Option(
None, "--branch", help="Branch (auto-detected from CWD; '_default' for non-git paths)"
),
) -> None:
"""Get neighboring entities of the given node(s)."""
from .graph import Graph
from .project import detect_branch

name = _default_repo(repo)
if branch is None:
branch = detect_branch(Path.cwd())
try:
g = Graph(name)
g = Graph(name, branch=branch)
result = g.get_neighbors(node_ids, rel=rel, lbl=label)
except Exception as e:
_json_error(str(e))

_json_out({"repo": name, **result})
_json_out({"repo": name, "branch": branch, **result})


# ── paths ──────────────────────────────────────────────────────────────
Expand All @@ -321,18 +363,24 @@ def paths(
repo: Optional[str] = typer.Option(
None, "--repo", help="Repository name (defaults to CWD name)"
),
branch: Optional[str] = typer.Option(
None, "--branch", help="Branch (auto-detected from CWD; '_default' for non-git paths)"
),
) -> None:
"""Find call-chain paths between two nodes."""
from .graph import Graph
from .project import detect_branch

name = _default_repo(repo)
if branch is None:
branch = detect_branch(Path.cwd())
try:
g = Graph(name)
g = Graph(name, branch=branch)
result = g.find_paths(src, dest)
except Exception as e:
_json_error(str(e))

_json_out({"repo": name, "paths": result})
_json_out({"repo": name, "branch": branch, "paths": result})


# ── info ───────────────────────────────────────────────────────────────
Expand All @@ -343,20 +391,26 @@ def info(
repo: Optional[str] = typer.Option(
None, "--repo", help="Repository name (defaults to CWD name)"
),
branch: Optional[str] = typer.Option(
None, "--branch", help="Branch (auto-detected from CWD; '_default' for non-git paths)"
),
) -> None:
"""Show repository statistics and metadata."""
from .graph import Graph
from .info import get_repo_info
from .project import detect_branch

name = _default_repo(repo)
if branch is None:
branch = detect_branch(Path.cwd())
try:
g = Graph(name)
g = Graph(name, branch=branch)
stats = g.stats()
metadata = get_repo_info(name) or {}
metadata = get_repo_info(name, branch) or {}
except Exception as e:
_json_error(str(e))

_json_out({"repo": name, **stats, "metadata": metadata})
_json_out({"repo": name, "branch": branch, **stats, "metadata": metadata})


if __name__ == "__main__":
Expand Down
6 changes: 4 additions & 2 deletions api/code_coverage/lcov/lcov.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import os
import sys
from typing import Optional

from ...graph import Graph

def lcovparse(content):
Expand Down Expand Up @@ -124,7 +126,7 @@ def _line(l, report):
else:
sys.stdout.write("Unknown method name %s" % method)

def process_lcov(repo: str, lcov_file: str) -> None:
def process_lcov(repo: str, lcov_file: str, branch: Optional[str] = None) -> None:
# create report from coverage lcov file
with open(lcov_file, "r") as file:
content = file.read() # Reads the entire file as a single string
Expand All @@ -134,7 +136,7 @@ def process_lcov(repo: str, lcov_file: str) -> None:
# SF:/__w/FalkorDB/FalkorDB/src/algorithms/detect_cycle.c
prefix = "/__w/FalkorDB/FalkorDB/" # prefix to remove

g = Graph(repo)
g = Graph(repo, branch=branch)

#---------------------------------------------------------------------------
# Process report
Expand Down
Loading
Loading