「2つのgitリポジトリがあって、その片方をもう一方に取り込みたい」という状況を考えます。依存ライブラリのソースを自分のプロジェクトで保持したい、といった状況が典型的でしょう。
この場合、通常は git submodule
を使うと思います。 git submodule
であれば、他のプロジェクトを履歴ごと自分のソースの一部として管理できて、かつ双方の履歴をきれいに分離できます。
ただ、双方の履歴が分離できるということは、双方の履歴を混ぜられないということでもあります。そのため、 git submodule
は、他のプロジェクトのソースに自プロジェクト独自の変更を加えて管理するといった用途には向かないように思います。ではどうすればいいだろうか、という試行錯誤の記録です。
- git submodule で取り込む
- 「Aのcloneにおけるsubmoruleへの変更をAのリモートのみに push 」はできない
- 「B由来のソースをAの内部で独自に管理する」とBの上流の変更が取り込めない
- git subtree で履歴を完全に混ぜる
- 謝辞
文章で書くだけでは状況がよく見えないので、本稿では主に図で状況を示します。 なお、2つのリポジトリにおけるコミットの状況を図示していきますが、実際の作業はこれらをローカルへcloneして行うことになるので、適宜読み替えてください。
git submodule
で取り込む
いま、自プロジェクトをgitリポジトリAで管理しており、そこにgitリポジトリBで管理されている他プロジェクトを取り込みたいとします。ここで、セオリー通りに git submodule
を使い、AにBを取り込むとします。
$ git clone [AのURL] $ git submodule add [BのURL] [取り込み先のディレクトリ]
このときの状況を図示するとこんな感じです。
以降、図中の色と記号はこう読んでください。
「Aのcloneにおけるsubmoruleへの変更をAのリモートのみに push
」はできない
ここで、submoduleとしたディレクトリ内で何かを変更してコミットし、その変更をリモートで共有したいとします。
submoduleへの変更を含めてpushする必要があるので、そのディレクトリ内で git push
するか、 git push --recurse-submodules=on-demand
を実行することになります。
このとき、このpushはAのみならずBにも適用されます。これをAのみに適用する方法はなさそうです。
この挙動は、「双方の履歴が独立している」というsubmoduleの意図には合致していますが、Bに伝播させたくないA独自の変更をB由来のファイルに施したい場合には困ります。 そうしたケースに対する対策としては以下のような方法が考えられそうです。
- Bで「A専用」のブランチを切ってそれをsubmoduleで取り込む
- Bをフォークして「A専用B」のリポジトリを作る
ただ、Aがプライべートだったり、専用にカスタマイズしたBを使いたいAのようなプロジェクトのgitリポジトリが増えたりすると、これらの対策ではあまりうれしくありません。
「B由来のソースをAの内部で独自に管理する」とBの上流の変更が取り込めない
では、AではBの履歴管理とは完全に独立してB由来のソースを使うのはどうでしょうか? たとえば、先ほどの状況でBへのpushが起こってほしくないので、submoduleについては「ローカルにcloneしたリポジトリ」での履歴管理だけで我慢する、という手はありそうです。
しかし、これだと、当然のことながら「submoduleの内容に依存するAの変更」をリモートで共有できません。 また、下図のように、Bの変更をあとで取り込むこともできません。
git subtree
で履歴を完全に混ぜる
結局、やりたかったことに一番近い解決策は、「A内でのB由来のファイルに対する履歴をAの履歴と完全に混ぜる」になりそうです。 それを実現する方法はいくつか考えられます。
git subtree
を使う- Bを取り込む専用のブランチを作り、その変更を主たるブランチに随時マージして使う(その際、混乱を避けるために
git worktree
を使うとよさそう)
やりたいことを図にするとこうなります。
ここでは、上記を git subtree
で実現する手順をメモしておきます。 git subtree
については下記の記事を参考にしました。
まず、Aをcloneしたディレクトリ内で、Bをリモートとして追加します。
そのBを git subtree
で指定のディレクトリにとってくる、というのが基本的な流れです。
gitコマンドとしては以下の2つを入力すればOK。
$ git remote add -f [B用のリモートの名前] [BのURL] $ git subtree add --prefix [Bを取り込むディレクトリ] [B用のリモートの名前] [Bのブランチ] --squash # あとはふつうにgit pushすればAのリモートのみに変更が反映される
もし仮に、ここで「Bにも変更を戻したい」場合には、 git subtree push
というコマンドを使って「そのための差分」を作り、それをGitHubのPull Requestなどで上流に送ればいいようです。
しかし、Aの履歴が混ざっているので、うまくやらないとうまくいかなそう(やってないのでわかりません)。
一方、Bの上流の変更をA内のB由来のファイルに適用するのは簡単です。簡単といっても、もちろん衝突は覚悟しないといけませんが…。
$ git fetch [B用のリモートの名前] [Bのブランチ] $ git subtree pull --prefix [Bを取り込んだディレクトリ] [B用のリモートの名前] [Bのブランチ] --squash
git worktree
を使った方法もそのうち試してみようと思います。
謝辞
この状況を考えることになったきっかけは、下記のなにげないツイートでした。
gitで管理しているプロジェクトAの内部で、やはりgitで管理されている他のプロジェクトBを利用したい場合、いつもはsubmoduleにしているのだけど、これだと利用されるほうのプロジェクトBをA専用に改造してその履歴をgitで管理することはできない(と理解している)。どうすればいいんだろう。
— keiichiro shikano λ♪ (@golden_lucky) May 3, 2020
このツイートにコメントをいただいた @anohana さん、@ymotongpoo さん(と某チャットで付き合っていただいたみなさん)、@soranoba さん、@YukiharuYABUKI さん、ありがとうございます。おかげで自分が本当は何をしたかったのか整理できました。とくに @soranoba さんには git subtree
の存在を教えていただき本当に助かりました。