1
/
5

【開発】スカッシュマージを卒業しプロダクトを加速させるブランチ戦略へ

こんにちは!

8月からSocialDogにジョインしたあっきーこと上田です!現在はチームリーダーとしてSocialDogの新規開発を仲間たちと励んでいます!開発たのしい〜〜〜!!

ところで私はgitが大好きです。なぜ好きなのかを語るとこの記事が終わらないので割愛しますが、

SocialDogにジョインした後、なんと早速gitの運用変更に携わったので、その経緯何をしたか、またおまけとしてマニアックなgitの活用方法について紹介していきます!

スカッシュマージとそのメリット

SocialDogでは以下の目的から2年ほどでプルリクエストをスカッシュマージで運用していました。

次のようなメリットがあるためです。

1トピック1コミットになる

プルリクエストがマージされたときにコミットが1つになるため、流れが追いやすくなります。

つまりgitのログ検索の操作でハマる時間の削減ができます

コミットを作る工数を減らせる

プルリクエストのタイトルをそのままコミットメッセージとして流用すること[^1] で、ベースブランチ[^2] のコミットメッセージは常にプルリクエストのタイトルになります

つまり、コミットのメッセージに都度悩む必要がなくなります。

またマージコミットを作る運用だと コミットをきれいにするため にrebase操作をしたりと、ややgitの上級的な操作をする必要が出てきます。

スカッシュマージによる運用であれば、意識しなくともベースブランチのコミットをキレイに保ち続けることができます。

スキルのグラデーションがある中でも 開発サイクルを高速に回しつつコミットをキレイに保つ というバランスを取れるので当時の開発チームにフィットしていました。

[^1]: GitHubではPull Requests設定で可能です。

[^2]: 開発のもとになるブランチ。よくあるのがmain・master・developブランチなど

スカッシュマージの課題

スカッシュマージによる運用は当初数年はうまくいっていましたが、開発の体制やコンテキストが大きくより複雑になるにつれて、いくつかの問題点が発生しました。

コミットが巨大になることがある

大きな機能の開発では、当該機能のためのトピックブランチを作成し、段階的に開発を進めることがあります。

この場合トピックブランチ上ではサブタスクごとにプルリクエストが作られるので、サブタスクの単位でコミットも作成されますが…

機能が完成し、いざベースブランチにマージするとき、今までのコンテキストを含んだコミットは1つのコミットにスカッシュされます。

この図では3個のコミットが1つのコミットにスカッシュされましたが、普通のコミットよりも情報量が3倍程度になったといっていいでしょう。

つまり、ソースコードを git blame[^3]などで検査しても、本来どのプルリクエストで修正されたのか がわからなくなります。

[^3]: 各プログラムコード行がどのコミットで追加されたかを調べる操作

コンフリクトが増えやすい

ある機能の開発が一定期間にわたるなら、定期的にトピックブランチへベースブランチをマージしたくなります。これは、ソースコードの実態をベースブランチと乖離させたくないためです。

ただ

①このマージをスカッシュマージ で行い

②ベースブランチのコミットが進んで…

③もう一度ベースブランチをトピックブランチにマージする

と、コンフリクトが発生します。

なぜでしょうか。

「どこまでマージしたか」という①の情報はスカッシュしたときに消え去ったため、③のマージのときに取り込み済のコミットも再度取り込もうとするからです。

なぜならコミットログだけでは、トピックブランチ上で反映済のコードが「ベースブランチ上で修正されたコードをすでに取り込んだコード」なのか「トピックブランチ上で修正されたコードがたまたまベースブランチ上のコードと一致したのか」区別できないため、gitはコンフリクトを起こして開発者に解決を任せるのです。

SocialDogではこの「同じコンフリクトを何度も解消する」工数が一部の開発者に重くのしかかっていました。

マージコミットを作る運用へ

結論からいうとプルリクエストのマージはマージコミットを作る運用に変えて上記課題を解決しました。

メリット1:コミットを分割できる

マージコミットを作るということは、スカッシュマージによってコミットが巨大になるデメリットをそのまま避けることに繋がります。

メリット2:不要なコンフリクトを避けられる

先程の①と③の図をマージコミットを作るマージで行うと、

①のマージは次のようになります。

そして③のマージを行うときには①でマージ済のコミット以外のコミットだけが取り込まれるので、先程のコンフリクトは発生しません。

gitのマージ原理を簡単に説明すると

  1. マージ元のコミット
  2. マージ先のコミット
  3. マージ元とマージ先の共通祖先コミット

の3つのコミット同士のファイルを比較してマージを行う3-wayマージというアルゴリズムでマージされます。

3-wayマージでは共通祖先コミット(図中 C_base)からマージ先コミット(図中 C_to) 、マージ元コミット(図中 C_from)へのファイル差分(diff)を取り(図中 p1、p2)、共通祖先コミットのファイルツリーに適用するという方法を取ります。(図中では C_base p1p2 を適用してマージコミット C_merge を作成しています)

①のマージでマージコミットを作ると、ベースブランチとトピックブランチとの共通祖先コミットが繰り上がり、①で取り込んだコミットを無視することができるのです。

デメリットと向き合う

マージコミットを作る運用へ切り替えるということは、いままでスカッシュマージで得ていたメリットがそのまま利用できなくなるというのがそのままデメリットになりそうです。

ここは次の2点で可能な限り軽減できるように配慮しました。

--first-parentフラグ

git log コマンドでは --first-parent というフラグが使えます。

このフラグを有効にすると、スカッシュマージと同等のログを表示できます。

画像引用元リポジトリ: https://github.com/containers/podman

CLIやサポートするGitクライアント限定の方法ではありますが、これで1トピック1コミットのログを見られるというスカッシュマージのメリットを利用できることを紹介し、ドキュメントにも記載しました。

コミットの作成ルールは設けない

マージ以外のコミットの作成方法についてはルールを増やしませんでした。

具体的には以下のようなルールを設けませんでした。

  • プルリクエスト作成前のrebaseの実施
  • コミットメッセージの書き方

次のような理由から、移行のコストを必要以上に上げないためです。

  • 「コンフリクト解消工数を減らしたい」が主要な目的
  • もしキレイなコミットが見たくなったら前述の --first-parent フラグを使えばよい

開発体制スケールに伴うブランチ戦略の変更

間もなく、SocialDogは複数のチームへ分かれることが決まっていました。社内リソースが増えたことによる開発体制のスケールが背景にあります。

マージコミットを作る運用に切り替えたことで、開発体制に合わせたブランチ戦略を採用しやすくなります。開発体制の変更に合わせ、ブランチの運用を次のように切り替えました。

もともとのブランチ運用

masterから開発ブランチを切ってmasterにマージするというシンプルな方法で運用していました。

リリースは所定のタイミングでmasterブランチをリリースします。

新ブランチ運用

結論として次の図のようなブランチ運用方法を採用しました。

何が変わったのか

環境ごとにブランチを分けて管理するようになりました。

具体的には次の2つを用意しています。

  • 本番環境用のproductionブランチ
  • 検証環境用のstagingブランチ

これはマージコミットを作るからこそ可能になった管理方法です。スカッシュマージでは前述のようなコンフリクトの問題が起きるため、複数ブランチを管理する考え方は採用しづらいです。

ブランチを目的別で用意することで、以下のようなメリットを得ることができました。

リリースの準備期間に開発を止めない

SocialDogのリリースフローは次のような流れでした。

  1. masterブランチへのマージを止める
  2. masterブランチをテストする
  3. masterブランチをリリースする
  4. masterブランチへのマージを再開する

1~3の間は数日かかることもあり、レビュー済のブランチもmasterにマージできなくなるため、この間に開発が停滞することがありました。

環境ごとにブランチを分けることで、このフローを次のように変えられました。

①masterブランチをstagingブランチへマージする

②stagingブランチをテストする

③stagingブランチをproductionブランチへマージしてproductionブランチをリリースする

リリースに

  • 含めたいコミットはstagingブランチへマージする
  • 含めたくないコミットはmasterブランチへマージする

というブランチの使い分けによって、masterブランチの開発を止めずに済むようになりました。

本番が何かわかりやすい

productionブランチは本番環境のコードと一致するため、本番環境と同等のコードを調査しやすくなりました。

またHotFix対応もシンプルになります。

SocialDogのHotFixとは「本番へすぐ適用したい修正」を意味します。

以前はmasterブランチを任意の時点でリリースしていたので、HotFixのために本番環境のコミットを調査してHotFix用ブランチを作成する必要がありました。

新しい運用ではHotFixのブランチはproductionブランチから切る形になりました。

修正を一刻も速くリリースしたいHotFix対応において、対応フローがシンプルになることは大事な点だと考えています。

他考慮・意識した点など

  • productionブランチが進んだらmasterブランチへマージします。これはstagingブランチ上で行った修正やHotFix対応をmasterに取り込むためです。ここはCIが通れば自動でマージするようGitHub Actionで仕組み化しました。
  • あえてgit-flowGitHub flowといった既存のブランチ運用フローにはこだわりませんでした。これらは特定の開発・リリースフローに最適化されています。SocialDogの開発・リリースフローをより効率化・最適化したいとき、ブランチ戦略も同時に再検討することは十分に考えられます。「〇〇フロー」に縛らないことで柔軟性を維持することは大事だと考えました。

ブランチ戦略 とプロダクトの価値

以上のことを踏まえると、ブランチの運用方法を考えることは、コードをリリースするまでのプロセスを考えることに繋がります。

もしコードをリリースするサイクルを効率化できれば、ユーザーへ価値を提供するサイクルも高速化できます。

たかがブランチ運用と思えるかもしれませんが、プロダクトの価値をより速く高めるためにも、ブランチ戦略ともいうべきこの運用について、折々で真面目に向き合いたいものです。

小話

冒頭にも書きましたが、この記事を書いてる上田は8月にSocialDogにジョインし、なんと10月にはブランチ運用の変更について担当を任せていただきました。

実は入社早々に「プルリクエストのマージ方法を変更したい」とバックログを起票しており、ここから

  • マージ方法の変更
  • ブランチの運用

について意見を求めていただき、運用変更に至ったという経緯があります。

SocialDogには「バックログは誰でもいつでも追加してOK!」という文化があります。

Jira 利用ガイドライン - SocialDog情報ポータル - Confluence

ブランチ運用における問題がたまたま起きていた、というタイミングのよさもあったと思いますが、「追加してOK!」からのまさかジョイン数ヶ月にも満たない時点で担当を任せてもらえるとは思いませんでした。非常に変化に対して前向きな会社で日々ワクワクしながら業務に当たっています。

SocialDogは「テクノロジーで世界をスマートに」のミッションに共感できるデベロッパーを募集しています!

ぜひ興味のある方は弊社のアツイ代表小西がアツイ話を語りますので、お気軽にご連絡ください!!!

株式会社SocialDogの募集・採用・求人情報 - Wantedly
株式会社SocialDogの新卒・中途・インターンの募集が234件あります。気軽に面談して話を聞いてみよう。職種や採用形態からあなたにあった募集を見つけることができます。募集では「どんなことをやるのか」はもちろん、「なぜやるのか」「どうやるのか」や実際に一緒に働くメンバーについて知ることができます。
https://www.wantedly.com/companies/socialdog/projects

おまけ

スカッシュマージの運用で困ったときのノウハウ も溜まったので、どこかの誰かのために残しておきます。

あくまでワークアラウンドのため、運用に困ったのであればマージコミットを作る運用 への切り替えができないかの検討をまずおすすめします。

スカッシュマージのままコンフリクトを避けたい

2度以上ベースブランチを取り込もうとしてコンフリクトしてしまうのを避ける方法です。

この図のケースでは、1度目にマージしたコミット Cx

  • コードの変更なし
  • マージコミットを作る

つまりファイル変更なしで再マージしておくことで、2度目のマージではコミット Cx 以前の差分は無視されます。

具体的には

git checkout topic # マージ先のブランチをチェックアウト
git merge -s ours Cx # Cx を変更なしでマージ

というコマンドで、コード変更なしでマージコミットを作成できます。

マージコミットさえできてしまえば、ベースブランチとの共通祖先が繰り上がるため、新しいコミットのことだけ考えればよいということになります。

SocialDogでは当時コンフリクトが93→18まで減ったりしました:

スカッシュマージのまま分割したコミットを見たい

スカッシュマージを行ってもGitHub上にスカッシュする前のコミットは残っています。

これは git ls-remote コマンドでGitHubリポジトリ上のrefsを一覧することで確認できます。

画像引用元リポジトリ: https://github.com/containers/podman

これを利用して、あたかもマージコミットを作った場合のコミットツリーを表示することができます。

replace というコマンドで「あるgitのオブジェクトを別のオブジェクトに置き換えて表示する」ことで実現します。

replaceの解説については Git - Git オブジェクトの置き換え が参考になるので、解説はこちらにゆずります。

具体的には次のようなスクリプトで置き換え用のオブジェクトを作成します。

#!/bin/bash

set -Eeuo pipefail

# refs/pull/*/head 以下をローカルに取得してから実行します
# 通常以下のコマンドで取得できます
# git fetch origin 'refs/pull/*/head:refs/pull/*/head'

if [[ $# -lt 1 ]] ; then
    echo "usage: $0 <revision range>"
    echo "example:"
    echo "  $0 HEAD"
    echo "  $0 abc01234..ef56789"
    exit 1
fi

git rev-list --oneline $@ | while read line ; do
    # 「プルリクエストタイトル (#XXX)」というコミットメッセージから
    # コミットハッシュとpull request numberのXXXを抜き出す
    if [[ "$line" =~ ^([0-9a-f]+).+\(#([0-9]+)\)$ ]] ; then
        hash=${BASH_REMATCH[1]}
        pr=${BASH_REMATCH[2]}
    else
        echo "skip: $line" >&2
        continue
    fi
    # 親コミットの数を取得
    # 「マージコミット」は2つ以上の親コミットを持つ
    parents=$(git cat-file commit $hash | perl -nle '/^$/ and exit; /^parent/ and print' | wc -l)
    if [[ $parents -gt 1 ]] ; then
        # すでにマージコミットなので無視
        continue
    fi

    # プルリクエストでマージしたコミット参照
    pr_head="refs/pull/$pr/head"

    # 置き換えオブジェクトの作成
    git replace --graft $hash $hash^ $pr_head
    echo "done: $line" >&2
done

詳しいスクリプトの解説は割愛します。gitのデータ構造を理解した操作が必要なので、興味のある方はGit公式が用意している Git - 配管(Plumbing)と磁器(Porcelain) のページが参考になると思います。

置き換えオブジェクトが作成されると、無事にスカッシュされていないコミットツリーを参照できます。

ただし置き換えオブジェクトを使う場合、git操作のパフォーマンスが落ちたり、一部の挙動にバグがあったり(執筆時点 v2.38.1)するため、あくまでワークアラウンドな手段であるということにご留意ください。

株式会社SocialDog's job postings
25 Likes
25 Likes

Weekly ranking

Show other rankings
Invitation from 株式会社SocialDog
If this story triggered your interest, have a chat with the team?