feat(plugin): 旧 plugins/ コピーインストールを repos/ 永続クローンへ移行する devbase plugin migrate を追加#31
Conversation
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>
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | codex | APPROVE
修正提案はありません。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | gemini | REQUEST_CHANGES
バッチ処理による保存の最適化と、.git ディレクトリの保護ロジックの導入により、移行処理の効率と安全性が向上しています。一方で、内部的なパース処理の重複や、リポジトリの重複排除ロジックに改善の余地があります。また、既存コードに含まれる非英語のメッセージについても、将来的な統一を推奨します。
| """ | ||
| if not repos: | ||
| return | ||
| unique = list({r.name: r for r in repos}.values()) |
There was a problem hiding this comment.
[minor / 正確性] リポジトリの重複排除を name のみで行うと URL が異なる同名リポジトリを正しく扱えません。URL をキーにした重複排除を検討してください。
There was a problem hiding this comment.
ご指摘ありがとうございます。本件は意図的な設計のため、今回は修正を見送ります (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 せず残します。
There was a problem hiding this comment.
ご指摘ありがとうございます。本件は意図的な設計のため、今回は修正を見送ります (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 せず残します。
There was a problem hiding this comment.
ご指摘ありがとうございます。本件は意図的な設計のため今回は修正を見送ります (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 せず残します。
There was a problem hiding this comment.
ご指摘ありがとうございます。本件は意図的な設計のため今回は修正を見送ります (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 せず残します。
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>
/ndf:fix 対応サマリ (PR2 round 8 / commit 0f4a440)前ラウンドの cross-review (codex APPROVE 0 件 / gemini REQUEST_CHANGES 2 件) を独自再判定の上で対応しました。 対応件数
重要度別 (独自再判定後)
修正 (fixed)
rejected (修正せず)
品質チェック (uv 環境)
CI
(補足: 自動化の手違いで上の inline 返信が重複投稿されています。内容は同一です。) 再レビューをお願いします。 |
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | codex | COMMENT
修正提案はインライン 1 件です。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | gemini | REQUEST_CHANGES
今回の変更(PLAN04: Persistent Clones への移行)は、プラグイン管理の堅牢性と効率を大幅に向上させるもので、全体的に非常に良く設計されています。特に、既存の plugins/ から repos/ への 2 段階の移行プロセスや、ファイル単位での差異検知による保全ロジックは秀逸です。
一方で、パフォーマンス面と不完全な状態からの復旧性において、以下の通り修正案があります。
…象に追加 (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>
fix | round 2 対応サマリcross-review round 2 の指摘 3 件に対応しました。 修正コミット: source 5f17a65 + test 3dded92 (0f4a440..3dded92) 対応件数 (独自再判定後)
修正 (resolved: 2 件)
rejected (Resolve せず: 1 件)
deferredなし 品質チェック
CIbase = release/PLAN04 のため GitHub Actions の対象外 (no checks reported = NONE)。 |
* 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>
何のために
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… と退避し上書きしない)InstalledPlugin.pathをrepos/配下へ書き換え、sync_projectsでprojects/シンボリックリンクを張り直すplugins/を走査し、--linkインストール・スキップされた旧コピー・保全された.bakのいずれも残っていなければ.gitkeepのみ残して掃除する一貫性・堅牢性
PluginRegistry.save_migration) で永続化してから、旧コピーの削除/退避を行う。save が失敗しても旧コピーは無傷で残り、再実行でクリーンにリトライできる.gitを持つディレクトリ (uncommitted/unpushed のローカル変更を含み得る) は削除せず、エラーで手動対応を促す。partial clone (.gitやregistry.ymlを欠く壊れたクローン) は再クローンで自己修復するコマンド連携
devbase plugin migrateサブコマンド追加 (lib/devbase/cli.py,lib/devbase/commands/plugin.py)install/update実行時に旧形式が残っていれば自動でマイグレーションを起動 (_auto_migrate)。手動実行は通常不要。preserved/skipped は毎回繰り返さないよう、簡潔なヒント 1 行に集約するdocs/user/cli-reference.mdにdevbase plugin migrateの説明と挙動表を追記Test plan
tests/plugin/test_migrator.py(約 1054 行 / 23 クラス・59 ケース) で網羅:repos/ベース・--linkは対象外projects/シンボリックリンク生成 +plugins/を.gitkeepへ掃除plugins/<name>.bakとして保全、既存.bakは上書きせず番号付き退避.git欠落) の再クローン、.gitを持つクローンの保護 (削除拒否)devbase plugin migrateコマンド実行・対象なし時の no-op