Skip to content

feat(graph): per-branch graph identity (T17 #651)#675

Merged
DvirDukhan merged 5 commits into
stagingfrom
dvirdukhan/mcp-t17-per-branch-graphs
May 28, 2026
Merged

feat(graph): per-branch graph identity (T17 #651)#675
DvirDukhan merged 5 commits into
stagingfrom
dvirdukhan/mcp-t17-per-branch-graphs

Conversation

@DvirDukhan
Copy link
Copy Markdown
Contributor

@DvirDukhan DvirDukhan commented May 27, 2026

Closes #651.

Stacked on #666 (mcp/t1-scaffold).

What

Each FalkorDB graph is now identified by (project, branch) rather
than just project. Concurrent agents indexing the same repo on
different branches no longer overwrite each other.

New graph name format: code:{project}:{branch} (default branch:
_default). The companion Redis info hash becomes
{project}:{branch}_info and the git-history graph becomes
{project}:{branch}_git.

Highlights

  • Graph / AsyncGraphQuery constructors accept branch=None.
  • All /api/* endpoints accept an optional branch field; responses
    include branch so the frontend can disambiguate.
  • CLI gains --branch on every read/index command and auto-detects
    from git rev-parse --abbrev-ref HEAD when omitted.
  • New cgraph migrate command (idempotent, --dry-run supported)
    promotes legacy <project> graphs to code:<project>:_default.
  • 24 new unit tests in tests/test_per_branch_graphs.py. Existing
    test_async_graph.py / test_cli.py / test_list_repos.py
    updated for the new dict return shape of get_repos.

Backward compatibility

  • Graph("foo") still works → composes to code:foo:_default.
  • Info reads fall back to the legacy {foo}_info key when the
    new key is absent, so un-migrated repos remain queryable until
    the operator runs cgraph migrate.
  • parse_graph_name returns None for legacy bare names so
    callers can detect and special-case them.

Migration story

One-shot rename of legacy artifacts via cgraph migrate:

  • graph <project>code:<project>:_default
  • Redis hash {<project>}_info{<project>}:_default_info
  • graph {<project>}_git{<project>}:_default_git

Safe to re-run. --dry-run previews actions without writing.

Not in scope

  • The MCP tools themselves (T4+) — those land in follow-up PRs.
  • Per-branch indexing of git history beyond what process_git_history
    already does (it now writes to the per-branch git graph but does
    not yet track multiple branches in a single run).

Test status

  • tests/test_per_branch_graphs.py: 24 ✅
  • tests/test_async_graph.py: 6 ✅
  • tests/test_cli.py: 10 ✅

Pre-existing failures unrelated to T17 (missing tests/git_repo
fixture, hard-coded port 6379 in test_graph_ops.py, analyzer
tests requiring specific source builds) remain unchanged from
mcp/t1-scaffold.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Summary by CodeRabbit

  • New Features

    • Branch-aware workflows across CLI, API, analysis, LLM, coverage, and git utilities; optional branch flag with auto-detection; branch included in listings and responses
    • New migrate command with dry-run to promote legacy data into per-branch namespaces
  • Refactor

    • Graph, storage, and Redis naming normalized for per-branch keys with legacy fallback
    • Repo metadata and commit tracking now branch-qualified
  • Tests

    • Added/updated tests covering per-branch behavior and migrations
  • Chores

    • Added dev dependency group to project config

Review Change Stack

Refactor FalkorDB graph naming so each (project, branch) pair gets
its own graph: 'code:{project}:{branch}'. This lets concurrent agents
working on different branches of the same repo index in parallel
without overwriting each other.

Changes:
- api/graph.py: add DEFAULT_BRANCH, compose_graph_name(),
  parse_graph_name(); Graph and AsyncGraphQuery constructors now
  accept (name, branch=None); Graph.from_raw_name() classmethod for
  internal callers that need to bypass composition (e.g. clone());
  get_repos()/async_get_repos() now return {project, branch, graph}
  dicts.
- api/info.py: branch-aware Redis hash keys
  ('{repo}:{branch}_info'); reads fall back to legacy '{repo}_info'
  for un-migrated graphs.
- api/git_utils: GitRepoName() and switch_commit() thread branch
  through; LegacyGitRepoName() retained for the migration helper.
- api/project.py: detect_branch() via 'git rev-parse --abbrev-ref
  HEAD'; Project.__init__ / from_git_repository /
  from_local_repository accept branch.
- api/index.py: all Pydantic request models gain
  'branch: Optional[str]'; endpoints thread it into
  AsyncGraphQuery + info functions; responses include 'branch'.
- api/cli.py: --branch flag on index / index-repo / search /
  neighbors / paths / info; new 'cgraph migrate' command.
- api/migrations/per_branch.py (NEW): idempotent migration that
  renames legacy '<project>' graphs to 'code:<project>:_default',
  '{<project>}_info' Redis keys to '{<project>}:_default_info',
  and '{<project>}_git' graphs to '{<project>}:_default_git'.
  Supports --dry-run.

Tests:
- tests/test_per_branch_graphs.py (NEW): 24 unit tests covering
  compose/parse helpers, Graph constructor branch awareness,
  AsyncGraphQuery, info-key shape, GitRepoName shape, and migration
  idempotency (with mocked FalkorDB).
- tests/test_async_graph.py, tests/test_cli.py,
  tests/endpoints/test_list_repos.py: updated assertions for the
  new dict return shape from get_repos / async_get_repos.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a378ce6-7479-4286-8015-b20c72421cf8

📥 Commits

Reviewing files that changed from the base of the PR and between 9dfc721 and 8554534.

📒 Files selected for processing (1)
  • api/git_utils/git_utils.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/git_utils/git_utils.py

📝 Walkthrough

Walkthrough

Implements per-(repo, branch) graph identity: compose graph names as code:{project}:{branch}, add branch-aware Graph/AsyncGraphQuery/Project/info/git utilities, propagate branch through CLI and REST endpoints, add one-shot migration, and update tests and coverage/analyzer/LLM callsites.

Changes

Per-branch graph identity

Layer / File(s) Summary
Graph naming and repo enumeration with branch composition
api/graph.py
compose_graph_name() and parse_graph_name() establish code:{project}:{branch} naming. get_repos() and async_get_repos() return structured {project, branch, graph} dicts with internal suffix filtering and legacy fallback to _default. Graph and AsyncGraphQuery accept optional branch and compose/select the correct graph key. New Graph.from_raw_name() constructor for cloned instances. Removed single-name graph_exists() helper.
Project initialization with branch detection and Graph construction
api/project.py
detect_branch() auto-detects git branch via git rev-parse --abbrev-ref HEAD, falling back to _default. Project.__init__ accepts optional branch, auto-detects when omitted, and constructs Graph with resolved branch. from_git_repository() and from_local_repository() propagate branch. Commit persistence and git history processing now include resolved branch.
Branch-scoped metadata storage and git graph naming
api/info.py, api/git_utils/git_utils.py
Redis repo metadata stored under branch-qualified keys via _repo_info_key(repo, branch) with legacy fallback. Functions set_repo_commit, get_repo_commit, save_repo_info, get_repo_info, and async_get_repo_info accept optional branch and perform branch-aware lookups. git_repo_name()/GitRepoName() and legacy_git_repo_name() compose branch-scoped git-graph keys ({repo}:{branch}_git) with deprecated aliases. build_commit_graph() and switch_commit() propagate branch for branch-scoped operations.
CLI branch-aware indexing, querying, and migration commands
api/cli.py
index and index-repo accept --branch parameter. Query commands search, neighbors, paths, info accept optional --branch and auto-detect via detect_branch() when omitted. list command enumerates (project, branch) pairs. New migrate command with --dry-run flag delegates to run_migration() and returns JSON result.
REST API branch parameters and response updates
api/index.py
Request models add optional branch field. Endpoints accept and propagate branch to AsyncGraphQuery/AsyncGitGraph constructors. analyze_folder auto-detects branch when not provided. analyze_repo propagates data.branch and returns resolved branch. Read endpoints (graph_entities, get_neighbors, repo_info, find_paths, auto_complete) include branch in responses.
Code coverage, analyzer, autocomplete, and LLM branch support
api/code_coverage/lcov/lcov.py, api/analyzers/source_analyzer.py, api/auto_complete.py, api/llm.py
process_lcov(), analyze_local_repository(), prefix_search(), async_prefix_search(), and ask() accept optional branch and pass it into Graph/AsyncGraphQuery/KnowledgeGraph constructors.
One-shot per-branch migration with dry-run
api/migrations/__init__.py, api/migrations/per_branch.py
Introduces run_migration(dry_run=False) orchestrating legacy-to-per-branch rename. Helpers _rename_graph() and _rename_redis_key() perform idempotent copy/delete/rename operations with dry-run preview. Renames single-name graphs to code:{project}:_default, updates Redis info keys and git companion graphs. Returns summary dict with counts and skipped projects.
Comprehensive test coverage
tests/test_per_branch_graphs.py, tests/endpoints/test_list_repos.py, tests/test_async_graph.py, tests/test_cli.py, pyproject.toml
New tests/test_per_branch_graphs.py covers graph naming composition, constructor behavior, async enumeration, Redis/git naming helpers, and dry-run/idempotency migration tests. Updates existing tests to assert {project, branch, graph} dicts. Adds dev dependencies to pyproject.toml.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

"🐰
Branches snug in code and tree,
Graphs now wear their names with glee,
Two indexers hop, no collision heard,
Each project:branch keeps its own word,
Hooray — the rabbit stamps approval with a cheerful squeak!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(graph): per-branch graph identity (T17 #651)' clearly and concisely describes the main change: introducing per-branch graph identity to enable multi-branch indexing and prevent concurrent agents from overwriting each other's data.
Linked Issues check ✅ Passed All major objectives from issue #651 are implemented: branch-aware Graph/AsyncGraphQuery, per-branch Redis keys in info.py, branch-scoped git graphs, CLI branch support with auto-detection, branch parameters on REST endpoints, and idempotent migration. Test coverage added for key scenarios.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #651 scope. The PR correctly implements per-branch naming, migration, and backward compatibility as specified, and explicitly defers cross-branch queries, branch GC, and per-branch quotas to future phases.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dvirdukhan/mcp-t17-per-branch-graphs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@DvirDukhan DvirDukhan marked this pull request as ready for review May 28, 2026 13:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements per-(project, branch) graph identity (issue #651, T17) so concurrent agents indexing the same repo on different branches no longer overwrite each other. Graph names become code:{project}:{branch} (default branch _default), with matching {project}:{branch}_info Redis hashes and {project}:{branch}_git history graphs. Adds CLI --branch support, a cgraph migrate command for legacy graphs, branch-aware FastAPI request/response models, and 24 new unit tests.

Changes:

  • New naming primitives (compose_graph_name / parse_graph_name / DEFAULT_BRANCH) plus branch-aware Graph, AsyncGraphQuery, get_repos, set_/get_repo_* and GitRepoName, with legacy-key fallback on reads.
  • CLI gains --branch on index/index-repo/search/neighbors/paths/info (auto-detected via git rev-parse --abbrev-ref HEAD) and a new idempotent cgraph migrate [--dry-run] powered by api/migrations/per_branch.py.
  • REST endpoints accept optional branch in models/query params and surface it in responses; get_repos returns {project, branch, graph} dicts; tests updated accordingly.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
api/graph.py Adds branch-aware naming helpers, updates Graph/AsyncGraphQuery, repo listing dict shape, from_raw_name/clone.
api/info.py Branch-scoped Redis keys with legacy fallback for reads.
api/project.py detect_branch via git, branch propagation through Project and analysis.
api/git_utils/git_utils.py Branch-aware GitRepoName/build_commit_graph/switch_commit; legacy git key helper.
api/index.py branch on all request models / read endpoints; responses include resolved branch.
api/cli.py --branch option on commands; new migrate subcommand.
api/migrations/init.py / api/migrations/per_branch.py One-shot legacy→_default graph/Redis migration with dry-run.
api/auto_complete.py Threads branch into sync/async prefix search.
api/analyzers/source_analyzer.py analyze_local_repository takes optional branch, auto-detects from path.
api/code_coverage/lcov/lcov.py process_lcov accepts branch.
tests/test_per_branch_graphs.py New tests for composition/parsing, Graph constructors, async repos, info/git key shapes, migration.
tests/test_async_graph.py / tests/test_cli.py / tests/endpoints/test_list_repos.py Updated to new dict return shape of get_repos.
pyproject.toml / uv.lock Adds [dependency-groups].dev with pytest-anyio.
Comments suppressed due to low confidence (2)

api/index.py:232

  • ChatRequest exposes an optional branch field, but chat() calls ask(data.repo, data.msg) without it. ask() / _create_kg_agent() ultimately construct KnowledgeGraph(name=repo_name, ...) using the bare project name, which after T17 resolves to a different (and likely non-existent) FalkorDB key than the per-branch graph the user just indexed.

As a result, chat against a per-branch repo silently targets the wrong (legacy/missing) graph, and the branch parameter is misleadingly accepted but ignored. Either propagate data.branch through ask and _create_kg_agent (so the KnowledgeGraph is created with the composed code:{project}:{branch} name), or drop branch from ChatRequest until chat is wired up.

@app.post('/api/chat')
async def chat(data: ChatRequest, _=Depends(public_or_auth)):
    """Chat with the CodeGraph language model."""

    try:
        answer = await ask(data.repo, data.msg)
    except Exception as e:
        logging.exception("Chat error for repo '%s': %s", data.repo, e)
        return JSONResponse({"status": "error", "response": "Internal server error"},
                            status_code=500)

    return {"status": "success", "response": answer}

api/index.py:302

  • list_commits now selects a per-branch git graph via GitRepoName(data.repo, data.branch), but unlike the other read endpoints in this PR (graph_entities, get_neighbors, auto_complete, repo_info, find_paths) the response does not include "branch". For consistency — and so the frontend can disambiguate which branch the commit list came from — please include "branch" (normalized via _normalize_branch / DEFAULT_BRANCH) in the response payload.
async def list_commits(data: RepoRequest, _=Depends(public_or_auth)):
    """List all commits of a specified repository."""

    git_graph = AsyncGitGraph(git_utils.GitRepoName(data.repo, data.branch))
    try:
        commits = await git_graph.list_commits()
    finally:
        await git_graph.close()
    return {"status": "success", "commits": commits}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pyproject.toml Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
api/git_utils/git_utils.py (1)

303-318: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fail fast when the branch-scoped current commit is missing.

get_repo_commit() can return None, but this path continues into git_graph.get_commits([current_hash, to]) and only later raises a generic "Commits not found" error. A direct guard here makes the failure branch-specific and much easier to diagnose.

Suggested change
     current_hash = get_repo_commit(repo, branch)
     logging.info(f"Current graph commit: {current_hash}")
+    if current_hash is None:
+        raise ValueError(f'Missing current commit metadata for repository "{repo}"')
 
     if current_hash == to:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/git_utils/git_utils.py` around lines 303 - 318, Check for a missing
branch-scoped commit immediately after calling get_repo_commit(repo, branch): if
current_hash is None, log an error mentioning the branch and raise a ValueError
with a descriptive message (e.g., "Current commit for branch X not found")
instead of proceeding to git_graph.get_commits; update the existing debug log
for current_hash to use an f-string (logging.debug(f"Current commit:
{current_hash} is the requested commit")) and then keep the existing flow using
git_graph.get_commits([current_hash, to]) only when current_hash is not None.
api/cli.py (1)

229-243: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Explicit --branch does not actually select that branch.

Project.from_git_repository(url, branch=branch) only tags the graph with branch; the factory in api/project.py clones the remote first and never checks out the requested branch before analyze_sources(). That means cgraph index-repo --branch feature-x can index the default checkout into the feature-x graph, which breaks the per-branch isolation this PR is adding. Please either check out the requested branch immediately after cloning or fail fast until that path supports branch selection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/cli.py` around lines 229 - 243, The branch option is never actually
checked out after cloning — Project.from_git_repository(url, branch=branch) only
tags the graph but does not change the working tree before analyze_sources(), so
indexing uses the default checkout. After creating the Project
(Project.from_git_repository) but before calling project.analyze_sources(),
either perform an explicit checkout of the requested branch on the cloned repo
(e.g., use the Project's repo handle to checkout branch) or raise an error if
branch switching is unsupported; ensure the checkout occurs before calling
project.analyze_sources(ignore=...) so analyze_sources() runs against the
requested branch.
🧹 Nitpick comments (1)
api/auto_complete.py (1)

6-6: ⚡ Quick win

Fix prefix_search return type annotation to match actual return value.

prefix_search appears to return completion collections (not a string), and async_prefix_search already advertises list. Aligning these hints avoids type-checking drift.

Suggested diff
-def prefix_search(repo: str, prefix: str, branch: Optional[str] = None) -> str:
+def prefix_search(repo: str, prefix: str, branch: Optional[str] = None) -> list:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/auto_complete.py` at line 6, Update the return type annotation of
prefix_search in api/auto_complete.py to match the actual returned completion
collection (as async_prefix_search does) instead of str; change the annotation
on def prefix_search(repo: str, prefix: str, branch: Optional[str] = None) ->
str to a list-return type (e.g., -> list or a more specific -> list[dict] / ->
List[Dict[str, Any]] or the actual Completion type used by async_prefix_search)
so static typing aligns with the real return value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/git_utils/git_utils.py`:
- Around line 17-30: Rename the mixed-case helpers GitRepoName and
LegacyGitRepoName to snake_case (git_repo_name, legacy_git_repo_name) and update
all call sites and tests to use the new names; keep the same behavior/formatting
(including the "{repo}" hash-tag behavior) so callers aren't otherwise affected.
In addition, adjust get_repo_commit's signature/typing to return Optional[str]
and modify switch_commit to guard against a None current_hash (i.e., check if
current_hash is None before using it and handle the "missing commit" path
appropriately—fail fast or return a clear error), updating any logic that
assumes a non-null return. Ensure you change only symbol names and usages
(function names: GitRepoName/LegacyGitRepoName ->
git_repo_name/legacy_git_repo_name, get_repo_commit, and switch_commit) so
linters and type-checkers reflect the Optional return type.

In `@api/graph.py`:
- Around line 131-133: Normalize the branch value before storing it on the
wrapper: if the incoming branch is None or the empty string, set a normalized
value (use DEFAULT_BRANCH) and assign that to self.branch before calling
compose_graph_name so self.name matches the actual graph key; update the same
logic in both constructor sites (the block using self.project / self.branch /
self.name and the other occurrence around lines 798-800) so exposed self.branch
never remains "" and always reflects the key used by compose_graph_name.
- Around line 95-99: The loop over db.list_graphs() currently filters out names
using _is_internal_suffix(g) before parse_graph_name(g), causing valid composed
graph names like "code:repo:release_tmp" to be incorrectly skipped; change the
logic so you first call parse_graph_name(g) (check for None) and then, if parsed
indicates a branch/name to inspect, apply _is_internal_suffix to the branch
component (or the parsed full name) to decide whether to continue; update the
loop in the function that iterates db.list_graphs() so parse_graph_name(g) runs
first and only then use _is_internal_suffix on the appropriate parsed part.

In `@api/index.py`:
- Around line 78-81: ChatRequest.branch is collected but never used: update the
chat handling and ask flow so branch is respected or explicitly rejected. Either
(A) change ask(repo, msg) to ask(repo, msg, branch: Optional[str]) and propagate
data.branch from chat() into the ask(...) call and into the graph lookup logic
inside ask (use branch when resolving the repository/graph), and update any
other places that call ask(...) (search for other ask( calls) to pass an
optional branch; or (B) if branch-scoped graphs aren't supported yet, validate
in chat() that data.branch is None and return a 400/error for non-null branch.
Ensure the change references the ChatRequest.branch field, the chat() handler,
and the ask(...) function/graph lookup logic so branch is either threaded
through or explicitly rejected.

In `@api/migrations/per_branch.py`:
- Around line 70-80: The current rename flow aborts when
db.connection.exists(dst) is true which leaves stale src graphs if a prior run
copied dst but failed before deleting src; update the rename logic that uses
db.connection.exists, db.select_graph(src), g.copy(dst) and g.delete() so that
if dst exists but src still exists you detect this partial-run state and either
(a) verify the graphs are identical (e.g. compare checksums/metadata) and if so
delete src and log a completed-retry, or (b) if verification is not possible,
attempt an atomic safe-retry by copying src to a temp dst name then swapping or
else log a clear error and surface failure; ensure dry_run still only logs
actions and do not delete in dry_run.

In `@api/project.py`:
- Around line 68-71: In Project.__init__, normalize an explicit empty string
branch the same as missing by treating branch == "" like None before
autodetection: update the branch handling (the block using detect_branch(path)
and DEFAULT_BRANCH) to check if branch is None or branch == "" (or use branch =
branch or ... pattern) so autodetection runs for empty strings and then assign
the resolved value to self.branch; reference the Project.__init__ parameter
branch, detect_branch(path), DEFAULT_BRANCH, and self.branch when making the
change.

---

Outside diff comments:
In `@api/cli.py`:
- Around line 229-243: The branch option is never actually checked out after
cloning — Project.from_git_repository(url, branch=branch) only tags the graph
but does not change the working tree before analyze_sources(), so indexing uses
the default checkout. After creating the Project (Project.from_git_repository)
but before calling project.analyze_sources(), either perform an explicit
checkout of the requested branch on the cloned repo (e.g., use the Project's
repo handle to checkout branch) or raise an error if branch switching is
unsupported; ensure the checkout occurs before calling
project.analyze_sources(ignore=...) so analyze_sources() runs against the
requested branch.

In `@api/git_utils/git_utils.py`:
- Around line 303-318: Check for a missing branch-scoped commit immediately
after calling get_repo_commit(repo, branch): if current_hash is None, log an
error mentioning the branch and raise a ValueError with a descriptive message
(e.g., "Current commit for branch X not found") instead of proceeding to
git_graph.get_commits; update the existing debug log for current_hash to use an
f-string (logging.debug(f"Current commit: {current_hash} is the requested
commit")) and then keep the existing flow using
git_graph.get_commits([current_hash, to]) only when current_hash is not None.

---

Nitpick comments:
In `@api/auto_complete.py`:
- Line 6: Update the return type annotation of prefix_search in
api/auto_complete.py to match the actual returned completion collection (as
async_prefix_search does) instead of str; change the annotation on def
prefix_search(repo: str, prefix: str, branch: Optional[str] = None) -> str to a
list-return type (e.g., -> list or a more specific -> list[dict] / ->
List[Dict[str, Any]] or the actual Completion type used by async_prefix_search)
so static typing aligns with the real return value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a2741065-86c8-4bb0-86c2-c7ad45352562

📥 Commits

Reviewing files that changed from the base of the PR and between d86860d and 04d458a.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • api/analyzers/source_analyzer.py
  • api/auto_complete.py
  • api/cli.py
  • api/code_coverage/lcov/lcov.py
  • api/git_utils/git_utils.py
  • api/graph.py
  • api/index.py
  • api/info.py
  • api/migrations/__init__.py
  • api/migrations/per_branch.py
  • api/project.py
  • pyproject.toml
  • tests/endpoints/test_list_repos.py
  • tests/test_async_graph.py
  • tests/test_cli.py
  • tests/test_per_branch_graphs.py

Comment thread api/git_utils/git_utils.py Outdated
Comment thread api/graph.py
Comment thread api/graph.py
Comment thread api/index.py
Comment thread api/migrations/per_branch.py
Comment thread api/project.py
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Actionable comments posted: 0

- pyproject + uv.lock: drop pytest-anyio 0.0.0 placeholder; anyio's
  pytest plugin (already required via tests extra) provides the
  @pytest.mark.anyio marker. (Copilot review)

- api/graph.py: when listing graphs, apply the internal-suffix filter
  to the *branch* component for parsed names (e.g.
  code:repo:main_git is hidden, but code:repo:release_tmp stays
  visible). Legacy bare names still filtered by full name.
  (CodeRabbit graph.py:99)

- api/graph.py: Graph/AsyncGraphQuery constructors now coerce
  branch="" to DEFAULT_BRANCH alongside branch=None, so self.branch
  matches the actual graph key produced by compose_graph_name.
  (CodeRabbit graph.py:133)

- api/project.py: same coercion for Project.__init__; an explicit
  empty-string branch now autodetects (or falls back to
  DEFAULT_BRANCH) instead of staying "". (CodeRabbit project.py:71)

- api/index.py + api/llm.py: ChatRequest.branch is now threaded
  through ask() / _ask_sync() / _create_kg_agent() and used to
  compose the FalkorDB graph key. Previously it was accepted on the
  request but silently ignored. (CodeRabbit index.py:81)

- api/migrations/per_branch.py: when both src and dst graphs exist
  (interrupted prior migration) the rename helper now deletes the
  leftover legacy src and reports success, so reruns can complete
  cleanly instead of skipping forever. Dry-run path preserved.
  (CodeRabbit per_branch.py:80)

- api/git_utils/git_utils.py: rename CamelCase helpers
  GitRepoName/LegacyGitRepoName -> git_repo_name/legacy_git_repo_name
  (snake_case per api guidelines); keep CamelCase as deprecated
  aliases so the public surface stays backwards compatible. Update
  the in-module callers. switch_commit now guards against a None
  current_hash and raises a clear ValueError instead of silently
  walking off the end. (CodeRabbit git_utils.py:30)

- api/info.py: get_repo_commit return type tightened to
  Optional[str] to reflect the documented None return.
  (CodeRabbit git_utils.py:30)

All 24 tests in tests/test_per_branch_graphs.py still pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Actionable comments posted: 0

@galshubeli
Copy link
Copy Markdown
Contributor

Reviewed #675 — great work, this is exactly what was needed. Migration story is solid, backwards-compat is real, hash-tag preservation is forward-thinking for cluster mode. Approving.

Three small follow-ups worth tracking, not blockers:

  1. The bench adapter in bench: fix find_symbol exact-match against nested properties.name #674 needs branch plumbed through — same shape change, otherwise prior bench runs continue to query _default instead of your actual checkout's branch.
  2. Our MCP tools (api/mcp/tools/structural.py) also need branch — they'll be querying the wrong graph after merge until we update them. I'll do that in a small PR stacked on this.
  3. GitRepoName("foo") is now silently returning a different string than before — worth either a deprecation warning on the alias or a one-line note in the changelog.

Per @galshubeli's review on #675: single-arg GitRepoName('foo') used to
return '{foo}_git' but now returns '{foo}:<DEFAULT_BRANCH>_git' — a
silent shape change for anyone still on the alias. Make it loud with
DeprecationWarning so leftover callers find out before they query a
graph that no longer exists.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@DvirDukhan
Copy link
Copy Markdown
Contributor Author

Thanks for the approval @galshubeli! Addressed #3 in 8554534GitRepoName/LegacyGitRepoName now emit DeprecationWarning (with the explicit shape-change note so call-sites discover the silent behavior change rather than 404'ing on a missing graph).

For the other two:

  1. bench adapter (bench: fix find_symbol exact-match against nested properties.name #674) — agreed, that's a sibling PR; I'll plumb branch through code_graph_adapter there once feat(graph): per-branch graph identity (T17 #651) #675 lands so we don't conflict.
  2. MCP tools — happy for you to stack a small PR on top of this one; the structural.py change should be the same shape (branch: str | None = Nonecompose_graph_name(project, branch)).

PR's ready to merge from my side.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Actionable comments posted: 0

@DvirDukhan DvirDukhan merged commit cc18c62 into staging May 28, 2026
10 of 12 checks passed
@DvirDukhan DvirDukhan deleted the dvirdukhan/mcp-t17-per-branch-graphs branch May 28, 2026 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[MCP T17] Per-branch graph identity (multi-branch indexing)

3 participants