Skip to content

Commit b9ad5d9

Browse files
takemi-ohamaclaude
andcommitted
fix(plugin): 名前指定インストールの @ref 拒否と status の空 path ガード
- installer.py: `devbase plugin install myplugin@v1` の名前指定インストール分岐で source.ref を _install_from_repo() に渡し既定ブランチを黙ってインストールしていた 問題を修正。未登録/登録済みリポジトリと同様に @ref を PluginError で拒否する (major) - status.py: plugin.path が空文字列の場合に環境ルートの projects/ を誤参照する 可能性を防ぐため事前ガードを追加し 0 件扱いとする (minor / 堅牢性) - 回帰テスト追加: test_install_ref_rejected_for_name_only / test_project_count_zero_when_path_empty Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d746317 commit b9ad5d9

4 files changed

Lines changed: 57 additions & 1 deletion

File tree

lib/devbase/commands/status.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ def _get_plugin_info(registry: PluginRegistry) -> list[dict]:
105105
# plugin.path は devbase_root からの相対パス。
106106
# repos/ ベース (repos/<repo>/<subdir>) と --link ベース
107107
# (plugins/<name>) の両方を同じロジックで解決する。
108+
# path が空の場合 (旧/破損エントリ) は devbase_root/projects を
109+
# 誤参照してしまうため、先にガードして 0 件扱いとする。
110+
if not plugin.path:
111+
results.append({"name": plugin.name, "project_count": 0})
112+
continue
108113
plugin_projects_dir = registry.devbase_root / plugin.path / "projects"
109114
if plugin_projects_dir.is_dir():
110115
project_count = sum(

lib/devbase/plugin/installer.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,24 @@ def install_plugin(
123123
plugins_dir = registry.get_plugins_dir()
124124

125125
if not source.repo and source.plugin_name:
126+
# Reject @ref on name-only installs too — the permanent clone tracks
127+
# the default branch and does not support pinned refs. Without this
128+
# guard, `devbase plugin install myplugin@v1` would silently drop the
129+
# ref in _install_from_repo() and install the default branch instead.
130+
# This matches the validation for unregistered/registered repos below.
131+
if source.ref:
132+
raise PluginError(
133+
f"Cannot use @{source.ref} with plugin '{source.plugin_name}'.\n"
134+
"Permanent clones track the default branch and do not support pinned refs.\n"
135+
f"Install without @ref:\n"
136+
f" devbase plugin install {source.plugin_name}"
137+
)
126138
result = registry.find_plugin_in_repos(source.plugin_name)
127139
if result:
128140
repo, avail_plugin = result
129141
repo_source = PluginSource(
130142
repo=repo.url, plugin_name=source.plugin_name,
131-
ref=source.ref, linked=False,
143+
ref=None, linked=False,
132144
)
133145
_install_from_repo(
134146
registry, repo_source, install_all=False,

tests/plugin/test_repos_core.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,27 @@ def test_install_ref_rejected_for_registered_repo(self, registry, devbase_root):
443443
with pytest.raises(PluginError, match="Cannot use @v2.0"):
444444
install_plugin(registry, "testorg/testrepo:myplugin@v2.0")
445445

446+
def test_install_ref_rejected_for_name_only(self, registry, devbase_root):
447+
"""@ref on a name-only install is rejected too.
448+
449+
Without the guard, `devbase plugin install myplugin@v1` would parse
450+
to (repo='', plugin_name='myplugin', ref='v1'), enter the
451+
find_plugin_in_repos branch and silently drop the ref in
452+
_install_from_repo(), installing the default branch instead.
453+
"""
454+
url = "https://github.com/testorg/testrepo.git"
455+
_make_repo_dir(devbase_root, "testorg/testrepo", [
456+
{"name": "myplugin", "path": "myplugin", "projects": ["myproj"]},
457+
])
458+
_register_repo(registry, "testorg/testrepo", url, [
459+
{"name": "myplugin", "path": "myplugin"},
460+
])
461+
462+
with pytest.raises(PluginError, match="Cannot use @v1"):
463+
install_plugin(registry, "myplugin@v1")
464+
# registry must not have installed the plugin from the default branch
465+
assert registry.get("myplugin") is None
466+
446467
def test_install_legacy_repo_without_local_path(self, registry, devbase_root):
447468
"""Legacy repos (no local_path) are auto-migrated to persistent clone."""
448469
url = "https://github.com/testorg/testrepo.git"

tests/plugin/test_status_project_count.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,21 @@ def test_project_count_zero_when_no_projects_dir(registry, devbase_root):
8585

8686
info = _get_plugin_info(registry)
8787
assert info == [{"name": "noproj", "project_count": 0}]
88+
89+
90+
def test_project_count_zero_when_path_empty(registry, devbase_root):
91+
"""path が空 (旧/破損エントリ) のとき環境ルートの projects を誤参照しない。"""
92+
# 環境ルートに projects/ が存在し、中身があっても 0 と数えること。
93+
_make_projects(devbase_root, ["root-proj-a", "root-proj-b"])
94+
95+
registry.add(InstalledPlugin(
96+
name="brokenpath",
97+
version="0.1.0",
98+
source="",
99+
installed_at=registry.now_iso(),
100+
path="",
101+
linked=False,
102+
))
103+
104+
info = _get_plugin_info(registry)
105+
assert info == [{"name": "brokenpath", "project_count": 0}]

0 commit comments

Comments
 (0)