From 3215469afb9315441f0bf70f9df45ef6484d8f3c Mon Sep 17 00:00:00 2001 From: leisen6688 Date: Fri, 10 Jul 2026 16:41:41 +0800 Subject: [PATCH] fix: include working tree paths in quick scans --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- engine/pyproject.toml | 2 +- engine/repobrain_engine/__init__.py | 2 +- engine/repobrain_engine/hub/scanner.py | 38 ++++++++++- engine/tests/test_hub_scanner.py | 87 +++++++++++++++++++++++++- 7 files changed, 125 insertions(+), 10 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ddaf7d9f..b1422b67 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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"] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 0117da92..82dd82ac 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -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", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index e2c5fc04..ec2b6b5d 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -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": { diff --git a/engine/pyproject.toml b/engine/pyproject.toml index e398d33a..2c564776 100644 --- a/engine/pyproject.toml +++ b/engine/pyproject.toml @@ -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 = [ diff --git a/engine/repobrain_engine/__init__.py b/engine/repobrain_engine/__init__.py index 0ed581a6..27a5f7c2 100644 --- a/engine/repobrain_engine/__init__.py +++ b/engine/repobrain_engine/__init__.py @@ -8,7 +8,7 @@ from repobrain_engine.config import Settings -__version__ = "0.3.1" +__version__ = "0.3.2" __all__ = [ "Settings", diff --git a/engine/repobrain_engine/hub/scanner.py b/engine/repobrain_engine/hub/scanner.py index 7659ed9a..c9e69e1a 100644 --- a/engine/repobrain_engine/hub/scanner.py +++ b/engine/repobrain_engine/hub/scanner.py @@ -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) diff --git a/engine/tests/test_hub_scanner.py b/engine/tests/test_hub_scanner.py index 1f9c0814..254fad04 100644 --- a/engine/tests/test_hub_scanner.py +++ b/engine/tests/test_hub_scanner.py @@ -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 @@ -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" @@ -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"],