読者です 読者をやめる 読者になる 読者になる

tanaka51のブログ

http://blog.tanaka51.jp に移転しました

Git のマージについて、自分的まとめ

チーム開発で Git を使ってから半年ちょい位経ちました。
Git 玄人な人たちに囲まれて開発していたおかげで、そこそこ Git 力がついてきました。
そんな中で、ブランチの統合(マージ)についての考え方が大分固まって来たのでまとめます。

まずは結論から。

  • 統合するブランチ → 統合されるブランチ : 統合の為に使うコマンド
    • ローカル(自分用) → ローカル(自分用) : 適当に
    • ローカル(自分用) → ローカル(リモート用) : merge --squash
    • リモート → ローカル(リモート用) : pull --rebase
    • ローカル(リモート用) → ローカル(自分用) : rebase
    • ローカル(リモート用) → ローカル(リモート用) : merge --no-ff

ローカル(リモート用)は、リモートドラッキングブランチからチェックアウトしたブランチを指してます。
要は、git checkout -b topic origin/topic みたいな感じで作成したブランチ。
ローカル(自分用)は、リモートに push する事の無い、完全にローカルなブランチです。
リモートは、リモートにあるブランチです。

ではその理由をしこしこと並べていきたいと思います。
玄人の方達には当たり前の事かもしれません。
そして、割と長いです。
(マージと統合が入り乱れてますが、同じ意味で使ってます。なおすの面倒なのでこのままで…)

前提

中央リポジトリには、リリース用ブランチ、開発用ブランチがあります。
普段は開発用ブランチにコミットし、レビューが終了したらリリース用ブランチにマージされます。

また、他にも以下のような考えのもとに上記結論に至っています。

Git のコミュニティ

Git の世界では、「コミットの歴史を綺麗に保つ」事が非常に重視されています。
(そんな風に僕は感じています)
これは、開発者にとって非常にメリットのあることなので、僕もそうするように努力をします。
ブランチの統合で使うコマンドをわざわざ使い分けるのも、その為です。

チーム開発

Git が一番力を発揮するのは、多人数で開発する時だと思っています。
そうすると必然的に「中央リポジトリ」が存在します。
中央リポジトリ = 誰でも見る事ができるリポジトリ です。

コミットの歴史を綺麗に保つ対象は、リモートにある中央リポジトリになります。
逆に言えば、ローカルにある自分だけのリポジトリはどうでも良いです。

ブランチの統合

中央リポジトリを持った開発をしている限り、ブランチの統合作業無しでは開発できません。
リポジトリとのやりとりは、いうなればブランチの統合作業のことです。
自分が操作するブランチがどのような意味・役割を持っているのか、意識する事が重要です。

ローカルリポジトリ内には、リモートリポジトリへ push されるブランチと、自分だけのブランチがあります。
普段の開発は、自分だけのブランチで行います。
みんなに見てもらいたい区切りで、リモートへ push されるブランチに統合、push します。

統合の種類
  • merge --no-ff
    • マージコミットを明示的につくる事によってブランチを統合する。
  • rebase
    • 統合対象ブランチに対して、統合するブランチと全く同じ内容のコミットの歴史を載せる。マージコミットをつくらない。
  • merge --squash
    • 統合対象ブランチに対して、統合するブランチの歴史を一つにまとめたコミットをつくる。マージコミットをつくらない。
開発の流れ

今回想定する流れは、以下のようになります。

  1. 「自分の開発用ブランチ」をきる
  2. ある程度できたら、「みんなの開発用ブランチ」へ統合する
  3. 「みんなの開発用ブランチ」を push する
  4. レビューする
  5. 「みんなの開発用ブランチ」から「リリース用ブランチ」へ統合する
何を達成したいのか
  1. コミットの歴史を綺麗に保つ
  2. コードレビューを必ずする

以上の2点を満足させるようなマージ戦略を考えます。

結論に至った理由

さて、以上の前提を念頭に、最初に提示した結論に至った理由を書いていきます。
説明しやすい順に書いていきます。

ローカル(自分用) → ローカル(自分用)

自分用は他の人に晒される事もないので、適当にやれば良いです。
最終的には、squash して一つにまとめますし。

結論 : 適当にやる

ローカル(自分用) → ローカル(リモート用)

ローカルで切ったブランチを他の人に晒したくなったタイミングで行われます。
この作業をしたあとに、ローカル(リモート用) はリモートにある中央リポジトリに push されます。

では、それぞれのブランチについて考えます。

  • ローカル(自分用)
    • 試行錯誤の末、小さなたくさんのコミットがつまれている。とてもじゃないけど、人様に見せられない。
  • ローカル(リモート用)
    • 人様にお見せする為の(push される)ブランチ。
    • レビュー対象だから、レビューしやすい様にまとめたい。

どうやって2つのブランチを統合するか、「統合の種類」に出てた3種類から考察します。

  • merge --no-ff
    • マージコミットつくるのは良いが、結局恥ずかしい歴史が晒される事には変わりない。
    • マージコミット、ファイルの差分を表示しないので、レビューしづらい。
  • rebase
    • 恥ずかしい歴史をそのまま載せるとか考えられない。
    • そもそも、リモート用リポジトリのコミットハッシュが変わって危険。
    • みんなが共有しているコミットがたった一つの間違いで変わってしまう…怖いよね。やらないように。
  • merge --squash
    • 今までの変更を一つのコミットにまとめられるなんて、まさに理想。
    • 一つにまとまってると、レビューもしやすい。

結論 : git merge --squash を使う

注意 : あまり大きいコミットになると分かりづらいから、そこら辺はうまいこと調整すれば良いんじゃないかな。

リモート → ローカル(リモート用)

これが行われる場面は、他の人の歴史を自分の歴史に統合する時です。
要は pull する時です。
それぞれのブランチの役割を考えます。

  • リモート
    • みんなが綺麗に保って育てた自慢のブランチ
  • ローカル(リモート用)
    • 私の自慢の修正をみんなに周知する為のブランチ
    • 後でリモートに統合する

ここに出てくるローカルブランチは、後でリモートに統合する事になります。
つまり綺麗に保つ必要性があります。

そして、pull にはブランチの統合の方法が2つあります。
一つはデフォルトの merge で、もう一つは rebase になります。
それぞれの特徴と、結論を見ます。

  • merge
    • マージコミットができる可能性があり、みんなが綺麗に保って育ててきた歴史を汚す事になる。
    • 私の自慢のコミットは統合されるべきだが、必要の無いコミットはいらない。
  • rebaes
    • みんなが綺麗に保って育ててきた歴史をそのまま載せられるから素晴らしい

結論 : git pull --rebase を使う

ローカル(リモート用) → ローカル(自分用)

これだけは入れてー!っていう急な修正が入った時とか。
上記と全く同じ理屈で rebase を使いましょう。
選択肢としては他にも merge --squash がありますね。
が、みんなが綺麗に保って育ててきた歴史を一つにまとめるなんて考えられないので却下です。

結論 : git rebase を使う

ローカル(リモート用) → ローカル(リモート用)

これは、開発ブランチ上でのレビューが終了して、リリースブランチにマージする時に行われます。
基本的にリポジトリ管理者だったり、チームリーダーがやったりします。

リポジトリの考察です。
マージする側を開発、される側をリモートとしましょう。

  • 開発
    • レビュー済みの綺麗な歴史が積まれている。
  • リモート
    • 後からみた時に、どんな歴史が刻まれたのか、しっかり把握しておきたい。
    • マージ後、タグが打たれたりする。

では、「統合の種類」からどれが適切か考えます。

  • merge --no-ff
    • マージコミットがつくられると、どの地点でこのブランチの歴史が変わったかが分かるので良い。
    • かつ、マージ元の歴史も参照できるのが良い。
  • rebase
    • 開発ブランチの歴史がそのまま残るのは良い。
    • が、コミットハッシュが変わる可能性を秘めてるので良い子はやらないように。
  • merge --squash
    • 開発ブランチの綺麗な歴史をひとまとめにするのは、もったいない。

もう一個、普通の merge について考えます。
merge --no-ff との違いは、fast-forward の場合にマージコミットがつくられない事です。
で、前提で話したようなやり方だと、ほぼ fast-forward でマージされます。
論点をマージコミットをつくるか、つくらないか、という所に絞れます。
そうすると、上述したように、どの時点でマージされたかが分かるようにマージコミットをつくった方が良い、という結論になります。
結果的に、リリース用ブランチにはマージコミットだけが乗っかります。

結論 : git merge --no-ff を使う。

実は不完全

開発用ブランチに自信を持ってマージして push したが、レビューでボロクソにされてリジェクトされた場合。
開発用ブランチにはボロクソなコミットが残ってしまいます。
このままだと、開発用ブランチ上の歴史が綺麗とは言えない状態です。
(このブランチ上にボロクソなコミットを残したまま、修正コミットを入れるのは残念な感じです)

解決策としては、自分のコミットは開発用ブランチに push するのでは無く、
「開発用ブランチに merge して欲しいブランチ」を remote に push して、
そのブランチをレビュー、リジェクトされたら新しいブランチを push、
レビューが通ったら開発用ブランチに merge する、
みたいな手法が考えられます。

もうここまで来ると結構面倒。
これほど労力を振り絞ってリポジトリを綺麗にする必要があるのか?
そこはチーム毎に決めれば良いと思います。
(Git に慣れればそれほど面倒でも無い気はしますが、メンバー全員 Git 堪能なチームなんてそうそう無い気もします:p)

もしくは、google 謹製の gerrit というシステムを使ったり、
github を使って pull request を用いる方法もあります。

終わり

実はこの記事を書こうと思ったのは、git のマージの為のコマンドってすごく考えられてできてるんだなーと思ったからです。
そして、マージコマンドをフル活用している俺カッコイイ!がしたいだけです。

あとは、気付いた方もいると思いますが "A successful Git branching model" に影響受けまくりです。
見えないチカラ: A successful Git branching model を翻訳しました

ツッコミ大歓迎なので、何かありましたらコメントなり @tanaka51 までメンション飛ばすなりしていただければと思います:)

Gitによるバージョン管理

Gitによるバージョン管理


入門Git

入門Git