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
15 changes: 11 additions & 4 deletions java_codebase_rag/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,20 +202,27 @@ def discover_project_root(start: Path) -> Path | None:
First match wins (closest to start). Config file takes priority over index
directory at the same level. Stops at $HOME inclusive — checks $HOME itself
but does not walk past it. Returns None if no marker found.

A bare ``.java-codebase-rag/`` index directory at ``$HOME`` is intentionally
NOT treated as an anchor (issue #357): a stray home-level index (e.g. an
accidental ``init`` run from home) would otherwise hijack resolution for any
command run from a ``$HOME`` subdir without its own marker, silently reading
and writing the home-level index. A config file at ``$HOME`` still anchors.
"""
start = start.resolve()
home = Path.home().resolve()

current = start
while True:
# Config file is the primary anchor
# Config file is the primary anchor (valid at every level, including $HOME).
if find_yaml_config_file(current) is not None:
return current
# Index directory is the secondary anchor (supports indexes without config)
if _has_index_dir(current):
# Index directory is the secondary anchor (supports indexes without config),
# but NOT at $HOME — see the docstring for the cross-project hijack rationale.
if current != home and _has_index_dir(current):
return current

# Stop if we've reached home (check home itself, but don't walk past it)
# Stop if we've reached home (config-file check above already handled home)
if current == home:
return None

Expand Down
42 changes: 42 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,48 @@ def test_discover_project_root_both_markers_same_level(self, tmp_path):
result = discover_project_root(tmp_path)
assert result == tmp_path

def test_discover_project_root_ignores_stray_index_dir_at_home(self, tmp_path, monkeypatch):
"""A bare .java-codebase-rag/ index dir at $HOME must not anchor project
root (issue #357). Otherwise a command run from any $HOME subdir without
its own marker silently resolves to $HOME and reads/writes the home-level
index (cross-project resolution)."""
fake_home = tmp_path / "home"
fake_home.mkdir()
project_dir = fake_home / "project"
project_dir.mkdir()
# Stray index dir at $HOME (e.g. an accidental `init` run from home).
stray_idx = fake_home / ".java-codebase-rag"
stray_idx.mkdir()
(stray_idx / "code_graph.lbug").write_bytes(b"\x00" * 16)

monkeypatch.setenv("HOME", str(fake_home))

result = discover_project_root(project_dir)
assert result is None, "stray ~/.java-codebase-rag/ must not anchor at $HOME (#357)"

def test_discover_project_root_config_at_home_still_anchors(self, tmp_path, monkeypatch):
"""A config file at $HOME still anchors even with a stray index dir beside
it — the #357 fix only demotes the bare index-dir signal at $HOME, not the
config-file anchor (a deliberate ~/.java-codebase-rag.yml is intentional)."""
fake_home = tmp_path / "home"
fake_home.mkdir()
project_dir = fake_home / "project"
project_dir.mkdir()
# Stray index dir at $HOME, NON-empty: a real accidental `init` leaves
# code_graph.lbug behind, so _has_index_dir (which requires non-empty)
# actually sees it. An empty mkdir() would be invisible to the index-anchor
# check and would not represent the documented "stray index dir beside it"
# scenario -- mirrors test_discover_project_root_ignores_stray_index_dir_at_home.
stray_idx = fake_home / ".java-codebase-rag"
stray_idx.mkdir()
(stray_idx / "code_graph.lbug").write_bytes(b"\x00" * 16)
(fake_home / YAML_CONFIG_FILENAMES[0]).write_text("# home config")

monkeypatch.setenv("HOME", str(fake_home))

result = discover_project_root(project_dir)
assert result == fake_home


class TestSourceRootFromYaml:
"""Tests for source_root YAML field parsing and resolution."""
Expand Down
Loading