git subtreeはsubmoduleと違って利用側のリポジトリにコミット履歴ごと取り込むのでどこまで反映されたのかや、subtreeと利用リポジトリ側の修正が同時にコミット出来るのでとても便利な機能です。
しかし、subtree側のモジュールも発展途上で利用側もモジュール側も頻繁に更新されるような場合、巷で一般的で紹介されているようなsubtree pull/pushによる更新の反映を行っても頻繁に競合して運用は難しいのではと悩みました。そこで運用方法を見直しsubtree split/mergeを活用した方法だと安全に運用できそうなので紹介します。
##オススメの運用方針
###git subtree push/pull は使用禁止にする
subtree利用側とsubtree側のリモートリポジトリ両方でもし同時に開発が進行していたのなら当然master等の同一ブランチに並行してコミットが入ることは制御出来ないためnon-forwardでrejectされて同期が取りにくかったり競合が発生しやすくなります。subtree利用側とされる側のリポジトリ間での直接の同期操作となるsubtree push/pullは避けるのが安全です。
###subtree側のブランチも全てローカルブランチで明確に管理する
各リポジトリへの反映はsubtree側のブランチをローカルブランチとして切り出した上で通常のリポジトリと同じようにgit push/pullを利用します。subtree間マージは必ずsubtree split/mergeを利用します。
これによってsubtreeを利用したとしてもローカルブランチ上でsubtree側を含む全リポジトリのコミット履歴が独立して表現されるので混乱を避けることが出来ます。
##コマンド例
subtree利用側がrepository_a、subtreeとしたいモジュールがmodule_aとします。まずモジュールmodule_aをリモートに追加して取り込んでおきます
$ git clone https://github.com/toshi3221/repository_a.git
$ cd repository_a
(master) $ git remote add module_a https://github.com/toshi3221/module_a.git
(master) $ git fetch module_a
新規にsubtreeを取り込む(subtree add)
まだsubtreeが構築されておらず、新規にmodule_aをModuleAフォルダのsubtreeとして取り込むとします。
(master) $ git branch subtree/module_a/master module_a/master
(master) $ git subtree add --prefix ModuleA subtree/module_a/master
これでrepository_aのmasterブランチにsubtreeのmodule_aがModuleAフォルダで追加されています。ローカルブランチ名は、subtreeであること、モジュール、ブランチが明確にわかる様に名称をつけておきます。
既存subtreeの切り出し(subtree split)
既にsubtreeが構築されているところから。repository_aがmodule_aを既にModuleAフォルダにsubtree管理しており、再度subtreeとしてローカルブランチsubtree/module_a/masterに切り出すとします。
(master) $ git subtree split --prefix ModuleA --rejoin -b subtree/module_a/master
Merge made by the 'ours' strategy.
Created branch 'subtree/module_a/master'
c560ffb84c195941f21ec63646f0f018fa1fad22
(master) $ git push origin master
(master) $ git log
commit e616e3b53393b555faa9fbf14ba480461606bf5e
Merge: 12b7840 c560ffb
Author: toshi3221 <toshi3221@dummy.com>
Date: Fri Sep 16 18:07:17 2016 +0900
Split 'ModuleA/' into commit 'c560ffb84c195941f21ec63646f0f018fa1fad22'
git-subtree-dir: ModuleA
git-subtree-mainline: 12b7840928c00afbd4d068a66932221c8535f87b
git-subtree-split: c560ffb84c195941f21ec63646f0f018fa1fad22
これでsubtree/module_a/masterブランチはmodule_aリポジトリのmasterブランチのみのコミット履歴に切り出しました。
--rejoinオプションはmasterブランチに現在のコミットまでsubtreeとしてmodule_aを切り出した旨のコミットが付加されます。subtreeをリポジトリに反映する場合は過去のコミット履歴を辿って必要なコミットをマージしますが、rejoinを行っておくとこの時点まで反映済と分かるのでこの時点まで履歴を辿る必要が無くなりsubtreeコマンドが高速化されるので、新しくcloneする場合やある程度コミット履歴がたまってきたら再度rejoinすることをオススメします。
###subtree側の更新を反映する(subtree merge)
(master) $ git checkout subtree/module_a/master
(subtree/module_a/master) $ git pull module_a master:subtree/module_a/master
Already up-to-date.
repository_aがmodule_aの更新反映を怠っていなければ反映済みとなるはずです。もし、module_aのmasterブランチが更新されていた場合はrepository_a側に更新を反映した方が良いでしょう:
(subtree/module_a/master) $ git checkout master
(master) $ git subtree merge --prefix ModuleA subtree/module_a/master
module_aの方の修正によってrepository_aが影響ないか確認し必要があれば修正を加えて反映しておきます
(master) $ git push origin master
###subtreeに加えた修正をトピックブランチでPull Requestする(subtree split)
次にmodule_aにも影響するようなrepository_aのコミットを行うとします。通常の開発であればトピックブランチを作成すると思いますのでmasterから枝を切ったtopicブランチでコミットしたとして、それをmodule_aにも反映します
(topic) $ git push origin topic
To https://github.com/toshi3221/repository_a.git
* [new branch] topic -> topic
(topic) $ git subtree split --prefix ModuleA --onto subtree/module_a/master -b subtree/module_a/topic
(topic) $ git checkout subtree/module_a/topic
(subtree/module_a/topic) $ git push module_a subtree/module_a/topic:topic
To https://github.com/toshi3221/module_a.git
* [new branch] subtree/module_a/topic -> topic
repository_aリポジトリ側でsubtreeに反映した修正をsubtree側に反映する場合はsubtree splitを利用して再度topicブランチを切り出します。subtree側にブランチを切り替えるとmodule_aのみのフォルダ構造になりsubtree mergeは出来ません。
--ontoオプションが無いと、既にsubtree splitで切りだし済みのsubtreeブランチとのコミット履歴の共通化が行われない場合があります。その場合は枝元のsubtree/module_a/masterを付加します。必ずontoオプションを付加しても全てのコミット履歴を読み直すのでrejoinの恩恵は受けられませんが特に問題ではありません。
module_a側にコミットを一度も反映していなかった場合等にrepository_a側で過去行ってきたコミット履歴がsubtree splitの時に全て記録されます。subtree側がオーナーであれば良いのですがOSS等の場合は相手側のコミット履歴を汚すことになりかねませんのでその場合は、merge --squashを使用してください。
これでtopicブランチをpul-reqする準備が整いました。あとはrepository_aやmodule_aがオーナーは誰なのかとか自分はどういう立場なのかによってどこでpul-reqを発行するのかしないのか等が変わってきますがそこはケースバイケースで対応してください。
ここではどちらのリポジトリも自分がオーナーであり、repository_a側のpul-reqで関連メンバーにチェックして貰い、topicブランチは既にリモートから削除されおり、module_aは手動でマージして良いよとした例とします
(master) $ git pull origin master --prune
(master) $ git branch -d topic
(master) $ git checkout subtree/module_a/master
(subtree/module_a/master) $ git pull module_a master:subtree/module_a/master
(subtree/module_a/master) $ git merge subtree/module_a/topic
(subtree/module_a/master) $ git push module_a subtree/module_a/master:master
(subtree/module_a/master) $ git branch -d subtree/module_a/topic
(subtree/module_a/master) $ git push module_a :topic
以上でsubtreeでも単独リポジトリとほぼ同等の運用が可能になります。