Skip to content

fix(plugin): copy_plugin で rmtree+copytree を rsync 同期に置換#17

Merged
takemi-ohama merged 3 commits into
mainfrom
fix/plugin-update-preserve-inode
May 22, 2026
Merged

fix(plugin): copy_plugin で rmtree+copytree を rsync 同期に置換#17
takemi-ohama merged 3 commits into
mainfrom
fix/plugin-update-preserve-inode

Conversation

@takemi-ohama
Copy link
Copy Markdown
Contributor

@takemi-ohama takemi-ohama commented May 21, 2026

Summary

devbase plugin update の 2 つの問題を修正:

  1. inode 置換による CWD 喪失: ユーザが projects/<proj> シンボリックリンク経由でプラグイン配下に cd 済みの状態で update を実行すると、copy_plugin()rmtree+copytree がディレクトリ inode を置き換え、シェルが宙ぶらりんになる
  2. ユーザ編集ファイルの黙示的消失: ユーザが compose.ymlenv を編集していたり、.env を作っていたりすると、update でこれらが全部消える / 上書きされる

修正内容

copy_plugin の update 経路を、shutil.rmtree(dest) + shutil.copytree(src, dest) から 保守的な in-place 同期 (_sync_dir) に置き換え。

同期セマンティクス (1 ファイル単位)

src dst content action report
存在 無し - コピー added
存在 存在 同じ no-op (none)
存在 存在 違う dst 維持、src を <name>.new に並置 kept_local
無し 存在 - dst 残す preserved_orphans

例外: プラグインルート直下の plugin.yml はプラグインメタデータ (registry の version / description ソース) であり、ユーザ編集対象ではないため 常時 upstream で上書きする (_ALWAYS_OVERWRITE_AT_ROOT)。

これにより:

  • ユーザの CWD inode が消えない (プラグインディレクトリと両方に存在するサブディレクトリの inode 維持)
  • ユーザが編集した compose.yml 等は 消えない — upstream 版は compose.yml.new で並置され、手で diff / merge できる
  • ユーザが独自に置いた .env, ログファイル等は 削除されない
  • plugin.yml (メタデータ) は 常に upstream に同期 — registry の version 表示が古くなる経路を塞ぐ
  • 内容が変わってなければ .new も作らない (ノイズなし)
  • 既存 .new は次回 sync で最新 upstream に再生成

copy_plugin の挙動変化

Updating 'carmo-system' from ...
[INFO]  Updating existing plugin 'carmo-system'
[WARN]    2 local edit(s) kept; upstream saved as .new alongside:
[WARN]      - projects/demo/compose.yml (upstream: compose.yml.new)
[WARN]      - env (upstream: env.new)
[INFO]    3 local-only file(s) preserved (not in upstream)
[INFO]  Installed plugin 'carmo-system' (v0.2.0)

再現エラー (修正前)

$ cd projects/carmo-system-console        # symlink → plugins/carmo-system/projects/...
$ ll
... README.md compose.yml env ...
$ devbase plugin update
... Updating 'carmo-system' ...
$ ll
total 0
drwxr-xr-x@ 2 ... ./
drwxr-xr-x@ 2 ... ../

→ シェルが古い inode を握ったまま、ファイルが消えたように見える。

検証

8 シナリオを 1 つのテストで検証 (PR ローカル):

シナリオ 期待 結果
plugin.yml (upstream 変更) upstream で上書き (常時)
compose.yml (ユーザ編集) ユーザ版維持 + .new
env (ユーザ編集) ユーザ版維持 + .new
README.md (内容同じ) no-op、.new 無し
CHANGELOG.md (upstream 新規) 追加
.env (ユーザ作成、upstream 無し) 保持 (orphan)
projects/demo/app.log (ユーザ作成) 保持 (orphan)
projects/demo/index.html (ユーザ編集) ユーザ版維持 + .new
プラグイン dir inode 保持
サブディレクトリ inode 保持

Test plan

  • 8 シナリオ自動テスト (PR description 参照)
  • inode 保持テスト
  • 実環境: ユーザ編集 + プラグイン配下 cd 状態 + devbase plugin update を実機で確認
  • 実環境: <file>.new の存在をユーザが気付き、diff / merge できることを確認
  • 実環境: plugin.yml がユーザ編集されていても update で upstream 版に戻ることを確認

やらないこと

  • 「壊滅的に綺麗にしたい」場合の --clean フラグは別 PR で検討

関連

takemi-ohama and others added 3 commits May 22, 2026 07:58
ユーザが `projects/<name>` シンボリックリンク経由で `plugins/<plugin>/projects/<name>/`
内部に `cd` した状態で `devbase plugin update` を実行すると、`copy_plugin()` の
`shutil.rmtree(dest); shutil.copytree(plugin_path, dest)` がプラグインディレクトリの
inode ごと置換し、ユーザのシェル CWD が "宙ぶらりんの旧 inode" を掴んだままになって
ファイルが消えたように見える問題があった。

`_sync_dir(src, dst)` を新設し、rmtree せずに既存ディレクトリの inode を保ったまま
ファイル差分を反映する (rsync 風)。差分は:

- src に存在しない dst エントリは削除 (orphan)
- src の symlink は再生成 (target が変わっていれば追従)
- src の dir は再帰的に同期 (両方に存在する dir は inode 維持)
- src の file は shutil.copy2 で上書き

これにより、ユーザの CWD が更新対象プラグイン配下にあっても、シェルは引き続き
同じ inode を参照し続け、`ll` で正しく中身を表示できる。

linked プラグインで dest が symlink の場合は従来どおり unlink + copytree。
新規インストールも従来どおり copytree。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
前コミットの rsync 同期は inode は保つが、ユーザがプラグイン配下で編集
したファイル (`compose.yml`, `env` 等) や独自に置いたファイル (`.env`,
ログ等) をそのまま上書き / 削除していた。これは旧 rmtree+copytree と
同等の data-loss セマンティクスで、UX 改善としては不十分。

挙動を以下の保守的なものに置き換える:

| src | dst | content | action |
|---|---|---|---|
| 存在 | 無し | - | コピー (added) |
| 存在 | 存在 | 同じ | no-op |
| 存在 | 存在 | 違う | dst 維持、src を `<name>.new` に並置 (kept_local) |
| 無し | 存在 | - | dst 残す (preserved_orphans) |

これにより:

- `.env` 等 upstream に無いファイルは保持
- ユーザが手で書き換えた `compose.yml` も保持、upstream 版は
  `compose.yml.new` として手元で diff/merge できる
- 内容が変わっていなければ `.new` も生成されない (ノイズなし)
- 既存の `.new` (前回の sync で残った) は次回 sync 時に再生成 (常に
  最新の upstream を反映)
- ファイル/dir/symlink の type mismatch は upstream 側を `.new` 経由で
  並置 (ユーザ側を壊さない)

`copy_plugin()` は同期後に `kept_local` / `preserved_orphans` 件数を
ログ出力し、ユーザに `.new` の存在と "merge の余地" を伝える。

NOTE: `plugin.yml` もユーザ編集として扱われるため、ユーザが意図せず
触っていた場合 registry に古い version が記録され得る。実害は表示が
古くなるだけで、`.new` が並置されるので気付ける。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`_sync_dir` のユーザ編集保護セマンティクスにより、ユーザが
意図せず `plugin.yml` を編集していた場合に registry の version /
description 表示が upstream と desync する可能性があった。

`plugin.yml` はプラグインのメタデータでありユーザ編集対象では
ないため、プラグインルート直下の `plugin.yml` のみ常時 upstream
で上書きする例外を `_ALWAYS_OVERWRITE_AT_ROOT` で表現する。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@takemi-ohama takemi-ohama merged commit e0408b0 into main May 22, 2026
5 checks passed
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