2025年1月14日
1977年生まれ、大阪府豊中市出身。株式会社ソニックガーデンのRailsプログラマ、およびプログラミングスクール「フィヨルドブートキャンプ」のメンター。ブログやQiitaなどでプログラミング関連の記事を多数公開している。将来の夢はプログラマーをみんなの憧れの職業にすること。主な著書に「プロを目指す人のためのRuby入門 改訂2版 言語仕様からテスト駆動開発・デバッグ技法まで」(技術評論社)などがある。
前回の記事ではgitのコミットに焦点を当て、良いコミットはどうあるべきかを議論しました。
しかし、業務で開発しているプログラムに対して変更を加える場合、いきなりmainブランチにコミット&プッシュするケースはまれだと思います。みなさんの現場でもほとんどの場合、以下のようなワークフロー(もしくはこれに類似した手順)で変更を適用しているはずです。
つまり、プログラムを修正する最小の単位はコミットですが、それをまとめた一段階大きな単位としてプルリクエスト(略称はプルリクやPR)があります。
そこで今回の記事ではコミットではなくプルリクエストに焦点を当て、良いプルリクエストと悪いプルリクエストについて議論してみようと思います。
前回の記事と同様、対象読者はまだ業務に入って間もない新人エンジニアですが、新人さんだけでなく、開発チームで新人エンジニアを指導しているリードエンジニアのみなさんも、チーム内で「良いプルリクエストとは何か?」を議論する叩き台として本記事を活用してもらえると嬉しいです。
なお、この記事の本文には前回書いたコミットの記事を読んでいる前提の箇所がいくつかあります。まだ読んでいない方は先にそちらを読んでからこの記事に戻ってきてください。
コミットに関する話題もそうでしたが、プルリクエストに対する考え方もまた、組織の文化の影響を受けやすいと思います。本記事では筆者のこれまでの開発経験をもとに「こういうプルリクエストがいいのでは?」という提案をしていきますが、もし「ウチのチームと考え方が全然違う!」という内容があれば、そちらの考え方を優先してもらっても大丈夫です。
ちょっと本文が長くなってしまったので、筆者の考える良いプルリクエストの条件を先に挙げておきます。
具体的にどういうことなのかは、ここから下で詳しく説明していきます。
最初に検討したいのはプルリクエストの大きさです。
プルリクエストの大きさは変更されたファイルの数(File changed)や、追加・削除された行数で定量化できます。レビューする側の立場からすると、大きすぎるプルリクエストのコードレビューは時間がかかるので非常に気が重くなります。
絶対的な基準があるわけではありませんが、筆者は経験的に以下の範囲に収まっていれば許容範囲かなと考えています(注:筆者が携わっているのは主にRuby on Railsを使った開発プロジェクトです。使用する言語やフレームワークによって望ましい数字は前後すると思います)。
もちろん、プルリクエストの内容によっては単純な修正でもdiffが大きくなったり、逆にdiff自体は小さいのにレビューにはとても苦労したりするケースがあるので、必ずしも「diffは小さい方が良い、大きいと悪い(小さいとレビューが楽、大きいとしんどい)」とは言えません。ですが、概してプルリクエストの大きさとレビューの大変さは比例します。
レビューを受ける側も、大きなプルリクエストほど指摘コメントがたくさん付くので、コードの修正やコメントのやりとりに苦労すると思います。diffが小さければそのぶんコメントの量も減るので、レビュー後の修正作業が楽になるはずです。
しかし、大規模な新機能開発があったりすると、さすがに「diffは300行以下に抑えてください」とは言えません。こういう場合は、プルリクエストをいくつかのフェーズに分割しましょう。具体的には以下のような手順で開発します。
この手順を図で表すと、以下のようなイメージになります。
開発用ブランチA-1やA-2は適当な単位で分割して、なるべくdiffを小さくするのがポイントです(たとえばA-1は主に管理画面を開発し、A-2では主にエンドユーザー画面を開発する等)。
mainブランチへマージする直前のプルリクエスト(上の手順でいうと手順8で作成したプルリクエスト)のdiffはかなり大きくなってしまいますが、基本的にどのコードもレビュー済みなので、最後はざーっと見て気になる点がなければOK、という程度で終わらせても問題ありません。
これは言わば「プルリクエストの小口化」です。大きなプルリクエストを最後にどかん!とレビューしてもらうのではなく、キリのいいタイミングで「まだ続きがあるけど、いったんここでレビューお願いします!」というふうに、レビューしやすいサイズにプルリクエストを分割しましょう。
前回のコミットの記事では、突然発生した不具合の原因調査をするために直近のコミット一覧を見て怪しいコミットがないか調査する、という話を書きました。
これと同じことはプルリクエストにも言えます。つまり、突然発生した不具合の原因調査をするために、直近にマージされたプルリクエストの一覧を見て、怪しいプルリクエストがないか調査する場合があります。
このとき、プルリクエストのタイトルが具体的でわかりやすいものになっているほど、調査がしやすくなります。たとえば、「商品画面の修正」ではなく、「商品の詳細画面に各種SNSのシェアボタンを設置」と書いてあった方がより具体的で調査を進めやすくなる、といった具合です。
プルリクエストの入力フォームはタイトル欄と説明欄が分かれています。
タイトルだけでなく、説明欄も具体的かつ、わかりやすく書きましょう。たとえば説明欄には以下のような情報が載っていることが望ましいです。
たとえば、「自分の日報に言及している日報を表示する」というタイトルのプルリクエストの説明欄は以下のように記述します。
## 概要 自分の日報のURLが他の日報の本文に含まれている場合、その日報を「この日報に言及している日報」として表示します。 ## 開発チケットのURL https://github.com/your/repository/issues/33 ## スクリーンショット <img width="707" alt="img" src="https://example.com/screenshot.png">
上の説明欄をGitHub上で表示すると以下のように表示されます。
また、必要に応じて以下のような情報も載せておくと、レビュアーに喜ばれるかもしれません。
こうした情報の一番の目的はレビュアーに対する情報提供ですが、それだけでなく「大きなレベルのコミットメッセージ」として、将来役に立つ可能性があります。すなわち、数年後に「いつ、誰が、なぜ、こんなコードの変更を入れたのか?」を調査する過程で、未来の開発者があなたの作ったプルリクエストを見に来るかもしれない、ということです。
前回の記事で紹介したように、GitHubではコミットの詳細画面からそのコミットが含まれるプルリクエストにジャンプすることができます。
たとえばコミットメッセージにはあまり詳細な情報が書いてない場合でも、そのコミットが含まれるプルリクエストを検索したときに詳しい情報がプルリクエスト内に書かれていれば、未来の開発者の「いったいなぜ?」に対する答えになるかもしれません。実際、筆者も5年以上前に作成されたプルリクエストを調査して「よくわからない謎コード」が導入された理由を発見したことがあります。
コミットメッセージと同様に、プルリクエストもまたそのプログラムの歴史そのものになり、場合によっては古文書のような役割を担うことがある、ということを押さえておきましょう。
前回のコミットの記事では、変更の目的とコミットが1対1で紐付くようなコミットを作成すべし、という話を書きました。
これと同じことがプルリクエストにも言えます。つまり、ある新機能をリリースするためのプルリクエストを作成したのであれば、そのプルリクエストに含まれるdiffはすべてその新機能に関連するdiffのみで構成されるべき、ということです。「たまたま発見した全く無関係なファイルのインデント修正」はその新機能のプルリクエストに含めるのではなく、別のプルリクエストとして作成しましょう。
なぜなら、コミットと同じようにプルリクエストもまた、マージ済みの変更をまとめてrevertする際に使われることがあるからです。新機能導入の変更をrevertしたいのに、同じプルリクエストにそのdiffが含まれていたためにインデントの修正まで元に戻ってしまった、というような事態は極力避けたいです。ゆえに、プルリクエストは意味のある単位でまとめ、プルリクエストの主題に合致しない無関係な修正は、別のプルリクエストを作成するようにしてください。
開発タスクをアサインされてからプルリクエストを作成するまでの一番シンプルなワークフローは以下のようになると思います。
上のワークフローでは一番最後にプルリクエストでは、一番最後にプルリクエストを作成しています。ですが、完成までに数日かかるような大きなプログラム修正を実施する場合は、早めにドラフトのプルリクエストを作るのがお勧めです。
具体的には以下のような手順になります(手順3に注目してください)。
プルリクエストをドラフトで作成するためには、プルリクエスト作成用ボタンのプルダウンメニューから”Create draft pull request”を選択します。
ドラフト状態のプルリクエストには”Draft”のバッジがつきます。
ドラフトから通常のプルリクエストに変更する場合は”Ready for review”ボタンをクリックします。
ドラフトのプルリクエストを作成するメリットは、作業の進捗や途中経過が周りの人からわかりやすくなることです。たとえば、先輩プログラマがあなたのドラフトプルリクエストをチラッと覗いて、「そこはこういうふうに実装した方がいいよ」とアドバイスをくれるかもしれません。また、自分自身が現時点で自分がどんな修正を加えたのか振り返りたいときにも、ドラフトプルリクエストがあればさっとdiffを確認することができます。もちろん、ドラフトプルリクエストはあくまでドラフトなので、そのプルリクエストは完成版ではなく「現在作業中」であることも周りの開発者に伝わります。
なお、現場によってはドラフト状態のことをWIP(ウィップ、Work In Progress)と呼ぶこともあります。こちらの呼び方も知っておくと良いでしょう。
小規模な修正や簡単な不具合修正であれば1プルリクエストにつき1コミットで済むかもしれません。一方、ある程度の規模の仕様変更や新機能追加を行う場合は、以下のように複数件のコミットが1プルリクエストに含まれるはずです。
さらに、コードレビューで指摘があった場合は、以下のようなコミットがさらに追加されるケースもあるでしょう。
個人的には1プルリクエスト内のコミット数がこれぐらいで収まっているなら許容範囲かなと思います。
ただし、コミット数があまりに多すぎる場合はちょっと問題です。前回の記事にも書いたとおり、あなたのコミットは将来他の開発者が「なんでこんなコードを書いたんだろう?」と疑問を覚えたときに調査の対象になる可能性があるからです。
このとき、git blameを使えば過去のコミットを遡って、ある変更が導入されたコミットを追跡することができます。たとえば、以下の画面操作ではLOGO_SIZEという定数が導入されたコミットに到達するまで、3回コミットをさかのぼっています。
こういったケースでは軽微な修正コミットが積み重なるほど、目的のコミットに到達するまでたくさんのコミットをさかのぼる必要が出てきます。
そこで、こうした問題を避けるために、無駄に細かいコミットを積み重ねないようにするためのTipsをいくつか紹介します。
gitではコミット時に--amend
オプションを付けることで直前のコミットを修正することができます。--amend
オプションの典型的な用途は直前に書き込んだコミットメッセージの修正です。
# amendオプションを付けて直前のコミットメッセージを修正する git commit --amend -m "実行ボタンのpaddingを微調整"
ですが、筆者はたまにコミット履歴を減らすために--amend
オプションを使います。
たとえば「○○機能用の管理画面を作成」というコミットを作成するために、「アレをして、コレをして、ソレをして全部完成したら最後にコミット」という手順にすると、「やっぱりアレが終わった時点にロールバックしたい」と思っても、その時点のコミットがないのでロールバックできません。しかし、「アレをしてコミット、コレをしてコミット、ソレをしてコミット」とその都度コミットしていくと、ただでさえ増えがちなコミット履歴がさらに増えてしまいます。
そこで活用したいのが--amend
オプションです。--amend
オプションを以下のような手順で使うと、コミットを1つだけに抑えることができます。
--amend
付きでコミット(直前のコミットがアレ+コレに変わる)--amend
付きでコミット(直前のコミットがアレ+コレ+ソレに変わる)こうするとコミット履歴は増やさずに、何かあればいつでも1つ前のステップにロールバックできる状態を作り出すことができます。
ただし、すでにGitHubにpush済みのコードを--amend
で修正すると、そのままではpushできなくなります。これはamendを使うとコミットは増えないものの、コミットIDはamendするたびに変わってしまう(=コミット履歴が改変される)ためです。もしpushしようとしてrejectされた場合は、--force-with-lease
オプションを付けてpushしてください。
# amendしたコミットをpushしようとするとrejectされる $ git push To github.com:JunichiIto/git-commit-sandbox.git ! [rejected] initial -> initial (non-fast-forward) error: failed to push some refs to 'github.com:JunichiIto/git-commit-sandbox.git' hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart. If you want to integrate the remote changes, hint: use 'git pull' before pushing again. hint: See the 'Note about fast-forwards' in 'git push --help' for details. # force-with-leaseオプションを付ければpushできる $ git push --force-with-lease Enumerating objects: 5, done. Counting objects: 100% (5/5), done. Delta compression using up to 8 threads Compressing objects: 100% (2/2), done. Writing objects: 100% (3/3), 352 bytes | 352.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) To github.com:JunichiIto/git-commit-sandbox.git + c7c597e...6b123d5 initial -> initial (forced update)
たまに自分のローカル環境ではエラーにならず、CI(GitHub ActionsやCircleCI等)でしかエラーが発生しないことがあります。
CI上でしか発生しないエラーに対処するには、原因や解決策を探るためにCI上で試行錯誤するしかありません。しかし、そうするとコミット履歴が無駄に増えていってしまいます。こういうケースでは以下のような手順で試行錯誤用のブランチとプルリクエストを作るのがお勧めです。
こうすればコミット履歴上はあたかも一発で問題を解決したかのように見せることができます。
ここではCIのエラー解決を例に挙げましたが、CIのエラーに限らず、「これは試行錯誤のコミットが増えそうだ」と思ったら試行錯誤専用のブランチを作ることを検討してみてください。
gitには複数のコミットを1つにまとめるスカッシュ(squash)という機能があります。
「夢中で開発してたら、まとまりのないぐちゃぐちゃのコミット履歴ができあがってしまった💧」という場合は、スカッシュを使ってコミットをまとめるのも良いかもしれません。
ただし、スカッシュはコミット履歴の改変になるため、コンフリクトの解決手順が複雑になったりする場合があります。ですので、ある程度gitの知識がないと使いこなすのは難しいかもしれません。かく言う筆者も実は上手に使いこなせていないので、スカッシュを使うことは滅多にないです😅
いちおう、スカッシュという手段はあることは頭の片隅に置いておいて損はないですが、普段からなるべく無駄に細かいコミットを積み重ねないことを意識して、スカッシュに頼らなくて済むようにするのが良いと思います。
なお、Ruby on Railsのようなオープンソースプロジェクトでは、積極的にスカッシュを使って「1プルリクエストにつき1コミット」を保つように求められるケースもあります。
プルリクエストは、1つのコミットにまとめておくことが望まれます。コミットを1つにまとめることで、新しい変更を安定版ブランチにバックポートしやすくなり、よくないコミットを取り消しやすくなり、Gitの履歴も多少追いやすくなります。Railsは巨大プロジェクトであり、不要なコミットが増えすぎると膨大なノイズが生じる可能性があります。
引用元: 「Ruby on Rails に貢献する方法 – Railsガイド」
1プルリクエストにつき1コミットを厳守するか、1プルリクエストにつき複数コミットを許容するかは、そのプロジェクトのルール次第です。筆者が参加しているプロジェクトではいずれも後者のスタイルを採用していますが、読者のみなさんの中には前者のスタイルで開発しているケースもあるかもしれません。その場合は「郷に入れば郷に従え」で、コミットをスカッシュするようにしてください。
プルリクエストを介したコメントのやりとりは通常テキストベースになると思います。ですが、プルリクエストの内容や規模によっては、以下のようにレビュアーと直接話す機会を設けるのも効果的です。
特に、レビューする側もされる側も、テキストだけのやりとりになると、ニュアンスがうまく伝わらずに相手をいらつかせてしまうこともよくあります。「コメント欄がなんかイヤな空気になってきたな」とか「これはテキストでうまく説明しようとすると骨が折れそうだ」と思ったら、リアルタイムに話す機会を設けましょう。そうすればそれまでの問題があっさりと解決したりします。
というわけで、本記事では筆者の開発経験を交えながら良いプルリクエストと悪いプルリクエストについて議論してみました。
前回のコミットの記事もそうですが、コミットもプルリクエストの作成も日常的に行う作業でありながら、わざわざ「どんなコミットがいいのか?」「どんなプルリクエストがいいのか?」と議論する機会はあまりないと思います。コミットやプルリクエストを学ぶと言っても、せいぜいレビュー時に先輩から「こうした方がいいよ」と注意されて少しずつ正しいやり方を覚えました、というパターンが多いのではないでしょうか。
前回の記事と今回の記事では現場の暗黙知になりがちな「コミットとプルリクエストのお作法」について、筆者なりの視点でなるべく丁寧に説明してみました。冒頭の「おことわり」にも書いたように、組織の文化やルールによっては本記事の内容をそのまま採用できない部分があるかもしれませんが、良いコミットや良いプルリクエストをチーム内で考えるための叩き台として、本記事を活用してもらえると幸いです!
関連記事
人気記事