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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"name": "repobrain",
"source": "./",
"description": "Repository knowledge engine: CLI-backed slash commands (rb-setup / rb-init / rb-refresh / rb-ask) for both Claude Code and Codex CLI, agent-repo-init skill, and optional MCP server.",
"version": "0.3.1",
"version": "0.3.2",
"category": "developer-tools",
"keywords": ["mcp", "knowledge-graph", "multi-agent", "scaffolding"]
}
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "repobrain",
"version": "0.3.1",
"version": "0.3.2",
"description": "Repository knowledge engine plugin. Bundles CLI-backed slash commands (rb-setup / rb-init / rb-refresh / rb-ask) plus the agent-repo-init skill. Works in both Claude Code and Codex CLI.",
"author": {
"name": "RepoBrain Team",
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "repobrain",
"version": "0.3.1",
"version": "0.3.2",
"description": "Repository knowledge engine plugin (Codex CLI). Bundles slash commands (/rb-setup, /rb-init, /rb-refresh, /rb-ask) plus the agent-repo-init skill. Backed by the rb-ask / rb-refresh CLI; MCP optional.",
"skills": "./skills/",
"interface": {
Expand Down
2 changes: 1 addition & 1 deletion engine/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "repobrain-engine"
version = "0.3.1"
version = "0.3.2"
description = "Repository knowledge engine with grounded Q&A and optional MCP server"
requires-python = ">=3.10"
dependencies = [
Expand Down
2 changes: 1 addition & 1 deletion engine/repobrain_engine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from repobrain_engine.config import Settings

__version__ = "0.3.1"
__version__ = "0.3.2"

__all__ = [
"Settings",
Expand Down
38 changes: 35 additions & 3 deletions engine/repobrain_engine/hub/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -889,16 +889,48 @@ def quick_scan(root: Path, since_sha: str) -> ScanReport:

try:
result = subprocess.run(
["git", "diff", "--name-only", since_sha, "HEAD"],
[
"git",
"diff",
"--name-only",
"-z",
"--no-renames",
"--relative",
since_sha,
"--",
".",
],
capture_output=True,
text=True,
cwd=str(root),
check=False,
)
if result.returncode != 0:
return full_scan(root)

changed_files = [f.strip() for f in result.stdout.splitlines() if f.strip()]
untracked_result = subprocess.run(
[
"git",
"ls-files",
"--others",
"--exclude-standard",
"-z",
"--",
".",
],
capture_output=True,
cwd=str(root),
check=False,
)
if untracked_result.returncode != 0:
return full_scan(root)

raw_paths = (
result.stdout.split(b"\0")
+ untracked_result.stdout.split(b"\0")
)
changed_files = sorted(
{os.fsdecode(raw_path) for raw_path in raw_paths if raw_path}
)
report.changed_files = changed_files
except FileNotFoundError:
return full_scan(root)
Expand Down
87 changes: 85 additions & 2 deletions engine/tests/test_hub_scanner.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for hub.scanner — pure Python, no LLM needed."""
import subprocess
from pathlib import Path

from repobrain_engine.hub._constants import WORKSPACE_ROOT_MODULE_ID
Expand Down Expand Up @@ -124,6 +125,90 @@ def test_quick_scan_falls_back_to_full(tmp_path: Path) -> None:
assert isinstance(report, ScanReport)


def _init_git_repo(path: Path) -> None:
subprocess.run(["git", "init", "-q"], cwd=path, check=True)
subprocess.run(
["git", "config", "user.email", "test@example.com"],
cwd=path,
check=True,
)
subprocess.run(
["git", "config", "user.name", "Test"],
cwd=path,
check=True,
)


def test_quick_scan_includes_worktree_and_untracked_changes(tmp_path: Path) -> None:
"""Quick scans include changes that have not been committed yet."""
_init_git_repo(tmp_path)
tracked = tmp_path / "tracked.py"
tracked.write_text("value = 1\n", encoding="utf-8")
subprocess.run(["git", "add", "tracked.py"], cwd=tmp_path, check=True)
subprocess.run(["git", "commit", "-qm", "base"], cwd=tmp_path, check=True)
base = subprocess.check_output(
["git", "rev-parse", "HEAD"], cwd=tmp_path, text=True
).strip()

tracked.write_text("value = 2\n", encoding="utf-8")
(tmp_path / "new.py").write_text("created = True\n", encoding="utf-8")

report = quick_scan(tmp_path, base)

assert report.changed_files == ["new.py", "tracked.py"]
assert report.file_count == 2
assert set(report.file_metadata) == {"tracked.py", "new.py"}


def test_quick_scan_uses_workspace_relative_non_ascii_paths(tmp_path: Path) -> None:
"""Git paths stay relative to a nested workspace and preserve Unicode."""
_init_git_repo(tmp_path)
subprocess.run(
["git", "config", "core.quotePath", "true"], cwd=tmp_path, check=True
)
workspace = tmp_path / "子目录"
workspace.mkdir()
source = workspace / "测试.py"
source.write_text("value = 1\n", encoding="utf-8")
subprocess.run(["git", "add", "子目录/测试.py"], cwd=tmp_path, check=True)
subprocess.run(["git", "commit", "-qm", "base"], cwd=tmp_path, check=True)
base = subprocess.check_output(
["git", "rev-parse", "HEAD"], cwd=tmp_path, text=True
).strip()

source.write_text("value = 2\n", encoding="utf-8")
subprocess.run(["git", "commit", "-qam", "change"], cwd=tmp_path, check=True)

report = quick_scan(workspace, base)

assert report.changed_files == ["测试.py"]
assert report.scanned_file_samples == ["测试.py"]
assert report.file_count == 1
assert "测试.py" in report.file_metadata


def test_quick_scan_reports_both_sides_of_rename(tmp_path: Path) -> None:
"""Renames invalidate both the old and new paths for incremental refresh."""
_init_git_repo(tmp_path)
source = tmp_path / "old.py"
source.write_text("value = 1\n", encoding="utf-8")
subprocess.run(["git", "add", "old.py"], cwd=tmp_path, check=True)
subprocess.run(["git", "commit", "-qm", "base"], cwd=tmp_path, check=True)
base = subprocess.check_output(
["git", "rev-parse", "HEAD"], cwd=tmp_path, text=True
).strip()

source.rename(tmp_path / "new.py")
subprocess.run(["git", "add", "-A"], cwd=tmp_path, check=True)
subprocess.run(["git", "commit", "-qm", "rename"], cwd=tmp_path, check=True)

report = quick_scan(tmp_path, base)

assert report.changed_files == ["new.py", "old.py"]
assert report.file_count == 1
assert set(report.file_metadata) == {"new.py"}


def test_detect_modules_finds_go_directories(tmp_path: Path) -> None:
"""Go source directories should be treated as analyzable modules."""
cmd_dir = tmp_path / "cmd"
Expand Down Expand Up @@ -241,8 +326,6 @@ def test_full_scan_git_summary_no_git(tmp_path: Path) -> None:

def test_full_scan_git_summary_with_repo(tmp_path: Path) -> None:
"""git_summary contains commit info when a git repo exists."""
import subprocess

subprocess.run(["git", "init"], cwd=str(tmp_path), capture_output=True, check=True)
subprocess.run(
["git", "config", "user.email", "test@test.com"],
Expand Down