Skip to content

Commit 37a7f7e

Browse files
authored
Merge branch 'staging' into dvirdukhan/parallel-second-pass
2 parents 3b743e7 + 2816d77 commit 37a7f7e

41 files changed

Lines changed: 2176 additions & 161 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/mcp-tests.yml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: MCP tests
2+
3+
on:
4+
push:
5+
branches: ["main", "staging", "mcp/**"]
6+
paths:
7+
- "api/mcp/**"
8+
- "tests/mcp/**"
9+
- "api/llm.py"
10+
- "api/graph.py"
11+
- "pyproject.toml"
12+
- "uv.lock"
13+
- ".github/workflows/mcp-tests.yml"
14+
pull_request:
15+
paths:
16+
- "api/mcp/**"
17+
- "tests/mcp/**"
18+
- "api/llm.py"
19+
- "api/graph.py"
20+
- "pyproject.toml"
21+
- "uv.lock"
22+
- ".github/workflows/mcp-tests.yml"
23+
workflow_dispatch:
24+
25+
permissions:
26+
contents: read
27+
28+
concurrency:
29+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
30+
cancel-in-progress: true
31+
32+
jobs:
33+
mcp-tests:
34+
runs-on: ubuntu-latest
35+
36+
services:
37+
falkordb:
38+
image: falkordb/falkordb:latest
39+
ports:
40+
- 6379:6379
41+
options: >-
42+
--health-cmd "redis-cli ping"
43+
--health-interval 5s
44+
--health-timeout 3s
45+
--health-retries 12
46+
47+
env:
48+
FALKORDB_HOST: localhost
49+
FALKORDB_PORT: "6379"
50+
51+
steps:
52+
- name: Checkout
53+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
54+
55+
- name: Setup Python
56+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
57+
with:
58+
python-version: "3.12"
59+
60+
- name: Install uv
61+
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
62+
with:
63+
version: "latest"
64+
enable-cache: true
65+
cache-dependency-glob: "uv.lock"
66+
67+
- name: Install backend dependencies
68+
run: uv sync --all-extras
69+
70+
- name: Verify FalkorDB reachable
71+
run: |
72+
sudo apt-get update -qq && sudo apt-get install -y redis-tools
73+
redis-cli -h localhost -p 6379 ping
74+
75+
- name: Run MCP test suite
76+
run: uv run pytest tests/mcp/ -v

.github/workflows/release-image.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
fi
2424
2525
- name: Login to DockerHub
26-
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
26+
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
2727
with:
2828
username: ${{ secrets.DOCKER_USERNAME }}
2929
password: ${{ secrets.DOCKER_PASSWORD }}

Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ COPY --from=node-base /usr/local/bin/node /usr/local/bin/node
2121
COPY --from=node-base /usr/local/lib/node_modules /usr/local/lib/node_modules
2222

2323
# Install netcat for wait loop in start.sh and system build tools
24-
RUN apt-get update && apt-get install -y --no-install-recommends \
24+
RUN apt-get update \
25+
&& apt-get install -y -f \
26+
&& apt-get install -y --no-install-recommends \
2527
netcat-openbsd \
2628
git \
2729
build-essential \

api/analyzers/analyzer.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ def resolve(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: P
5757
locations = lsp.request_definition(str(file_path), node.start_point.row, node.start_point.column)
5858
return [(files[Path(self.resolve_path(location['absolutePath'], path))], files[Path(self.resolve_path(location['absolutePath'], path))].tree.root_node.descendant_for_point_range(Point(location['range']['start']['line'], location['range']['start']['character']), Point(location['range']['end']['line'], location['range']['end']['character']))) for location in locations if location and Path(self.resolve_path(location['absolutePath'], path)) in files]
5959
except Exception:
60+
import logging
61+
logging.getLogger(__name__).warning(
62+
"resolve() failed for %s @%d:%d",
63+
file_path, node.start_point.row, node.start_point.column,
64+
exc_info=True,
65+
)
6066
return []
6167

6268
@abstractmethod

api/analyzers/source_analyzer.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from multilspy.multilspy_logger import MultilspyLogger
2020

2121
import logging
22+
import sys
2223
# Configure logging
2324
logging.basicConfig(level=logging.DEBUG, format='%(filename)s - %(asctime)s - %(levelname)s - %(message)s')
2425

@@ -164,7 +165,29 @@ def second_pass(self, graph: Graph, files: list[Path], path: Path) -> None:
164165
else:
165166
lsps[".java"] = NullLanguageServer()
166167
if any(path.rglob('*.py')):
167-
config = MultilspyConfig.from_dict({"code_language": "python", "environment_path": f"{path}/venv"})
168+
py_venv = path / "venv"
169+
py_dotvenv = path / ".venv"
170+
if py_venv.is_dir() and (py_venv / "bin" / "python").exists():
171+
env_path = str(py_venv)
172+
elif py_dotvenv.is_dir() and (py_dotvenv / "bin" / "python").exists():
173+
env_path = str(py_dotvenv)
174+
else:
175+
# Fall back to the host's Python environment so jedi has a
176+
# valid interpreter to introspect; otherwise every
177+
# request_definition() raises InvalidPythonEnvironment and
178+
# we'd silently produce a graph with zero CALLS edges.
179+
# sys.prefix is the active environment root and is more
180+
# reliable than deriving it from sys.executable (which breaks
181+
# when the interpreter is a wrapper/shim).
182+
env_path = sys.prefix
183+
logging.info(
184+
"No venv at %s; falling back to host env %s for jedi LSP",
185+
path, env_path,
186+
)
187+
config = MultilspyConfig.from_dict({
188+
"code_language": "python",
189+
"environment_path": env_path,
190+
})
168191
lsps[".py"] = SyncLanguageServer.create(config, logger, str(path))
169192
else:
170193
lsps[".py"] = NullLanguageServer()
@@ -189,6 +212,14 @@ def second_pass(self, graph: Graph, files: list[Path], path: Path) -> None:
189212
resolvable: list[Path] = []
190213
for file_path in files:
191214
if file_path not in self.files:
215+
# first_pass skipped this file (e.g. parse error, empty,
216+
# untracked, or ignored after entering the candidate list).
217+
# Skip in second_pass too instead of crashing the whole
218+
# index.
219+
logging.warning(
220+
"second_pass: %s not in files map (first_pass skipped it); skipping",
221+
file_path,
222+
)
192223
continue
193224
if isinstance(lsps.get(file_path.suffix), NullLanguageServer):
194225
continue
@@ -287,21 +318,26 @@ def analyze_local_folder(self, path: str, g: Graph, ignore: Optional[list[str]]
287318

288319
logging.info("Done analyzing path")
289320

290-
def analyze_local_repository(self, path: str, ignore: Optional[list[str]] = None) -> Graph:
321+
def analyze_local_repository(self, path: str, ignore: Optional[list[str]] = None, branch: Optional[str] = None) -> Graph:
291322
"""
292323
Analyze a local Git repository.
293324
294325
Args:
295326
path (str): Path to a local git repository
296327
ignore (List(str)): List of paths to skip
328+
branch (Optional[str]): Branch name. Auto-detected from the
329+
checkout when ``None``.
297330
"""
298331
if ignore is None:
299332
ignore = []
300333

301334
from pygit2.repository import Repository
335+
from ..project import detect_branch
302336

303337
proj_name = Path(path).name
304-
graph = Graph(proj_name)
338+
if branch is None:
339+
branch = detect_branch(Path(path))
340+
graph = Graph(proj_name, branch=branch)
305341
self.analyze_local_folder(path, graph, ignore)
306342

307343
# Save processed commit hash to the DB

api/auto_complete.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1+
from typing import Optional
2+
13
from .graph import Graph, AsyncGraphQuery
24

35

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

911

10-
async def async_prefix_search(repo: str, prefix: str) -> list:
12+
async def async_prefix_search(repo: str, prefix: str, branch: Optional[str] = None) -> list:
1113
"""Async version of prefix_search using AsyncGraphQuery."""
12-
g = AsyncGraphQuery(repo)
14+
g = AsyncGraphQuery(repo, branch=branch)
1315
try:
1416
return await g.prefix_search(prefix)
1517
finally:

0 commit comments

Comments
 (0)