Skip to content

feat(plugin): 旧 plugins/ コピーインストールを repos/ 永続クローンへ移行する devbase plugin migrate を追加#31

Merged
takemi-ohama merged 12 commits into
release/PLAN04from
feature/PLAN04-migration
May 28, 2026
Merged

feat(plugin): 旧 plugins/ コピーインストールを repos/ 永続クローンへ移行する devbase plugin migrate を追加#31
takemi-ohama merged 12 commits into
release/PLAN04from
feature/PLAN04-migration

Conversation

@takemi-ohama
Copy link
Copy Markdown
Contributor

何のために

PR1 (#29) で plugin 管理を repos/ 配下の永続クローン方式に切り替えたが、それ以前に plugins/<name>/ へファイルコピーされた既存インストールは新方式へ移行されないまま残る。コピーは git pull による update ができず、projects/ のシンボリックリンクも旧パスを指したままになる。

本 PR は、旧形式のコピーインストールを検出して repos/ 永続クローンベースへ移行するロジック (devbase plugin migrate) を追加し、既存ユーザーが追加操作なしで新方式へ移れるようにする。

何を

マイグレーションロジック (lib/devbase/plugin/migrator.py 新規)

  • 旧形式 (plugins/<name> 下のコピー、かつ --link でない) のインストールを検出 (needs_migration)
  • ソースリポジトリが repos/ に未クローンなら永続クローンを作成。local_path 未設定の旧登録や、健全な既存クローンが残っている場合はそれを再利用する
  • 旧コピーとクローン内プラグインの差分を判定:
    • 一致 → 旧コピーを削除し repos/ へ移行 (migrated)
    • ユーザーによるローカル変更あり → 旧コピーを plugins/<name>.bak として保全 (preserved。既存 .bak がある場合は .bak-2, .bak-3 … と退避し上書きしない)
    • ソース未登録・registry.yml 不在など移行不能 → スキップしエラーを報告 (skipped)
  • 差分判定は exec ビット・サイズ・バイト内容で比較し、ファイルは固定長チャンク読みでメモリを圧迫しない。symlink のターゲット差分・型不一致・コピー側のみに存在するエントリ (ユーザー追加) も検出。upstream 側のみの追加は「コピー削除で失われるデータ」ではないため差分とみなさない
  • InstalledPlugin.pathrepos/ 配下へ書き換え、sync_projectsprojects/ シンボリックリンクを張り直す
  • 移行後に plugins/ を走査し、--link インストール・スキップされた旧コピー・保全された .bak のいずれも残っていなければ .gitkeep のみ残して掃除する

一貫性・堅牢性

  • 二相コミット: registry へのクローン行と path 書き換えを 単一の plugins.yml save (PluginRegistry.save_migration) で永続化してから、旧コピーの削除/退避を行う。save が失敗しても旧コピーは無傷で残り、再実行でクリーンにリトライできる
  • .git を持つディレクトリ (uncommitted/unpushed のローカル変更を含み得る) は削除せず、エラーで手動対応を促す。partial clone (.gitregistry.yml を欠く壊れたクローン) は再クローンで自己修復する

コマンド連携

  • devbase plugin migrate サブコマンド追加 (lib/devbase/cli.py, lib/devbase/commands/plugin.py)
  • install / update 実行時に旧形式が残っていれば自動でマイグレーションを起動 (_auto_migrate)。手動実行は通常不要。preserved/skipped は毎回繰り返さないよう、簡潔なヒント 1 行に集約する
  • docs/user/cli-reference.mddevbase plugin migrate の説明と挙動表を追記

Test plan

tests/plugin/test_migrator.py (約 1054 行 / 23 クラス・59 ケース) で網羅:

  • 旧形式判定: コピーは legacy、repos/ ベース・--link は対象外
  • クリーン移行: path 書き換え + 旧コピー削除 + projects/ シンボリックリンク生成 + plugins/.gitkeep へ掃除
  • ローカル変更あり: plugins/<name>.bak として保全、既存 .bak は上書きせず番号付き退避
  • 差分判定: 内容差・同サイズ別内容・コピー側のみのファイル/symlink/空ディレクトリ・symlink ターゲット差・型不一致・exec ビット差を検出。upstream のみの追加・read/write 権限差は差分としない
  • 未クローンのソースリポジトリのクローン、健全な既存クローンの再利用
  • partial clone (.git 欠落) の再クローン、.git を持つクローンの保護 (削除拒否)
  • registry 書き込みの単一 save バッチ化 (複数プラグイン・複数リポジトリ)
  • registry 永続化後にのみ旧コピーを retire / save 失敗時はコピー無傷
  • devbase plugin migrate コマンド実行・対象なし時の no-op
  • install / update 時の自動マイグレーション起動と warning 抑制

takemi-ohama and others added 9 commits May 28, 2026 04:34
PLAN04 PR2。PR1 (#29) で repos/ 永続クローン方式に切り替えたが、PR1 以前に
plugins/<name>/ へファイルコピーされた既存インストールは移行されないため、その
移行ロジックを追加する。

- migrator.py (新規):
  - needs_migration / _is_legacy_plugin: legacy plugins/ インストールの検出
    (linked は --link 専用として除外)
  - _dirs_differ: コピーとクローンの差分検出 (内容変更・追加ファイルを保守的に差分扱い)
  - migrate: 未クローン repo の永続クローン作成、InstalledPlugin.path の repos/ 書き換え、
    差分なしは plugins/<name> 削除・差分ありは <name>.bak 保全、sync_projects 再実行、
    --link/.bak/skip が無ければ plugins/ を .gitkeep のみに正規化
- plugin migrate サブコマンド (cli.py / commands/plugin.py)
- install/update 初回実行時に _auto_migrate で自動移行

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
cross-review round 1 の major 指摘 4 件 (codex 2 / gemini 2) に対応。

- _dirs_differ: regular file のみ比較していたため legacy copy のみに存在する
  symlink / 空ディレクトリ / 型不一致を差分として検知できず、後続の
  shutil.rmtree で silently 削除される恐れがあった。全エントリを対象に
  型 + 内容 (file は byte, symlink は target) を比較するよう厳密化
- _unique_bak_path: 既存の <name>.bak を無条件に rmtree していたため、
  前回 migration で保全した未整理バックアップが消失する恐れがあった。
  存在時は .bak-2, .bak-3 ... と一意名に退避するよう変更
- migrate: filesystem の退避/削除が成功してから registry.add で
  plugins.yml を repos/ path に書き換えるよう順序を入れ替え。失敗時に
  registry だけ先行更新され retry も効かなくなる partial state を防止
- _cleanup_plugins_dir: .bak-N 形式も保全 .bak として検知するよう調整
- 上記挙動を網羅するテスト 6 件追加

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cross-review round 2 の指摘対応。

- _dirs_differ: upstream 専用追加 (clone にのみ存在) を差分扱いしないよう
  変更。コピー側にのみ存在するエントリ・共通エントリの型/target/内容差分の
  みを preserved 判定に使い、通常の upstream 更新で不要な .bak 退避が発生
  しないようにした (codex#91 / gemini#91 重複指摘)。
- _files_equal: read_bytes() の全読み込みを 64KB チャンクのストリーム比較に
  置き換え、巨大ファイルでのメモリ枯渇リスクを排除 (gemini#105)。
- _ensure_repo_cloned: 前回 clone 失敗で残った partial dir (.git 無し /
  registry.yml 不正) を検知して削除・再 clone するよう修正。無限に
  parse 失敗を繰り返す経路を解消 (codex#132)。
- _cleanup_plugins_dir: .gitkeep でも .bak でもない想定外エントリが残る場合
  は cleaned=True と報告せず False を返すよう修正 (gemini#176)。
- docs: devbase plugin migrate の CLI リファレンスを追加 (codex review body)。

テスト 4 件追加 (upstream 専用追加は差分なし / 同サイズ内容差 / partial
clone 再 clone / cleanup の想定外エントリ保持)。全 252 件 pass。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cross-review round 3 の指摘に対応:

- _cleanup_plugins_dir の `.bak` 判定を `'.bak' in name` から
  `<name>.bak[-N]` 末尾一致 (_is_bak_name) に修正。my.bakery 等の誤マッチを排除
- migrate ループ内の per-plugin `registry.add` を loop 末尾の単一
  `registry.add_many` に集約し plugins.yml の保存頻発を解消
  (各 plugin の fs 移動と entry 構築は同一 try 内のため失敗時の retry 性は維持)
- _ensure_repo_cloned で local_path 設定済みでも .git/registry.yml を
  検証 (_clone_is_healthy)。壊れた既存 dir は除去して再 clone
- _files_equal で S_IMODE を比較。旧コピーの実行ビット変更を差分扱いし保全
- _auto_migrate の preserved/skipped 再通知を loud な per-plugin WARNING から
  簡潔な INFO ヒント 1 行に抑制 (詳細は devbase plugin migrate 側で出力)

migrator テスト 12 件追加 (.bak 末尾判定 / clone 健全性 / 実行ビット差分 /
batched save / broken local_path 再 clone / 警告抑制)。全 264 件 pass。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…PR2 round4)

- registry.add_many: 引数内で名前が重複する場合 last-wins で一意化してから
  反映し、plugins.yml に矛盾エントリが残らないようにした
- _files_equal: 全権限ビット比較を exec ビット (+x) 限定に変更し、umask /
  group 設定差による誤った .bak 退避を防止
- _ensure_repo_cloned: local_path 記録済みだが unhealthy な既存 dir でも
  .git があれば未コミット/未 push のローカル変更を失わないよう rmtree せず
  PluginError を送出し、.git 欠落 (真に壊れている) 場合のみ再 clone する

test: add_many 重複排除 / exec ビット限定 / .git 付き clone 保全の 6 件を追加

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…検知 (PR2 round5)

cross-review round 5 で指摘された 4 件に対応:

- derived clone 経路 (.git 保護): repo.local_path 未設定でも repos/<derived>
  に .git 付き既存 clone があり registry.yml だけ欠ける場合、無条件 rmtree で
  未コミット/未 push のローカル変更を失っていた。_reclaim_or_protect_existing
  を新設し local_path 経路と同じく .git 有りは削除せず PluginError で復旧案内
  する (freshly clone した分のみ破棄)。
- registry 先行永続化: 旧 plugins/ コピーの削除/.bak 退避 → add_many の順序を
  逆転し、検証済み path rewrite を破壊的 fs 操作の前に 1 回保存する二相構成へ。
  保存失敗時はコピー無傷で abort (次回 retry 可能)、phase2 の retire 失敗は
  registry が既に有効な repos/ clone を指すため lingering copy として
  _cleanup_plugins_dir が surface する (silent data loss を排除)。
- clone_dir がファイル/symlink で squat: clone_dir.is_dir() のみでは git_clone
  が失敗するため、ファイル/symlink は unlink して再 clone (git tree を持たない
  ため損失なし)。
- _entry_kind == 'other' (socket/pipe/device): 内容比較できず identical を
  証明できないため diverged 扱いとし .bak 保全に倒す。

migrator テスト 5 件追加 (derived .git 保護 / clone_dir ファイル squat /
registry 保存失敗でコピー無傷 / retire は保存後 / fifo は差分扱い)。
全 275 件 pass。ruff (E9,F63,F7,F82) / compileall pass。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
round 5 で derived 経路 (local_path 未設定) の `.git` 付き既存 dir を
無条件に保護 (PluginError) していたが過剰だった。`repos/<derived>` に
.git + registry.yml が両方そろった健全 clone が残っている場合は
PluginError で migration を skip せず、そのまま reuse して local_path を
永続化するよう修正。

- 健全 clone (.git + registry.yml) → reuse + local_path 永続化
- .git ありだが unhealthy (registry.yml 欠落) → 従来どおり保護 (PluginError)
- .git 無し / file・symlink squat → reclaim して再 clone

local_path 経路と derived 経路で挙動を揃え、fresh clone 後と healthy reuse
後の local_path 永続化を `_persist_repo_local_path` に共通化した。

健全 derived clone が reuse され migration が skip されないことを検証する
テスト `test_derived_path_with_healthy_clone_is_reused` を追加。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_ensure_repo_cloned が clone のたびに add_repository で plugins.yml を
保存していたため、多数リポジトリ移行時に保存回数が repo 数に比例していた。
clone 済み repo 行を pending_repos に貯め、path rewrite と合わせて Phase2
(破壊的 cleanup) の直前に save_migration で 1 回だけ保存するよう変更。

二相アトミシティは維持: 旧 copy 削除より前に registry が必ず flush 済みで
あること (clone を指す local_path / plugin path の両方) を不変条件として
保持。save_migration は repos + plugins を単一 load+save で upsert する。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor Author

@takemi-ohama takemi-ohama left a comment

Choose a reason for hiding this comment

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

🤖 cross-review | round 1 | codex | APPROVE

修正提案はありません。

Copy link
Copy Markdown
Contributor Author

@takemi-ohama takemi-ohama left a comment

Choose a reason for hiding this comment

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

🤖 cross-review | round 1 | gemini | REQUEST_CHANGES

バッチ処理による保存の最適化と、.git ディレクトリの保護ロジックの導入により、移行処理の効率と安全性が向上しています。一方で、内部的なパース処理の重複や、リポジトリの重複排除ロジックに改善の余地があります。また、既存コードに含まれる非英語のメッセージについても、将来的な統一を推奨します。

"""
if not repos:
return
unique = list({r.name: r for r in repos}.values())
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[minor / 正確性] リポジトリの重複排除を name のみで行うと URL が異なる同名リポジトリを正しく扱えません。URL をキーにした重複排除を検討してください。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ご指摘ありがとうございます。本件は意図的な設計のため、今回は修正を見送ります (rejected)。

plugins.yml のレジストリは name を一意キーとする upsert モデルで全体が統一されています:

  • get_repository(name) / remove_repository(name) / _apply_repositories いずれも name キー
  • _apply_plugins も同じく name キーで last-wins upsert

_apply_repositories の name ベース重複排除はこの一貫したキー設計に沿ったもので、"同名・別 URL" は別レコードではなく 同一リポジトリの URL 更新 (upsert / last-wins) として扱うのが正しい挙動です。URL をキーにすると同名で URL の異なる 2 行が plugins.yml に並ぶことになり、name 一意を前提とする get_repository(name) 等の参照系と矛盾します (URL 引きが必要な箇所には別途 get_repository_by_url が用意済みです)。

したがって "正確性" 上の欠陥ではなく、キー設計の選択という設計判断であり、現状の name 一意設計を維持します。URL キーへの移行が必要であればレジストリ全体のモデル変更として別 issue で扱うのが適切と考えます。本スレッドは Resolve せず残します。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ご指摘ありがとうございます。本件は意図的な設計のため、今回は修正を見送ります (rejected)。

plugins.yml のレジストリは name を一意キーとする upsert モデルで全体が統一されています:

  • get_repository(name) / remove_repository(name) / _apply_repositories いずれも name キー
  • _apply_plugins も同じく name キーで last-wins upsert

_apply_repositories の name ベース重複排除はこの一貫したキー設計に沿ったもので、"同名・別 URL" は別レコードではなく 同一リポジトリの URL 更新 (upsert / last-wins) として扱うのが正しい挙動です。URL をキーにすると同名で URL の異なる 2 行が plugins.yml に並ぶことになり、name 一意を前提とする get_repository(name) 等の参照系と矛盾します (URL 引きが必要な箇所には別途 get_repository_by_url が用意済みです)。

したがって "正確性" 上の欠陥ではなく、キー設計の選択という設計判断であり、現状の name 一意設計を維持します。URL キーへの移行が必要であればレジストリ全体のモデル変更として別 issue で扱うのが適切と考えます。本スレッドは Resolve せず残します。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ご指摘ありがとうございます。本件は意図的な設計のため今回は修正を見送ります (rejected)。

plugins.yml のレジストリは name を一意キーとする upsert モデルで全体が統一されています (get_repository(name) / remove_repository(name) / _apply_repositories / _apply_plugins いずれも name キーで last-wins)。_apply_repositories の name ベース重複排除はこの一貫したキー設計に沿ったもので、同名・別 URL は別レコードではなく同一リポジトリの URL 更新 (upsert / last-wins) として扱うのが正しい挙動です。URL をキーにすると同名で URL の異なる 2 行が plugins.yml に並び、name 一意を前提とする get_repository(name) 等の参照系と矛盾します (URL 引きには別途 get_repository_by_url が用意済み)。したがって正確性の欠陥ではなくキー設計の選択という設計判断であり、現状の name 一意設計を維持します。URL キー化が必要であればレジストリ全体のモデル変更として別 issue 扱いが適切と考えます。本スレッドは Resolve せず残します。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ご指摘ありがとうございます。本件は意図的な設計のため今回は修正を見送ります (rejected)。plugins.yml のレジストリは name を一意キーとする upsert モデルで全体が統一されています (get_repository(name) / remove_repository(name) / _apply_repositories / _apply_plugins いずれも name キーで last-wins)。_apply_repositories の name ベース重複排除はこの一貫したキー設計に沿ったもので、同名・別 URL は別レコードではなく同一リポジトリの URL 更新 (upsert / last-wins) として扱うのが正しい挙動です。URL をキーにすると同名で URL の異なる 2 行が plugins.yml に並び、name 一意を前提とする get_repository(name) 等の参照系と矛盾します (URL 引きには別途 get_repository_by_url が用意済み)。したがって正確性の欠陥ではなくキー設計の選択という設計判断であり、現状の name 一意設計を維持します。本スレッドは Resolve せず残します。

Comment thread lib/devbase/plugin/migrator.py Outdated
gemini review (migrator.py:428 [minor/performance]) 対応。

_ensure_repo_cloned が clone/reuse 時に parse_registry_yml した RegistryInfo
を戻り値で返すようにし、migrate ループ側で再パースしていた重複を解消した。
さらに _build_persisted_repo もパース済み reg_info を受け取る形に変更し、
fresh-clone 経路での二重パース (helper 内 + ループ) も排除。結果として
registry.yml の読み込みはリポジトリあたり最大 1 回 (local_path fast path は
lazy fallback) に削減。未使用になった _build_persisted_repo の registry 引数も
除去。挙動・テスト (plugin 203 / migrator 60) は不変。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@takemi-ohama
Copy link
Copy Markdown
Contributor Author

takemi-ohama commented May 28, 2026

/ndf:fix 対応サマリ (PR2 round 8 / commit 0f4a440)

前ラウンドの cross-review (codex APPROVE 0 件 / gemini REQUEST_CHANGES 2 件) を独自再判定の上で対応しました。

対応件数

区分 件数
修正 (fixed) 1
rejected 1
deferred 0

重要度別 (独自再判定後)

critical major minor nit
0 0 1 (fixed) 1 (rejected)

修正 (fixed)

  • [minor / performance] lib/devbase/plugin/migrator.py:428 — migrate ループ内の registry.yml 重複パースを解消。_ensure_repo_cloned が clone / 健全 clone reuse 時に取得した RegistryInfo を戻り値で返し、ループ側はそれを再利用 (local_path fast path のみ lazy fallback)。あわせて _build_persisted_repo もパース済み reg_info を受け取る形にし、fresh-clone 経路の二重パースも排除。registry.yml 読み込みをリポジトリあたり最大 1 回に削減。未使用化した registry 引数も除去。 -> Resolved

rejected (修正せず)

  • [minor / 正確性] lib/devbase/plugin/registry.py:100 _apply_repositories の name ベース重複排除 -> URL キー化提案。plugins.yml レジストリは name 一意キーの upsert モデルで全体統一されており (get_repository(name) / remove_repository(name) / _apply_plugins も name キー)、"同名・別 URL" は同一リポジトリの URL 更新 (last-wins upsert) として扱うのが正しい設計です。URL キー化は name 一意前提の参照系と矛盾するため、正確性の欠陥ではなく設計判断と判断し見送りました (URL 引きには別途 get_repository_by_url あり)。Resolve せずスレッドに理由を返信済み。

品質チェック (uv 環境)

  • pytest tests/plugin/ -q: 110 passed
  • pytest tests/plugin/test_migrator.py -q: 59 passed
  • ruff check --select E9,F63,F7,F82 lib/ (uvx): All checks passed
  • ruff check --select F lib/devbase/plugin/migrator.py (uvx): All checks passed
  • compileall lib/: OK

CI

  • base = release/PLAN04 のため ci.yml (pull_request branches:[main]) は trigger されず CI=NONE (正常 / gh pr checks 31 = "no checks reported")。

(補足: 自動化の手違いで上の inline 返信が重複投稿されています。内容は同一です。)

再レビューをお願いします。

Copy link
Copy Markdown
Contributor Author

@takemi-ohama takemi-ohama left a comment

Choose a reason for hiding this comment

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

🤖 cross-review | round 2 | codex | COMMENT

修正提案はインライン 1 件です。

Comment thread lib/devbase/cli.py
Copy link
Copy Markdown
Contributor Author

@takemi-ohama takemi-ohama left a comment

Choose a reason for hiding this comment

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

🤖 cross-review | round 2 | gemini | REQUEST_CHANGES

今回の変更(PLAN04: Persistent Clones への移行)は、プラグイン管理の堅牢性と効率を大幅に向上させるもので、全体的に非常に良く設計されています。特に、既存の plugins/ から repos/ への 2 段階の移行プロセスや、ファイル単位での差異検知による保全ロジックは秀逸です。

一方で、パフォーマンス面と不完全な状態からの復旧性において、以下の通り修正案があります。

Comment thread lib/devbase/plugin/migrator.py Outdated
Comment thread lib/devbase/plugin/migrator.py
takemi-ohama and others added 2 commits May 28, 2026 16:27
…象に追加 (PR2 round9)

- migrate ループ内の registry.get_repository_by_url (毎回 plugins.yml を再読込) を
  ループ前の URL→repo 辞書索引 1 回に置換し、O(N) ディスク I/O を O(1) に集約
- SUBCMD_MAP[('plugin','pl')] に 'migrate' を追加し、devbase plugin mi / pl mi の
  prefix 解決が効くよう修正 (従来は argparse エラー)
- 再読込が plugin 数に比例しないことを検証する回帰テストを追加

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…帰テストを追加 (PR2 round9)

round9 で migrate ループ内の get_repository_by_url (毎回 plugins.yml 再読込) を
ループ前の URL→repo 辞書索引 1 回に置換した変更の回帰防止。_load 呼び出し回数を
計数し plugin 数より少ないことを検証する。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@takemi-ohama
Copy link
Copy Markdown
Contributor Author

fix | round 2 対応サマリ

cross-review round 2 の指摘 3 件に対応しました。

修正コミット: source 5f17a65 + test 3dded92 (0f4a440..3dded92)

対応件数 (独自再判定後)

重要度 件数 内容
major 2 1 件修正 / 1 件 rejected (安全設計)
minor 1 修正

修正 (resolved: 2 件)

  • [major/性能] migrator.py:430 — migrate ループ内の get_repository_by_url (毎回 plugins.yml を再読込) を、ループ前の URL→repo 辞書索引 1 回に置換。O(N) ディスク I/O → O(1)。回帰テスト test_repo_lookup_does_not_reload_plugins_yml_per_plugin 追加。
  • [minor/CLI] cli.py:237 — SUBCMD_MAP[('plugin','pl')] に 'migrate' を追加。devbase plugin mi / pl mi の prefix 解決を有効化 (従来は argparse エラー)。

rejected (Resolve せず: 1 件)

  • [major/正確性] migrator.py:210 — _reclaim_or_protect_existing で dirty でない .git ツリーを自動削除する案。git status 判定は未 push のローカルコミットを検知できず、自動削除すると黙って破棄し得るため、一律保護 (回復可能エラー) を維持。破壊的再取得は repo remove --force の明示 opt-in に限定するプロジェクト方針に沿う。詳細は当該スレッド参照。

deferred

なし

品質チェック

  • pytest tests/plugin/: 111 passed (新規テスト +1)
  • ruff (E9,F63,F7,F82): All checks passed
  • compileall lib/: OK

CI

base = release/PLAN04 のため GitHub Actions の対象外 (no checks reported = NONE)。

@takemi-ohama takemi-ohama merged commit 5a6158a into release/PLAN04 May 28, 2026
takemi-ohama added a commit that referenced this pull request May 28, 2026
* chore: PLAN04 release branch 作成

* feat(plugin): repos/ 永続クローンによるプラグイン管理と projects/ 直接シンボリックリンク (#29)

* chore: PLAN04-repos-core Draft PR 作成

* feat(plugin): repos/ 永続クローン + 直接リンク install (PLAN04-repos-core)

plugins/ 中間層を廃止し、repos/ に git clone を永続保持して
projects/ からシンボリックリンクで直接参照する構造に変更。

- models.py: RegisteredRepository に local_path フィールド追加
- registry.py: get_repos_dir() 追加
- repo_manager.py: repos/ 永続クローン、git pull refresh、dirty check 付き remove
- installer.py: repos/ ベースのシンボリックリンク install、repos/ 保護 uninstall、
  copy_plugin / _sync_dir 等のコピー系ロジック削除
- syncer.py: InstalledPlugin.path ベース走査、同名衝突時の .<owner> suffix リンク
- updater.py: git pull ベース update
- cli.py: repo remove に --force オプション追加
- .gitignore: repos/ 追加

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(plugin): サブディレクトリ配置・dirty検知・suffix衝突の3件修正

- installer.py / updater.py: rel_path を plugin名ではなく
  plugin_path.relative_to(devbase_root) で算出し、
  registry.yml の path が name と異なるサブディレクトリ配置に対応
- repo_manager.py: upstream未設定時に @{u}..HEAD が失敗して
  dirty=false となりデータ損失の恐れがあった問題を修正。
  upstream未設定時は dirty 扱いにして安全側に倒す
- syncer.py: collision suffix を owner のみ → owner--repo に変更し、
  同一 owner の複数 repo で同名 project が衝突する問題を修正。
  既存 symlink 存在チェックも追加

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(plugin): pull前スナップショット・repo全体更新・clone失敗クリーンアップ

- updater: git pull 前に旧 plugin の projects をスナップショットし、
  pull 後の migration で旧ディレクトリが消えても移行先を検出可能に
- updater: name 指定の update でも同一 repo の全 installed plugin の
  metadata (version/path) を pull 後に再読み込みして整合性を維持
- repo_manager: repo add で clone 後の registry.yml parse や名前衝突
  失敗時に clone_dir を自動削除し、リトライ時の詰まりを防止
- syncer: path.split('/') を Path().parts に変更 (OS 非依存化)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(plugin): round 3 レビュー指摘対応 — 直接 install の自動 repo 登録 + cleanup

- installer.py: user/repo:plugin-name 形式で未登録リポジトリを指定した際に
  自動で repo add を実行し、既存の直接指定形式を維持 (codex round 3 major)
- updater.py: _update_repo_plugins の未使用引数 repo_local_path を削除 (gemini round 3 minor)
- repo_manager.py: git_clone を try/except ブロック内に移動し、
  部分 clone 失敗時もディレクトリを自動クリーンアップ (gemini round 3 minor)

全 210 テスト PASSED

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(plugin): round 4 レビュー指摘対応 — @ref 拒否・refresh メタデータ同期・pull エラー改善

- installer.py: 未登録リポジトリの自動登録時に @ref を明示的に拒否
  (永続 clone はデフォルトブランチを追跡するため、pinned ref と矛盾する)
- repo_manager.py: refresh_repository で git pull 後に installed plugin の
  metadata (version/path) を再計算し sync_projects() を実行
- repo_manager.py: _git_pull で upstream tracking branch の有無を事前検査し、
  未設定時に具体的な修正手順を含むエラーメッセージを返す

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(plugin): round 5 レビュー指摘対応 — NameError修正・エラーメッセージ改善・レガシーrepo移行・batch refresh効率化

- installer.py:131 — @ref 拒否時の未定義変数 `url` を `repo_url` に修正 (NameError 解消)
- repo_manager.py:133 — _git_pull の upstream 未設定エラーで detached HEAD/remote未設定を個別判定、remote名を動的取得
- repo_manager.py:379 — refresh_repository に sync パラメータ追加、batch refresh 時は最後に1回だけ sync_projects 実行
- installer.py:223 — legacy repo (local_path 未設定) の自動移行: 初回 install 時に永続 clone を作成して local_path を設定
- テスト追加: @ref 拒否の PluginError テスト、legacy repo migration テスト (計 212 tests PASSED)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(plugin): レガシー移行時の整合性修正 + UX改善 (round 6 review)

- installer.py: レガシーrepo移行時に parse_registry_yml で検証してから
  plugins.yml へ保存するように変更。plugins リストも registry.yml から
  最新情報を取得して更新 (major x2 対応)
- repo_manager.py: 複数リモート時に origin を優先選択 (minor)
- repo_manager.py: detached HEAD エラーに具体的な復帰コマンド例を追加 (minor)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(plugin): print→logger 統一 + SSH/HTTPS URL 重複登録検知 (deferred nit)

- installer.py: _install_from_repo 内の print() を logger.info() に統一
- repo_manager.py: add_repository で SSH/HTTPS 形式の URL バリアント重複を
  _url_to_repos_dirname 正規化により検知し、RepositoryError で拒否

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(plugin): round 1 レビュー指摘対応 — @ref拒否・host付きdirname・refresh snapshot・print→logger・migration テスト

- 登録済みrepoへの @ref 指定を PluginError で拒否(codex + gemini 指摘)
- _url_to_repos_dirname に host を含め、異なるホストの同名 repo の衝突を防止
- refresh_repository で git pull 前に _snapshot_plugin_projects を取得し
  _update_repo_plugins に渡すことで、pull 後のディレクトリ変更時も移行可能に
- repo_manager.py の残存 print() を logger.info() に統一
- _migrate_removed_plugin / _snapshot_plugin_projects のテスト追加(3件)
- refresh の pre_pull_projects 受け渡し検証テスト追加

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(plugin): 旧 plugins/ コピーインストールを repos/ 永続クローンへ移行する devbase plugin migrate を追加 (#31)

* chore: PLAN04-migration Draft PR 作成

* feat(plugin): 既存 plugins/ コピーインストールを repos/ 永続クローンへ移行

PLAN04 PR2。PR1 (#29) で repos/ 永続クローン方式に切り替えたが、PR1 以前に
plugins/<name>/ へファイルコピーされた既存インストールは移行されないため、その
移行ロジックを追加する。

- migrator.py (新規):
  - needs_migration / _is_legacy_plugin: legacy plugins/ インストールの検出
    (linked は --link 専用として除外)
  - _dirs_differ: コピーとクローンの差分検出 (内容変更・追加ファイルを保守的に差分扱い)
  - migrate: 未クローン repo の永続クローン作成、InstalledPlugin.path の repos/ 書き換え、
    差分なしは plugins/<name> 削除・差分ありは <name>.bak 保全、sync_projects 再実行、
    --link/.bak/skip が無ければ plugins/ を .gitkeep のみに正規化
- plugin migrate サブコマンド (cli.py / commands/plugin.py)
- install/update 初回実行時に _auto_migrate で自動移行

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(plugin): migration の symlink 差分検知漏れ / .bak 上書き / registry 先行更新を修正

cross-review round 1 の major 指摘 4 件 (codex 2 / gemini 2) に対応。

- _dirs_differ: regular file のみ比較していたため legacy copy のみに存在する
  symlink / 空ディレクトリ / 型不一致を差分として検知できず、後続の
  shutil.rmtree で silently 削除される恐れがあった。全エントリを対象に
  型 + 内容 (file は byte, symlink は target) を比較するよう厳密化
- _unique_bak_path: 既存の <name>.bak を無条件に rmtree していたため、
  前回 migration で保全した未整理バックアップが消失する恐れがあった。
  存在時は .bak-2, .bak-3 ... と一意名に退避するよう変更
- migrate: filesystem の退避/削除が成功してから registry.add で
  plugins.yml を repos/ path に書き換えるよう順序を入れ替え。失敗時に
  registry だけ先行更新され retry も効かなくなる partial state を防止
- _cleanup_plugins_dir: .bak-N 形式も保全 .bak として検知するよう調整
- 上記挙動を網羅するテスト 6 件追加

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(plugin): migration の差分判定厳密化 / partial clone 復旧 / cleanup 報告修正

cross-review round 2 の指摘対応。

- _dirs_differ: upstream 専用追加 (clone にのみ存在) を差分扱いしないよう
  変更。コピー側にのみ存在するエントリ・共通エントリの型/target/内容差分の
  みを preserved 判定に使い、通常の upstream 更新で不要な .bak 退避が発生
  しないようにした (codex#91 / gemini#91 重複指摘)。
- _files_equal: read_bytes() の全読み込みを 64KB チャンクのストリーム比較に
  置き換え、巨大ファイルでのメモリ枯渇リスクを排除 (gemini#105)。
- _ensure_repo_cloned: 前回 clone 失敗で残った partial dir (.git 無し /
  registry.yml 不正) を検知して削除・再 clone するよう修正。無限に
  parse 失敗を繰り返す経路を解消 (codex#132)。
- _cleanup_plugins_dir: .gitkeep でも .bak でもない想定外エントリが残る場合
  は cleaned=True と報告せず False を返すよう修正 (gemini#176)。
- docs: devbase plugin migrate の CLI リファレンスを追加 (codex review body)。

テスト 4 件追加 (upstream 専用追加は差分なし / 同サイズ内容差 / partial
clone 再 clone / cleanup の想定外エントリ保持)。全 252 件 pass。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(plugin): migration の保全判定・clone 健全性・registry 保存効率を改善 (PR2 round3)

cross-review round 3 の指摘に対応:

- _cleanup_plugins_dir の `.bak` 判定を `'.bak' in name` から
  `<name>.bak[-N]` 末尾一致 (_is_bak_name) に修正。my.bakery 等の誤マッチを排除
- migrate ループ内の per-plugin `registry.add` を loop 末尾の単一
  `registry.add_many` に集約し plugins.yml の保存頻発を解消
  (各 plugin の fs 移動と entry 構築は同一 try 内のため失敗時の retry 性は維持)
- _ensure_repo_cloned で local_path 設定済みでも .git/registry.yml を
  検証 (_clone_is_healthy)。壊れた既存 dir は除去して再 clone
- _files_equal で S_IMODE を比較。旧コピーの実行ビット変更を差分扱いし保全
- _auto_migrate の preserved/skipped 再通知を loud な per-plugin WARNING から
  簡潔な INFO ヒント 1 行に抑制 (詳細は devbase plugin migrate 側で出力)

migrator テスト 12 件追加 (.bak 末尾判定 / clone 健全性 / 実行ビット差分 /
batched save / broken local_path 再 clone / 警告抑制)。全 264 件 pass。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(plugin): migration の registry 重複排除 / exec ビット限定比較 / 健全 clone 保全 (PR2 round4)

- registry.add_many: 引数内で名前が重複する場合 last-wins で一意化してから
  反映し、plugins.yml に矛盾エントリが残らないようにした
- _files_equal: 全権限ビット比較を exec ビット (+x) 限定に変更し、umask /
  group 設定差による誤った .bak 退避を防止
- _ensure_repo_cloned: local_path 記録済みだが unhealthy な既存 dir でも
  .git があれば未コミット/未 push のローカル変更を失わないよう rmtree せず
  PluginError を送出し、.git 欠落 (真に壊れている) 場合のみ再 clone する

test: add_many 重複排除 / exec ビット限定 / .git 付き clone 保全の 6 件を追加

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(plugin): migration の derived clone 保全 / registry 先行永続化 / 特殊ファイル差分検知 (PR2 round5)

cross-review round 5 で指摘された 4 件に対応:

- derived clone 経路 (.git 保護): repo.local_path 未設定でも repos/<derived>
  に .git 付き既存 clone があり registry.yml だけ欠ける場合、無条件 rmtree で
  未コミット/未 push のローカル変更を失っていた。_reclaim_or_protect_existing
  を新設し local_path 経路と同じく .git 有りは削除せず PluginError で復旧案内
  する (freshly clone した分のみ破棄)。
- registry 先行永続化: 旧 plugins/ コピーの削除/.bak 退避 → add_many の順序を
  逆転し、検証済み path rewrite を破壊的 fs 操作の前に 1 回保存する二相構成へ。
  保存失敗時はコピー無傷で abort (次回 retry 可能)、phase2 の retire 失敗は
  registry が既に有効な repos/ clone を指すため lingering copy として
  _cleanup_plugins_dir が surface する (silent data loss を排除)。
- clone_dir がファイル/symlink で squat: clone_dir.is_dir() のみでは git_clone
  が失敗するため、ファイル/symlink は unlink して再 clone (git tree を持たない
  ため損失なし)。
- _entry_kind == 'other' (socket/pipe/device): 内容比較できず identical を
  証明できないため diverged 扱いとし .bak 保全に倒す。

migrator テスト 5 件追加 (derived .git 保護 / clone_dir ファイル squat /
registry 保存失敗でコピー無傷 / retire は保存後 / fifo は差分扱い)。
全 275 件 pass。ruff (E9,F63,F7,F82) / compileall pass。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(plugin): derived clone 経路で健全 clone を reuse する (round 6)

round 5 で derived 経路 (local_path 未設定) の `.git` 付き既存 dir を
無条件に保護 (PluginError) していたが過剰だった。`repos/<derived>` に
.git + registry.yml が両方そろった健全 clone が残っている場合は
PluginError で migration を skip せず、そのまま reuse して local_path を
永続化するよう修正。

- 健全 clone (.git + registry.yml) → reuse + local_path 永続化
- .git ありだが unhealthy (registry.yml 欠落) → 従来どおり保護 (PluginError)
- .git 無し / file・symlink squat → reclaim して再 clone

local_path 経路と derived 経路で挙動を揃え、fresh clone 後と healthy reuse
後の local_path 永続化を `_persist_repo_local_path` に共通化した。

健全 derived clone が reuse され migration が skip されないことを検証する
テスト `test_derived_path_with_healthy_clone_is_reused` を追加。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf(plugin): migration の clone 永続化を単一保存に集約 (round 7)

_ensure_repo_cloned が clone のたびに add_repository で plugins.yml を
保存していたため、多数リポジトリ移行時に保存回数が repo 数に比例していた。
clone 済み repo 行を pending_repos に貯め、path rewrite と合わせて Phase2
(破壊的 cleanup) の直前に save_migration で 1 回だけ保存するよう変更。

二相アトミシティは維持: 旧 copy 削除より前に registry が必ず flush 済みで
あること (clone を指す local_path / plugin path の両方) を不変条件として
保持。save_migration は repos + plugins を単一 load+save で upsert する。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf(plugin): migration の registry.yml パースをリポジトリあたり 1 回に集約 (PR2 round8)

gemini review (migrator.py:428 [minor/performance]) 対応。

_ensure_repo_cloned が clone/reuse 時に parse_registry_yml した RegistryInfo
を戻り値で返すようにし、migrate ループ側で再パースしていた重複を解消した。
さらに _build_persisted_repo もパース済み reg_info を受け取る形に変更し、
fresh-clone 経路での二重パース (helper 内 + ループ) も排除。結果として
registry.yml の読み込みはリポジトリあたり最大 1 回 (local_path fast path は
lazy fallback) に削減。未使用になった _build_persisted_repo の registry 引数も
除去。挙動・テスト (plugin 203 / migrator 60) は不変。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf(plugin): migration の repo 解決をループ前に 1 回へ集約 + migrate を prefix 解決対象に追加 (PR2 round9)

- migrate ループ内の registry.get_repository_by_url (毎回 plugins.yml を再読込) を
  ループ前の URL→repo 辞書索引 1 回に置換し、O(N) ディスク I/O を O(1) に集約
- SUBCMD_MAP[('plugin','pl')] に 'migrate' を追加し、devbase plugin mi / pl mi の
  prefix 解決が効くよう修正 (従来は argparse エラー)
- 再読込が plugin 数に比例しないことを検証する回帰テストを追加

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(plugin): migration の repo 解決が plugin 数に比例して plugins.yml を再読込しない回帰テストを追加 (PR2 round9)

round9 で migrate ループ内の get_repository_by_url (毎回 plugins.yml 再読込) を
ループ前の URL→repo 辞書索引 1 回に置換した変更の回帰防止。_load 呼び出し回数を
計数し plugin 数より少ないことを検証する。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* fix(plugin): repo refresh の壊れた移行を成功扱いにせず移行時の registry.yml 重複パースを抑制

- refresh_repository: _update_repo_plugins が repo_errors を返した場合は
  warning で握りつぶさず RepositoryError として伝播。pull 後に削除された
  プラグインの移行失敗を成功扱いにしない (major)
- migrate: 同一リポジトリの複数プラグイン移行時、local_path fast path で
  返る registry.yml の遅延パースを URL 単位でキャッシュしリポジトリあたり
  1 回に抑制 (minor / performance)
- 両挙動の回帰テストを追加

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugin): update 時に registry.yml の指す plugin directory 存在を検証する

_update_repo_plugins が registry.yml の path をそのまま plugins.yml に書き
込むため、path が実在しないディレクトリを指していても repos/.../missing で
成功扱いになっていた。_register_repo_plugin と同様に plugin_path.is_dir() を
検証し、存在しない場合は registry を更新せず errors に積むよう修正。

回帰テスト test_update_errors_when_registry_path_missing を追加。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(status): repos/ ベースプラグインの project_count を plugin.path 基準で算出する

devbase status の _get_plugin_info が project_count を旧レイアウト
plugins/<name>/projects から数えていたため、PLAN04 で repos/<repo>/<subdir>
へ移行したプラグインの project_count が常に 0 表示されていた。
registry.devbase_root / plugin.path / projects を基準に数えるよう変更し、
plugin.py の表示ロジックと整合させた(repos/ と --link 両方を解決)。

回帰テスト test_status_project_count.py を追加(repos ベース / --link /
projects 無しの 3 ケース)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 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>

* perf(plugin): migrate ループでクローン済み repo と registry.yml パースを同一リポジトリのプラグイン間で再利用

更新後の repo を repos_by_url に書き戻し、後続プラグインが local_path
fast path を通るようにして clone-reuse 分岐の再入 (registry.yml 再パース +
pending_repos 重複登録) を回避。clone/reuse パスの reg_info も
reg_info_by_url にキャッシュし、同一リポジトリの複数プラグイン移行時の
registry.yml パースを 1 回に抑制。

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant