git の submodule、便利ですが安定してない(変更の多い)リポジトリに使うと 地獄です。
Submodule Hellです。
やばいです。
開発で変更が入りまくるリポジトリをうっかりsubmoduleにしちゃった!
かなり開発して入り組んできたけどどうしよう…。
#なぜsubmoduleが地獄になるのか?
- 更新が多いリポジトリはコミットが2箇所になって地獄
- しかも変更がsha-1形式でしか分からなくて地獄
- submoduleのクラス名・メソッド名等をリネームすると、親側もリネームが必要になって無駄に2回のコミットをしなくちゃいけなくて地獄。
- さらに、親のコミットはコードの修正&submoduleの変更の取り込みも必要
- submoduleの参照先同士が衝突した時、何が悪いのか探すのが大変すぎて地獄
- いともたやすくsubmodule側が detached HEAD になって自分のコミットを見失うので地獄(reflogから復活とかある)
- PullRequestの時に、何個も送らなきゃ行けなくて地獄
- PullRequestをレビューする方も、見る箇所が別ページに行くの必須で地獄
- ブランチの数も x submodule数で増加するので管理コスト増加で地獄
- この悩みわかってない人に、「え、だって機能毎に分割してるんでしょ?いいじゃん」とか言われたりして地獄
####サブモジュールが素晴らしく見える理由
- 分割統治
- 複数の箇所で利用可能
- 意味的に別リポジトリがスッキリする
まぁ、基本的に完全に安定しているライブラリをリンクして使う…みたいな使用法以外では使わないほうが良いと思います。
できれば月1回以下の更新が望ましい…。週1は辛い(フレームワークのBeta版とかだとよくある)
毎日submoduleを更新しなきゃいけない状況をsubmodule地獄と呼びます。(命名:俺。"Submodule Hell"で検索すると結構ありますが。)
基本的に submodule地獄になるような状況を最初から回避する ことが一番大切です。
いまsubmodule地獄にいるんですけど!?
地獄に入ってしまったらしょうがない。
脱出しましょう。
脱出法1:submoduleを親リポジトリに統合する方法
ようするに、親リポジトリとsubmoduleリポジトリに分かれているから良くない。
ひとつに統合しましょう。(参考 )
submoduleを親リポジトリに統合する
git rm --cached testSubModule
git rm .gitmodules
rm -rf testSubModule/.git
git add testSubModule
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Your branch is up-to-date with 'origin/master'.
#
# Changes to be committed:
# deleted: .gitmodules
# deleted: testSubModule
# new file: testSubModule/README.md
# new file: testSubModule/...submoduleのファイルがたくさん...
# new file: testSubModule/...
# new file: testSubModule/...
# new file: testSubModule/...
##もう一度submoduleに分割する方法
安定版になった!submoduleに分割したい!という時には以下の方法で。
親リポジトリから削除します。
git rm -r --cached testSubModule
新しいリポジトリとして構築し直します。
cd testSubModule
git init
git add .
git commit -m "change to submodule"
これで新しいリポジトリを作成しました。
さらにremoteとして、元々使ってたsubmoduleのパスを追加します。
git remote add origin リモートリポジトリのパス(git@github.com:Xxx/testSubModule.git等)
pullし、おそらく衝突するので、強制的にローカルの内容で上書きし、コミットします。
git pull origin master
git checkout --ours .
git commit -a
最期にpushしてやれば終了です。
git push origin master
submoduleとして追加し直す場合は、 git rm testSubModule
の後に親の階層に戻り、 git submodule add リモートリポジトリのパス
とかすれば良いと思います。
さて、この方法ですが察しの良い方はお気づきと思いますが、この方法だと、当然ですが統合・再分割の過程において 履歴は消えます 。
「履歴もsubmoduleのほうに持って行きたい!><」とお考えの方には、残念ですが、現状だとそういう方法は 無いです… …あるけど、危ういです。
#脱出法2:履歴を持ったまま統合・そして分割する
git --filter-branch
という最強のオプションを使います。
歴史を改ざんします。よく分かってないと怖いですね。
履歴を持ったまま、親リポジトリに統合する方法
こちら を参考にさせていただきました。
git filter-branch --index-filter \
'git ls-files -s | sed "s@[[:cntrl:]]\"*@&testSubModule/@" |
GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
git update-index --index-info &&
mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE || true' HEAD
mac以外の環境では[[:cntrl:]]
部は、単に /t
に置き換えてください。
作業ブランチを作成。
git checkout -b __work_for_import__
親ブランチに移動し、取り込む。
作業パス(cdの移動先)は環境に合わせて適切に。
cd ..
git remote add importSubmodule testSubModule/
リモート(いままでsubmoduleだったところ)から取得。
履歴を含んで testSubModule/ ディレクトリ以下に、submoduleのファイルが移ってきました。
git pull importSubmodule __work_for_import__
##履歴を持ったまま、submoduleに分割する方法
上記の逆です。
git filter-branch
を使って、 testSubModule/
ディレクトリ以外の変更を「なかったこと」にします。
git filter-branch --subdirectory-filter testSubModule/ HEAD
submoduleだったリポジトリにPUSHして終了。
(ただ、こういう時は新しくリポジトリを作ったほうが絶対安全です)
git push importSubmodule __work_for_import__
親リポジトリがすべてなかったことになってしまったので、修復をかけます。
git reset --hard HEAD^
あとは通常にsubmoduleを取り込んでください。
また、この処理はリポジトリを汚しまくる&処理時間がかなり掛かると思うので、注意して行ってください。
#まとめ
切出したり戻したりするの大変だから、安定版まで親リポジトリと一緒に育てて
submoduleが安定した時にディレクトリごと切り出せ
地獄から脱出する前に、地獄に来ないことをオススメします。
おわり
あ、あと git subtree
はこの問題についてある程度の解決策になります。
ただし、更新頻度が高くsubmodule間の依存性が高いような場合は、やはり親リポジトリに統合した方がよいでしょう。