2025年1月6日
1977年生まれ、大阪府豊中市出身。株式会社ソニックガーデンのRailsプログラマ、およびプログラミングスクール「フィヨルドブートキャンプ」のメンター。ブログやQiitaなどでプログラミング関連の記事を多数公開している。将来の夢はプログラマーをみんなの憧れの職業にすること。主な著書に「プロを目指す人のためのRuby入門 改訂2版 言語仕様からテスト駆動開発・デバッグ技法まで」(技術評論社)などがある。
みなさん、こだわりを持ってコードの変更内容をgitにコミットしていますか?
ある程度プログラマ歴の長い人はそれまでの経験から「コミットはこうあるべき」というご自身のこだわりや観点があるかと思います。一方、プログラミング歴が浅い人はgitの使い方に慣れることに精一杯で、なんとなく雰囲気でコミットしている人も多いかもしれません。
この記事ではそんなプログラミング初心者のみなさんのために、コミットは何のためにあるのか?そしてどんなコミットが良いコミットなのか?(または悪いコミットなのか?)を説明していきます。また、開発チームで新人エンジニアを指導しているリードエンジニアのみなさんも、チーム内で「良いコミットは何か?」を議論するための叩き台として本記事の内容をご活用ください。
まずはコミットが役に立つユースケース(シナリオ)を見ていき、各ユースケースにおいてコミットに求められる特性(つまり、どういうコミットだと嬉しいか?)を確認していきましょう。
ある程度コードを書き進めたが、途中で方針があまりよくないことに気付いてしまい、途中まで書いたコードを全部取り消したい、と思うことがあります。
git restore
を使えば特定のファイルを直前のコミットの状態に戻すことができます。
# something.rbの変更を直前のコミットの状態に戻す git restore something.rb
git reset
を使うと、リポジトリ全体を直前のコミットの状態に戻すことができます。
# リポジトリ全体を直前のコミットの状態に戻す git reset --hard HEAD
直前ではなく、それより前の特定のコミットまで戻りたい場合はHEAD
ではなく、コミットIDを指定します(注:この場合、過去のコミット履歴を改変してしまう恐れがあるので、gitの操作に自信がない場合は先輩プログラマに相談してください)。
# リポジトリ全体をコミットID=6a59d88の状態に戻す git reset --hard 6a59d88
もしくは、3つ前のコミットで変更した内容が、あとから実は対応不要だったとわかった場合はgit revert
でその変更を元に戻すことができます(注:git revert
では変更を元に戻すための新しいコミットが作成されます)。
# コミットID=6a59d88の変更を元に戻すための変更を加える git revert 6a59d88
こうした操作を実行する際に大事なコミットの特性は以下の通りです。
git revert
したいが、該当コミットに目的Aと目的Bの変更を含んでいると、git revert
したときに元に戻したくない変更Bも一緒に戻ってしまう)たとえば何らかの理由でブランチAで実施していた変更はブランチごと不要になったが、そのうちのコミットXだけブランチBに適用したい、と思うことがあります。そんなときはブランチBでgit cherry-pick
を実行すれば、ブランチAのコミットXをブランチBに適用できます。
# 別ブランチのコミットID=6a59d88の変更を現在のブランチに適用する git cherry-pick 6a59d88
このときに大事なコミットの特性は以下の通りです。
git cherry-pick
したいが、該当コミットに目的Aと目的Bの変更を含んでいると、git cherry-pick
したときに不要な変更Bも一緒に取り込まれてしまう)今まで問題なく動いていた機能がある日を境に突然バグってしまった、という問題に遭遇することがあります。
「きっとあのプログラム修正が原因だな」とすぐに目星が付いたときはいいですが、そうでない場合は直近のコミットログを振り返って、不具合の発生に関連しそうなコミットがないか調査しなければなりません。
たとえば、「特定のユーザーだけ、なぜかログインできなくなった」というような不具合が発生した場合は、コミット一覧を見てログイン処理に関連しそうなコミットを探す必要があります。
このときに大事なコミットの特性は以下の通りです。
特にチームで開発している場合は、自分以外の誰かが導入した変更がバグの原因になっている可能性もあります。そのため、コミットメッセージがその変更をコミットした本人以外にもわかりやすく書かれているかどうかで、調査のしやすさが大きく変わってきます。
また、コミットの回数が多すぎると、こうした調査の邪魔になります。たとえば以下のように「リファクタリング」や「コードレビュー対応」みたいな軽微な修正コミットが大量に並んでいると、問題を引き起こしたコミットを見つける手間が増えてしまいます。
それよりも以下のように「このコミットはこの目的を実現するためのもの」というふうにまとまっている方が調査するときは楽ですよね。
もちろん、現実の開発ではある程度コミットが細かく分かれてしまうのは避けられませんが、軽微な修正コミットばかりが必要以上に連続しないように意識しておくことは大事です。
なお、それなりに大きいプログラム修正はがんばって1コミットにまとめようとするより、複数のコミットに分割した上でそれらを1プルリクエストにまとめる方が自然かもしれません。プルリクエストの考え方については次回の記事で詳しく説明する予定です。
あなたは既存のシステムにある新機能を追加するタスクをアサインされました。その新機能に関連しそうなコードを調査していたところ、「なんだこれは?」と首をかしげるようなコードを見つけました。そのコードが書かれた背景がさっぱりつかめない場合は、git blame
でその変更が導入されたときのコミットを調査することができます。
# something.rb の各行について、最終コミットを確認する git blame something.rb
ただし、筆者はgit blame
をターミナルで実行することは滅多にありません。RubyMine(JetBrainsl製のIDE)、もしくはGitHub上で各行の最終コミットを確認します。
ですが、最終コミットを見れば必ず疑問が解決するとは限りません。もしそのコミットが「リファクタリング」や「typoの修正」だと背景や目的はわからないため、そういう場合はさらにその前のコミットを調査する必要があります。GitHubではコミットメッセージの右側に出てくる四角いアイコンをクリックすることで、更新の履歴をどんどん過去に遡っていくことができます。
たとえば以下は、LOGO_SIZE
という定数が導入されたコミットを探し出す場合の操作例です。
このときに大事なコミットの特性は以下の通りです。
ちなみに、GitHubでは以下の画像のようにコミットの詳細画面からそのコミットが含まれるプルリクエストにジャンプすることができます。コミットメッセージ内の情報が不足している場合は、プルリクエストを見に行くとより詳しい情報を得られるかもしれません。
本記事では学習用や個人の趣味で開発するプログラムではなく、業務で開発するプログラムを前提としてコミットの良し悪しを説明しています。
このとき、まず大前提として押さえておきたいのは、業務で使われるプログラムの寿命はものすごく長い、ということです。世の中には10年以上継続して開発し続けられているシステムもざらにあります。こうなると、「前任者はすでに退職していた」とか「もはや当時のやりとりを覚えていない」といったことが当たり前のように発生します。
そんなときに役立つのが過去のコミットのdiffやコミットメッセージです。当時のコミットを参照すれば、あなたの知りたかった情報が見つかるかもしれません。コミット履歴はそのプログラムの歴史そのものであり、場合によっては古文書のような役割を担います。
あなたがコミットするのはその一瞬だけですが、その一瞬のコミットがその後ずっと保持され、忘れた頃に他の誰かがあなたのコミットを参照しに来るかもしれません。未来の開発者に感謝されるような、そんな最高のコミットを毎回決めましょう!
では、上で説明したユースケースを踏まえて、良いコミットの条件とは何かを考えてみましょう。
変更を取り消したいのでここまでロールバックしたい、という場合に特定のコミットが参照されます。せっかくコードを元に戻したのに、実はその時点のコードだとプログラムが正常に動作しない、ということでは安心してコードをロールバックできません。ですので、コミットする場合はその時点でプログラムが正常に動作するかどうか確認するようにしましょう。
自動テストが整備されているプロジェクトであれば、全部のテストがパスすることが最低限の必須条件になります。
自分がアサインされたタスク(新機能の追加等)を対応している最中に「あれ、ここのコード、なんかインデントが変だぞ?」と思っても、そのコードがタスクと無関係な既存のコードなのであれば、その場で修正するのはNGです。なぜなら、新機能の追加とインデントの修正は目的が別々だからです。異なる目的の修正を1つのコミットにまとめてしまうと、新機能をrevertしたいと思ったりしたときにインデントの修正も一緒にロールバックされてしまいます。
そもそも目的が異なるなら新機能の追加とインデントの修正はコミットレベルではなく、タスクレベルで分割すべきです。インデントの修正は別タスクとして別ブランチでコードを修正し、独立したプルリクエストをつくるようにしましょう。
自分にアサインされたタスクが大きい場合は、「すべての要件を全部実装し終えてからコミットする」という考え方だと、何日にも渡ってコミットできない日々が続くかもしれません。そういうときはキリのいいタイミングで逐次コミットするようにしてください。そうしないと、「やっぱりさっきの状態に戻したい」と思ってもさっとロールバックできなくなります。
何を持って「キリが良い」と見なすかはケースバイケースですが、たとえば「エンドユーザー画面だけでなく、管理者画面の開発も必要」「エンドユーザー画面は正常系だけでなく異常系の処理も考慮しないといけない」といった開発タスクであれば、以下のようなタイミングでコミットするのが良いかもしれません。
筆者の経験上、初心者の人ほどコミットしていない変更を大量に何日も溜め込む傾向があるように思います。「もう3日以上コミットしてない」とか、「コミット待ちのファイルが30個を超えている」といった場合はおそらく黄色信号です。キリがいいタイミングを自分で見極められないなら、先輩プログラマに「すいません、今こんな状態なんですが、どこでコミットしたらいいでしょうか?😭」と相談しましょう!
プログラミング初心者の人はコミットメッセージの重要性を理解していない人が多いせいか、結構いい加減なコミットメッセージを付ける人をよく見かけます。
筆者は過去にコミット一覧を開いたら最初から最後まで “Initial commit” というコミットメッセージが並んだGitHubリポジトリを見たことがあります。”Initial commit”って「最初のコミット」っていう意味ですからね。2回目以降のコミットは”Initial commit”じゃないですよ!
そこまで極端ではなくても
・・・というように、全く同じコミットメッセージが3つ4つ連続しているようなケースはまあまあの頻度で見かけます。これだと1件目の「something.rbの修正」と2件目の「something.rbの修正」はどう違うのか、いちいちコミットの詳細を確認しないと判別できません。そうではなく、どこをどう直したのか、もう少し具体的な内容を書きましょう。たとえば、
・・・といった具合です。
アサインされたタスクの開発とリリースが終わって「ふう、やれやれ」と思っても、それで全部おしまい、というわけではありません。もしかすると数年後に別の開発者がなぜこんな変更を入れたのかを知りたがって、git blameコマンドであなたのコミットを調査しにくるかもしれません。そんなときのために、コミットメッセージに変更の背景に関する説明や、開発チケットのURL(IssueのURL)を含めておきましょう。こうすれば、数年後にやってきた別の開発者(もしかすると未来のあなた自身かも?)の「なぜ?」という疑問に答えやすくなります。
ちなみに、gitでは複数行のコミットメッセージを書けることをご存じでしょうか?git commit -m "(メッセージ)"
ではなく、ただのgit commit
を実行するとテキストエディタ(Vim等)が起動します。1行目にはいつも通り変更の概要を簡潔に記述しましょう。そして2行目以降に背景の説明や開発チケットのURLを記述してください。こうすれば長文のテキストも無理なく記述することができます。
以下の画像はコミットメッセージの2行目にGitHub issueのURLを記入する例です。
ただし、背景の説明や開発チケットのURLを毎回書く必要はありません。軽微なtypoの修正にいちいち詳細な説明を書くのはやりすぎです。背景を記述するのは新機能の追加や不具合の修正など、ある程度まとまった単位のコミットに限定して構いません。「このコミットには詳細な情報を残した方が未来の開発者に役立ちそうかどうか?」を自問して、その答えが”YES”だった場合に、詳しい背景を残すようにしましょう。
コードレビューで挙がってきた指摘事項に従ってコードを修正した場合、そのコミットメッセージをみなさんはどんなふうに書きますか?
プログラミング初心者の人の中にはときどき「コードレビューの修正」みたいなコミットメッセージを書く人がいます。ですが、筆者はこれはあまりよくないコミットメッセージだと考えています。なぜなら、「コードレビューの修正」では何をどう変更したのか、コミットメッセージだけ読んでも想像がつかないからです。
そもそも「コードレビューを受けて、その指摘事項を対応した」というのは変更の背景に該当するはずです。もしそれを書くならコミットメッセージの2行目以降が良いでしょう。ですので、次のように「1行目に具体的な修正内容」「2行目以降にコードレビューの対応であること」を書く方が良いと筆者は考えます。
fooメソッドをprivateメソッドに変更 下記レビューコメントの対応 https://github.com/your/repository/pull/1430#discussion_r1825434161
とはいえ、数年経ってしまえば「Aさんは自分の意思でそのコードを書いたのか、それともレビュアーの指摘に従ってコードを書き替えたのか」という事実は大した問題ではなくなります。よって、多くの場合は「fooメソッドをprivateメソッドに変更」の1行だけでも十分でしょう。
fooメソッドをprivateメソッドに変更
ただし、プルリクエストのコメント欄で長い議論を繰り広げた末に「この実装でいく」と決めたような場合は話が別です。そういう場合は背景をしっかりコミットメッセージに残しておきましょう。
fooメソッドのパフォーマンス改善 このアルゴリズムを採用することになった背景については下記PRコメントを参照。 https://github.com/your/repository/pull/1430#discussion_r1825434161
プログラミング初心者の人はgit addして即git commitしてる人が多いかもしれません。しかし、これは危険です。なぜなら、自分のうっかりミスで無関係なdiffや、不要なデバッグ用コードが紛れ込んでいる可能性があるためです。
こうしたトラブルを防ぐために、git commitする前に、git diffでどんなコードがコミットされるのかをセルフレビューするようにしましょう。
以下はgit diffを使ってこれからコミットするコードのdiffを確認する様子です。
コミット前にセルフレビューすると「しまった、デバッグ用のログ出力が残ったままになっていた!」というふうに、しょうもないうっかりミスを発見することがよくあります。きれいなコミットを作成するために毎回必ずセルフレビューを実施し、「これで完璧!」と太鼓判を押せる状態にしてからコミットするようにしましょう。
というわけで、本記事ではプログラミング初心者の方を対象に、コミットが活用される場面を想定しながら良いコミットと良くないコミットについてあれこれ説明してみました。今まで適当にコミットをしてた人も本記事の内容を参考にして、しっかりとした意図や目的を持ってコミットのタイミングやコミットメッセージを考えられるようになってもらえると嬉しいです。
なお、「どんなコミットが良いコミットなのか?」という問いに対する答えは、開発チームごとにある程度変わってくるため、本記事で書いた内容が全ていつでもどこでも正しい、とは限りません。もしご自身のチームの考えにそぐわない部分があれば「この記事にはこう書いてあるけど、ウチのチームではこうしようね」と、チーム内の意識を揃えておくと良いでしょう。
さて、今回はコミットに焦点を当てましたが、おそらく業務ではコミットだけでなく、プルリクエストを作成する機会もきっと多いはずです。そこで次回は良いプルリクエストと悪いプルリクエストについてあれこれ考えてみたいと思います。次回もどうぞお楽しみに!
関連記事
人気記事