gitで外部moduleを扱う方法(subtree)


概要

例えば、webページを作っている「project A」「project B」があるとします。

全く別のプロジェクトですがCSSだけは同じものを使いたい。

どんどんアップデートしてCSSを充実させたい。

そして新しく「project C」を作るときもこのCSSを使いたい。

これを叶える仕組みがgitには標準で備わっています。

「subtree」という仕組みです。

上記の図の様にgitで管理している「project A」の中でまた別のgitリポジトリ「CSS」も管理しています。

「CSS」に機能を追加してCSSリポジトリにPUSHしたら「project B」でも機能追加したCSSを使うことができます。
もちろん逆も可能です。

参考サイト

git subtreeの練習 - Weblog - Hail2u.net

git subtree使ってみた - Qiita

上記のサイトにも書いてあるんですが、gitのバージョンによって色んな方法があるみたいです。

git1.7の途中でsubtreeが導入されたようなので、バージョン1.8以降なら今回の方法でいけると思います(未検証)
git subtree -hとコマンド打ってみてヘルプが表示されたら使えます。

ちなみに下記のサイトはちょっと情報が古いようなので注意。

Git - サブツリーマージ

How to use the subtree merge strategy

subtreeの使い方

それでは実際にやってみましょう。
例として、プロジェクトにCSSのリポジトリを取り込む方法を書いていきます。

前準備

現在のプロジェクトにサブディレクトリとして外部リポジトリを取り込みます。

前準備として外部リポジトリをリモートリポジトリとして登録します。

git remote add <name> <url>

<name>の部分には適当な名前を入れます。仮に「cssrepo」とします。

<url>はCSSリポジトリのURLです。

subtree add

このコマンドでプロジェクトに外部リポジトリをサブディレクトリとして取り込むことができます。

shell
git subtree add   --prefix=<prefix> --squash <repository> <commit>

<prefix>は取り込むディレクトリの名前を指定します。
新しく作りますので既存のディレクトリ名と被らないようにします。

例えば「css」というディレクトリ名を付ける場合は[css]と入力します。

[modules]ディレクトリの中の[css]ディレクトリにしたいのなら[modules/css]とします。
ここで[css/]と最後にスラッシュを入れたら後々うまく行かなくなるので注意してください。

<repository>は前準備で登録したリポジトリ名です。

この場合は[cssrepo]となります。

<commit>の部分は、ヘルプではcommitとなっているんですがブランチ名でいいようです。
今回は[master]とします。

--squashオプションを付けると履歴を継承しないで取り込めます。

example

shell
% git subtree add --prefix=modules/css --squash cssrepo master
git fetch cssrepo master
From /Users/hoge/git/tests/css
 * branch            master     -> FETCH_HEAD
Added dir 'modules/css'

subtree push

次はプロジェクト内に取り込んだcssを更新してpushしてみましょう。

pushする前にはcommitが必要ですが、これは普通にやればOKです。

git commit -m 'h1タグのcolorがgoldになるように更新' -a

以下のようにsubtreeを付けると指定したディレクトリだけpushできます。

shell
git subtree push  --prefix=<prefix> --squash <repository> <refspec...>

<prefix>は取り込むディレクトリの名前を指定します。

addと同じです。最後にスラッシュを入れないように注意。

<repository>外部リポジトリ名です。この場合は[cssrepo]となります。

<refspec...>[ローカルのブランチ名 : リモートのリポジトリ名]です。

コロン以降を省略するとローカル、リモートともに同じブランチを指定したことになります。

参考

今さら聞けないgit pushコマンド - shoma2da's diary

example

shell
% git subtree push --prefix=modules/css --squash cssrepo master
git push using:  cssrepo master
-n 1/      22 (0)
-n 2/      22 (1)
-n 3/      22 (2)
-n 4/      22 (3)
-n 5/      22 (4)
-n 6/      22 (5)
-n 7/      22 (6)
-n 8/      22 (7)
-n 9/      22 (8)
-n 10/      22 (9)
-n 11/      22 (10)
-n 12/      22 (11)
-n 13/      22 (12)
-n 14/      22 (13)
-n 15/      22 (14)
-n 16/      22 (15)
-n 17/      22 (16)
-n 18/      22 (17)
-n 19/      22 (18)
-n 20/      22 (19)
-n 21/      22 (19)
-n 22/      22 (20)
Counting objects: 5, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 351 bytes | 0 bytes/s, done.
Total 3 (delta 2), reused 0 (delta 0)
To /Users/hoge/git/tests/css.git
   ca32985..c54cf15  c54cf15de7a448205e4fe4e6991db8ef12270c18 -> master

remoteで[git log]すると更新されてるのが確認できます。

subtree pull

今度は逆に外部で更新されたリポジトリをpullしましょう。

git subtree pull  --prefix=<prefix> --squash <repository> <refspec...>

<prefix>は取り込むディレクトリの名前を指定します。

add、pushと同じです。最後にスラッシュを入れないように注意。

<repository>外部リポジトリ名です。この場合は[cssrepo]となります。

<refspec...>[ローカルのブランチ名 : リモートのリポジトリ名]です。

コロン以降を省略するとローカル、リモートともに同じブランチを指定したことになります。

example

shell
% git subtree pull --prefix=modules/css --squash cssrepo master
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From /Users/hoge/git/tests/css
 * branch            master     -> FETCH_HEAD
Auto-merging modules/css/oreore.css
CONFLICT (content): Merge conflict in modules/css/oreore.css
Automatic merge failed; fix conflicts and then commit the result.

ローカルのsubtreeにあるファイルとリモートの同ファイルがコンフリクトしています。

実はここがハマりどころでした。

[git subtree]で検索してヒットしたサイトを見ていると、どこもコンフリクトなんか発生していません。

しかしこれ絶対コンフリクトするんじゃないでしょうか?
普通のpullと違って、subtree pullは内部的にブランチをmergeするのと同じような処理をしているんじゃないでしょうか。

なので素直にコンフリクトを解消して次に行きましょう。

コンフリクトした時はp4merge

とは言え、コンフリクトすると「git怖い!」ってなりますよね。

コンフリクトはgitを使う上で超えなくてはならない壁です。

そして案外、壁は低いのかもしれません。

コンフリクトしたときに[git mergetool]とコマンドを打つとマージツールというのが起動します。
これでコンフリクトを解消すればいいんですが、デフォルトの設定では何がなんだかわからないかもしれません。
そこで[p4merge]というGUIアプリケーションを導入すると簡単にmergetoolを使うことができます。

興味のある方は以下のサイトを参考にしてみてください。

Gitのdiffツールとマージツールを設定する(p4merge)Mac - 成らぬは人の為さぬなりけり

Gitを使った分散開発管理16 – p4mergeでマージを行う | Developers.IO

Gitのマージツールにp4mergeを使ってみた

コンフリクトを解消して、% git commit -aしたら完了です。

git aliasを追加

そんなわけで

git subtree add --prefix=<prefix> --squash <repository> <refspec>

git subtree push --prefix=<prefix> --squash <repository> <refspec>

git subtree pull --prefix=<prefix> --squash <repository> <refspec>

と、この3つのコマンドを覚えたらsubtreeはほぼ使えます。

でも…コマンド長くね?めんどくさくね?

なのでエイリアスを設定しましょう。

例えば[git log]と打つのがめんどくさいなら下記のコマンドをシェルにコピペして実行してください。
git config --global alias.l log
これでエイリアスが設定されて[git l]と入力することで[git log]の代わりになります。

今度は設定ファイルを直接編集してみましょう。ホームディレクトリに
[.gitconfig]というファイルがあるので開いてみてください。

~/.gitconfig
[alias]
    l = log

さっき登録したエイリアスが入ってると思います。

[l]は[log]と解釈してよね、という設定です。

さて、subtreeを簡単に使うためのエイリアスを3行ほど追加してみます。

.gitconfig
[alias]
    l = log
    subadd = "!f () { git subtree add --prefix=${1}  --squash ${2}  ${3} ;};f"
    subpush = "!f () { git subtree push --prefix=${1}  --squash ${2}  ${3} ;};f"
    subpull = "!f () { git subtree pull --prefix=${1}  --squash ${2}  ${3} ;};f"

こうなりました

git subtree add --prefix=<prefix> --squash <repository> <refspec>
↓
git subadd <prefix> <repository> <refspec>

git subtree push --prefix=<prefix> --squash <repository> <refspec>
↓
git subpush <prefix> <repository> <refspec>

git subtree pull --prefix=<prefix> --squash <repository> <refspec>
↓
git subpull <prefix> <repository> <refspec>

これでもう長いコマンド打たなくてすみます。

疑問

git subtree -hと入力するとsubtreeのHELPが見られます。

% git subtree -h
usage: git subtree add   --prefix=<prefix> <commit>
   or: git subtree add   --prefix=<prefix> <repository> <ref>
   or: git subtree merge --prefix=<prefix> <commit>
   or: git subtree pull  --prefix=<prefix> <repository> <ref>
   or: git subtree push  --prefix=<prefix> <repository> <ref>
   or: git subtree split --prefix=<prefix> <commit...>

    -h, --help            show the help
    -q                    quiet
    -d                    show debug messages
    -P, --prefix ...      the name of the subdir to split out
    -m, --message ...     use the given message as the commit message for the merge commit

options for 'split'
    --annotate ...        add a prefix to commit message of new commits
    -b, --branch ...      create a new branch from the split subtree
    --ignore-joins        ignore prior --rejoin commits
    --onto ...            try connecting new tree to an existing one
    --rejoin              merge the new branch back into HEAD

options for 'add', 'merge', 'pull' and 'push'
    --squash              merge subtree changes as a single commit

で、疑問なんですけど。git subtree splitってどう使うのでしょうか?

あと、git subtree pullした時にコンフリクトしない方法って存在するんでしょうか?

まとめ

# 取り込みたい外部モジュールのリポジトリをリモートに登録
git remote add <name> <url>

# 外部モジュールをプロジェクトに取り込む
git subtree add --prefix=<prefix> --squash <repository> <refspec>

# 外部リポジトリにpush(事前にadd,commitが必要)
git subtree push --prefix=<prefix> --squash <repository> <refspec>

# 外部リポジトリの更新をpull
git subtree pull --prefix=<prefix> --squash <repository> <refspec>
git mergetool
git commit -m 'hoge' -a

エイリアスの設定

.gitconfig
[alias]
    subadd = "!f () { git subtree add --prefix=${1}  --squash ${2}  ${3} ;};f"
    subpush = "!f () { git subtree push --prefix=${1}  --squash ${2}  ${3} ;};f"
    subpull = "!f () { git subtree pull --prefix=${1}  --squash ${2}  ${3} ;};f"

エイリアス設定後のコマンド

git subadd <prefix> <repository> <refspec>

git subpush <prefix> <repository> <refspec>

git subpull <prefix> <repository> <refspec>

おまけ

subtree pullするときにマージコミットが記録されるのが気になる人は
--no-commitを付けると省略されます。