git merge-distinct: octopus-merge で複数ブランチを効率的に処理する

git merge-distinct とは、git の octopus-merge を利用することで、変更点が互いに競合しない複数のブランチを単一の HEAD にマージできる、ちょっとしたツールです。なぜ、このようなものが必要なのでしょうか?それは、単一のフィーチャーブランチにおける独立した開発が便利である一方、テストやステージングサーバー向けデプロイを向けに複数ブランチをまとめることが有益な場合も多いためです。

論理的あるいは文字通りの競合が起こり得るため、この戦略が全てのブランチで利用できるわけではありません。しかし、これが非常に便利なユースケースがいくつか存在します。現に、あなたは今それを見ているのです。

私たちのデベロッパーブログ

アトラシアンのデベロッパーブログ (Atlassian Devlopers) には、二つの環境があります。一つは、今あなたが見ている本番環境、もう1つは、主にレビュー向けに使用されるステージング環境です。私たちは、自社のブログをコードにように扱っています。即ち、各記事はフィーチャーブランチ上で書かれ、プルリクエストでレビューされるのです。また、私たちはブランチレベルで継続的デプロイメントを実践しているため、フィーチャーブランチが作成あるいは更新される度に、Bamboo サーバーがサイトを再構築して、デプロイします。レビュアーが、生のマークダウンではなくレンダリングされたものを読めるように、プルリクエストではブログのステージングバージョンをリンクするのが一般的となっています。

rendered
レンダリング済 vs. 生のマークダウン

これは、弊社のブログの執筆者が増加したことで、複数の記事が同時に執筆されるようになるまでは、見事に機能していました。単一のステージングサーバーと複数のブランチでは、一番最後に修正されたブランチがサーバーにデプロイされるという「最後にアップデートしたもの勝ち」ゲームになってしまったのです。あなたのブランチがステージングされてレビュー待ちだとしたら、それは残念!次のプッシュで上書きされてしまうのです:

clobbering

これは、ステージングされた記事へのリンクが基本的に 404 ページとなってしまうことを意味しており、レビュアーはローカルに特定のブランチを構築しなくてはならないのです。サイトをローカルに構築するとなると、時間が非常にかかる上、一部の技術に長けていないユーザーはレビュープロセスへ参加できなくなってしまいます。

私のステージングされた記事が同僚によって最後に上書きされた時、私はいい加減、問題を解決することにしました。私は、自分の記事が常にレビュー向けに利用できるよう、3 つの解決策を考え出したのです:

オプション 1: 各ブランチ向けに、新たなステージングサーバーを立ち上げる

ステージングサーバーが複数あれば (各ブランチに一つずつ)、互いの変更を上書きする事態が無くなります:

multi-staging

しかし、これでは瞬く間にAWSの請求書が膨れ上がってしまいます。git branch --no-merge
では、他に 8 つのブログが開発中であることが示されているため、現時点でのブログ執筆者に対応するだけでも 8 つ のステージングサーバーが必要になることが分かります。

オプション 2: アップデートを継続的に私の ブランチにプッシュするスクリプトを書く

何度も頻繁にプッシュしていれば、他人の変更を上書きできますよね!ムハハハハハハ!

insta-clobber

この方法であれば、私の 問題は簡単かつ素早く解決できるものの、これは明らかに私たちの 4 つめのコアバリューに違反しています。率直に言うならば、かなりムカつく行為です。

オプション 3: ブランチをオクトパスマージして、結果をステージングする

Git がサポートしている octopus-merge というマージ戦略では、二つ以上のブランチをマージできます (場合によっては、それ以上多くも可能)。私は、未処理のブランチが一つ以上ある場合は、それらをマージして、その結果をステージングサーバーにデプロイできるはずだと考えたのです:

merge-before-deploy

パッと見は複雑そうですが、オクトパスマージの実行は比較的単純です (コマンドは、単にgit merge <branch0> <branch1>.. <branchN>)。しかし、私たちのデベロッパーブログのケースに関しては、いくつかの特別な要件が存在します:

  1. マージは、絶対に競合してはいけません。このステージングジョブは非対話型で実行されるため、誰もそれを解決できる人がいません。
  2. マージに含まれるブランチは、静的コンテンツを変更するものだけにしなくてはいけません。コードは、自動的にマージするには危険すぎます。git が認識する形で変更点が競合しない場合でも、コンパイルの失敗もしくはわずかなバグが原因となって、論理的競合に陥る可能性があります。
  3. あなたのコンテンツが、まだレビュー向けに準備が整っていない場合、マージからオプトアウトする方法 がなくてはいけません。
  4. 私たちのビルドスクリプトに対して直接の解決策を講じるのではなく、将来の類似した問題を解決できる汎用ツール を構築したいのです。

これらの要件を念頭に、私は git merge-distinct を作成しました。シェルスクリプトで書くには複雑性が私の許容範囲を超えているため、これは Node.js で書かれ、npm でパッケージされています。引数抜きで実行されると、あなたの現行の HEAD および非競合の変更を含むレポジトリ内のその他全てのローカルブランチから、新たなマージコミットを作成します:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ git merge-distinct
Merged 3 parents:
    feature/current-branch
    feature/another-branch
    feature/yet-another-branch

$ git log -n 1
commit 2d04b8bd51e3883b0af60defe39a90e568289b1b
Merge: a51aba5 06a467e 8263654
Author: Tim Pettersen <tim@atlassian.com>
Date: Tue Jan 13 14:28:03 2015 -0800

    Merge result of:
      feature/current-branch
      feature/another-branch
      feature/yet-another-branch

git 競合の回避

git merge-distinct が絶対に競合しない理由、それは同じパスを修正するブランチを絶対にマージしないからです。内部では git branch --no-merge を実行して、現行 HEAD にどのブランチをマージするかを決定します。これを繰り返した後、同一パスへの変更を含む全てのブランチを無視します。

論理的競合の回避

確実に静的コンテンツのみがマージされるよう、私は --exclude--include オプションを利用して、マージ候補となるブランチで変更可能なパスを、ユーザーが指定できるようにしました。例えば、以下のコマンドは .js ファイルを一切変更せず app/posts/ 下のみを変更している全てのブランチをマージします:

1
$ git merge-distinct –include ‘app/posts/**’ –exclude ‘**/*.js’

ブランチの選択的マージ

デベロッパーが、自分たちの変更がマージされ(そして結果的にステージングされ)ることからオプトアウトできるように、対象ブランチのパターンをユーザーが指定できるようにしました。例えば、以下のブランチは feature/ で始まる全てのブランチをマージします:

1
$ git merge-distinct ‘feature/**’

git merge-distinct はまた、マージコミットのカスタム化に関するいくつかのオプションに対応しています。:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git-merge-distinct –help

Usage: git merge-distinct [<options>] [<branch glob>]

Options:

-i, –include <path glob>     only branches with changes modifying paths
                             matching this pattern will be included
-x, –exclude <path glob>     any branches with changes modifying paths
                             matching this pattern will be excluded
-n, –no-commit               perform the merge but do not autocommit, to give
                             the user a chance to inspect and further tweak
                             the merge result before committing.
-m, –message <message>       override the default commit message

私たちは、Bamboo Node.js plugin を使用することで、これをデベロッパーブログのビルド過程に組み込みました。その結果、互いの変更を毎回プッシュで上書きすることが無くなりました。

git merge-distinct は、全体あるいは部分的に静的なその他のプロジェクト、また自動化によって複数のブランチを組合せなくてはならない、他のユースケースでも恐らく機能するだけの一般性を備えています。ソース を確認するか、あるいは以下を利用してローカルにインストールすることも可能です (git、node.js と npm がインストール済という想定):

1
$ npm install -g git-merge-distinct

git は、gitで始まる、あなたのパス上のその他のコマンドを認識できるため、通常の git コマンド同様に git merge-distinct を呼び出すことが可能です。

フィードバックや問題、またこれが有益となるその他のユースケースについては、Twitter (私は @kannonboy) で私にお知らせ下さい。

*本ブログは Atlassian Developers の翻訳です。本文中の日時などは投稿当時のものですのでご了承ください。
*原文 : 2015 年 1 月 15 日投稿 "git merge-distinct: staging multiple branches with octopus-merge"