12周年記念パーティ開催! 2024/5/10(金) 19:00

Web制作者のための実践Git 第1回 適切な履歴の作り方

すでにGitを使っている人を対象に、Gitの使いこなしを解説します。第1回目は適切な履歴とは何か、また、その作り方に焦点を当てます。

発行

著者 外村 和仁 フロントエンド・エンジニア
Web制作者のための実践Git シリーズの記事一覧

はじめに

このシリーズでは、Gitを普段から使っているが、もっと便利に使いこなしたいという方*に向けて、Gitをさらに使いこなすための機能や、考え方について解説します。

*注:Gitを使ったことがない方

このシリーズでは、Gitの初歩的な使い方については詳しく触れていません。Gitを使ったことがないという方は、「Web制作者のためのGit入門」シリーズをご一読ください。

Gitの操作は、基本的にはターミナルでコマンドラインから行うことを前提としますが、SourceTreeなどのGUIツールを使っている方にとって役に立たないかというと、そんなことはありません。

GUIのツールは基本的にコマンドラインでできることに対して、直感的に操作できるインターフェースを提供しているだけですから、コマンドラインでできることを学べば、GUIをより便利に使えるようになるはずです。

本シリーズでは次のような内容を扱う予定です。

  • 適切な履歴を作るコツ
  • 間違った変更をもとに戻す方法
  • ブランチの運用
  • コンフリクトの解消方法
  • リモートとの連携

第1回目のこの記事では、適切な履歴を作るためのテクニックや考え方について解説していきます。

適切な履歴とは

Gitはバックアップを行ったり、ファイルを共有するためのツールではありません。副次的な機能として、そのようなこともできますが、Gitのもっともコアな機能はソースコードなどの履歴を記録し、それを追跡できるようにすることです。

このコアの機能を有効に使うためには、ひとつひとつの履歴(コミット)において「いつ」「だれが」「なぜ」「何を」変更したのかがわかることが重要です。これらすべてが適切に設定されているものを、今回は「適切な履歴」と呼ぶことにします。

「いつ」はコミットした日付なのでGitが自動的に設定してくれますし、「だれが」は最初に自分の名前とメールアドレスを設定すれば、あとはそれを使うので特に考えることはありません。

考える必要があるのは「なぜ」と「何を」です。「なぜ」はコミットメッセージ、「何を」はコミットの粒度を指すと考えてください。コミットの粒度とは、ひとつのコミットに含まれる変更の度合いや規模のことです。

これらが適切になっていないと、Gitはただのバックアップツールになってしまいます。

ひとりでGitを使う場合は単なるバックアップツールとして使ってもよいかもしれません。けれど、複数人で開発を行う場合は、過去の履歴から知ることができる情報の適切さによって開発効率が大きく変わってきます。

過去の履歴から読み取りたい情報は、たとえば、以下のようなものがあるでしょう。

  • だれがどのような理由でこのロジックを変更したかを知りたい
  • なぜ以前まであったメソッドが削除されたかを知りたい
  • いつ、どのような変更が原因でバグが発生したかを突き止めたい

適切な履歴を作るというのは、上記のような、履歴から得られる情報に対してすばやく正確にアクセスするための手段なのです。

良いコミットメッセージ、悪いコミットメッセージ

まずは「なぜ」その変更を行ったかを記録するためのコミットメッセージについて解説します。Gitを使っているユーザーであれば普段からコミットメッセージは書いているはずです。

コミットメッセージは、変更履歴をあとからみたときに、どのような変更を行ったのかをすばやく認識するためのものです。したがって、どのような変更だったのかが一目で見てわからないコミットメッセージはあまり意味のないものになります。

たとえば次のようなコミットメッセージは悪い例です。

バグ修正

このコミットメッセージは何のバグをどのように修正したのか、まったくわからず、結局変更のdiffを見ることになるため、コミットメッセージとしてはほとんど意味がありません。次のようにすればどうでしょう。

User APIのageフィールドの値がない場合にエラーになるバグを修正

これであればひと目で、どのような修正を行ったのかがわかります。これは良いコミットメッセージといえます。

次に以下の例を見てください。

Ajaxのタイムアウト値を5000msから10000msに変更

これは「何を」変更したのかが書かれていて悪くはないのですが、もっと詳しく書くことでコミットメッセージとしての価値がより上がります。この例が良くない点は「なぜ10000msにしたか」ということが書かれていないことです。

このような場合、1行目に概要を書き、ひとつ空行を置いて、3行目からその詳細を書くという、Gitの標準的なコミットメッセージのフォーマットで書くのがよいでしょう。たとえば次のように書きます。

Ajaxのタイムアウト値を5000msから10000msに変更

5000msでタイムアウトすると、モバイルなどの3G環境下でタイムアウトが
頻発するため10000msに変更した。

このように書いてあればコミットメッセージをみただけで、「なぜ変更したのか」がすぐわかります。

他の人が見た時にコミットメッセージを見るだけで、変更の内容がおおむね把握できることを心がけて書くといいかもしれません。ひとりで開発している場合でも、未来の自分は変更を覚えていない可能性が高いので、そういったケースでも有効になるはずです。

コラム:チケットとコミット

バージョン管理と併用して、RedmineBacklogGitHub Issuesなどのチケット管理を利用しているプロジェクトは多いと思います。それらのチケットとコミットを紐付けることによって、どのチケットに対するコミットかを明確にし、あとから見るときにより多くの情報を提供することができます。

たとえばGitHub Issuesでは、コミットメッセージに#チケット番号と記述することでチケットとコミットが双方から参照できるようになります。

仮にチケット番号100のIssueがあったとすると、コミットメッセージには次のように書きます。

機能Aの追加 #100

GitHub上でコミットメッセージを見ると#100の部分にチケットページへのリンクが張られ、チケットのページにはコミットメッセージに#100とつけられたコミットがすべて表示されます。

次のスクリーンショットは、コミットされたコードに紐づいたIssueです。Issueの画面のコミットが表示されており、コミットをクリックすると、そのコードを見ることができます。

次のスクリーンショットは、紐づいたコードです。コミットメッセージにある#20はIssueの番号で、クリックすると、そのIssueを見ることができます。

コミットの粒度の適切さ

次に、ひとつのコミットで「何を」変更したかを適切に記録するためにはどうすればいいのかを見ていきましょう。何を変更したかを適切に記録するということは、ひとつひとつのコミットの粒度が適切であるということです。

たとえば次のようなコミットはよくありません。

機能Aの追加と機能Bのバグ修正

「機能Aの追加」と「機能Bのバグ修正」という2つの変更がひとつのコミットに混ざっています。基本的にひとつのコミットに2つ以上の変更を含めるべきではありません。

たとえば、あとからやはり機能Aは必要なかったから元に戻したいとなった場合、「機能Aの追加」と「機能Bのバグ修正」というコミットが別々になっていれば、git revertコマンドで「機能Aの追加」のコミットを取り消すことができます。

しかしこの2つの変更が同じコミットになっていると、「機能Aの追加」だけ元に戻したいのに、「機能Bのバグ修正」まで元に戻ってしまいます。このように、Gitの機能の恩恵をより多くうけるために、ひとつのコミットを適切な粒度で行う必要があるのです。

次に、以下のようなコミットがあったとします。

検索機能の実装

このコミットにはサーバーサイドのAPIの実装からJavaScript、HTML、CSSのフロントエンド実装のすべてが含まれていたとしましょう。たしかにひとつのコミットで、ひとまとまりの変更という原則は守られていますが、このコミットは粒度が大きすぎます。このような比較的大きな機能の実装はコミットではなく、ブランチにするのが適当な粒度です。

こういったケースにおいて、「このような単位でコミットするべきである」というベストプラクティスがあるわけではありませんが、「Entryモデルのsearchメソッド実装」など、ひとつひとつの小さい機能ごとにコミットを作っていくのがベターではないかと思います。

コミットの粒度に関しても、コミットメッセージの場合と同じように、あとから他の人が見て変更の意図がわかりやすく、履歴として意味のあるものにすることを心がけることが大切です。

git add -p

ここで適切な粒度でコミットするためのテクニックのひとつを紹介します。たとえば、次のようなHTMLがあったとします。

<ul>
  <li><a href="/">Home</a></li>
  <li><a href="/about.html">About</a>
  <li><a href="/help.html">Help</a></li>
</ul>

このHTMLに対してコンテンツ部分の追加を行いました。さらに、コンテンツ部分を追加している間にナビゲーションのAboutの部分だけ</li>の閉じタグがないことに気づき、その修正も行いました。その結果、次のようなコードになりました。

<ul>
  <li><a href="/">Home</a></li>
  <li><a href="/about.html">About</a></li>
  <li><a href="/help.html">Help</a></li>
</ul>

<div class="content">
  contents...
</div>

これでコードとしてはよさそうですが、diffは次のようになります。

$ git diff
diff --git a/sample.html b/sample.html
index 9c91139..04de33d 100644
--- a/sample.html
+++ b/sample.html
@@ -9,10 +9,14 @@

 <ul>
   <li><a href="/">Home</a></li>
-  <li><a href="/about.html">About</a>
+  <li><a href="/about.html">About</a></li>
   <li><a href="/help.html">Help</a></li>
 </ul>

+<div class="content">
+  contents...
+</div>

このままコミットしてしまうと、「リスト部分の閉じタグ忘れ修正」と「コンテンツの追加」という2つの変更がひとつのコミットになってしまいます。しかし、この2つは別々のコミットにしたいところです。

さて、どのようにしたらいいでしょうか。

いったんコンテンツ部分を削除して、閉じタグ忘れの変更だけをコミットし、さらにコンテンツ部分をもとに戻すというのでもできなくはありませんが、Gitはもっとスマートな方法で解決できます。

git add -pというコマンドを使うと、同じファイルであっても、変更を選択してステージング(インデックスに追加)することができます。コマンドを実行すると次のような画面になります。

$ git add -p
diff --git a/sample.html b/sample.html
index 0bb7f2e..3c1f17e 100644
--- a/sample.html
+++ b/sample.html
@@ -1,5 +1,9 @@
 <ul>
   <li><a href="/">Home</a></li>
-  <li><a href="/about.html">About</a>
+  <li><a href="/about.html">About</a></li>
   <li><a href="/help.html">Help</a></li>
 </ul>
+
+<div class="content">
+  contents...
+</div>
Stage this hunk [y,n,q,a,d,/,s,e,?]?

ここで何かキー入力することで、この変更をどうするか選択することができます。選択できるキーは一番下に表示されているもので、それぞれ次のような意味があります。

キー 意味
y この変更をステージングする
n この変更をステージングしない
q 終了する
a これ以降の変更をすべてステージングする
d これ以降の変更をすべてステージングしない
/ 正規表現で変更内容の検索を行う
s この変更をさらに分割する
e この変更の分割をエディタを起動して手動で行う
? ヘルプを表示する

上記の例の場合、変更した箇所が近いため同じhunk(変更の塊)として表示されています。ですので、hunkをさらに分割する必要があります。そのためにはsを選択します。そうすると次のような表示になります。

Split into 2 hunks.
@@ -1,5 +1,5 @@
 <ul>
   <li><a href="/">Home</a></li>
-  <li><a href="/about.html">About</a>
+  <li><a href="/about.html">About</a></li>
   <li><a href="/help.html">Help</a></li>
 </ul>
Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]?

変更が分割されて閉じタグ忘れだけの変更が表示されています。ここでyを押してこの変更をステージングします。すると次は以下の表示になります。

@@ -4,2 +4,6 @@
   <li><a href="/help.html">Help</a></li>
 </ul>
+
+<div class="content">
+  contents...
+</div>
Stage this hunk [y,n,q,a,d,/,K,g,e,?]?

こんどはコンテンツの追加ですから、ここではnを選択してこの変更はステージングしません。そうすると、同じファイルに対して同時に2つの変更を行ったにもかかわらず、一部の変更だけがステージングされている状態になります。

この状態でgit commitでコミットすれば、閉じタグの修正だけがコミットされます。あとは残りのコンテンツ追加をコミットすれば、無事2つのコミットとして履歴に記録されます。

git add -pコマンドを使うと複数の変更を行ったあとでも、適切な粒度でコミットを作ることができるので、とても便利です。

まとめ

今回はコミットメッセージの書き方や、コミットの粒度について解説しました。コミットメッセージやコミットの粒度を適切にするのは、「では今日からやってください」といわれて、いきなりできるような簡単のものではありません。

それは経験を通して、だんだんと身につくスキルだと思います。慣れは必要ですが、適切な履歴を作ることの恩恵はとても大きいものがあるので、このスキルを身につけておいて損はないはずです。そのためには日頃から「このコミットメッセージは適切か?」「このコミットの粒度はこれでいいのか?」と意識する必要があります。

しかし、必要以上に堅苦しく考える必要はありません。というのも、今回解説したような適切な履歴は、あくまでリモートリポジトリに公開するときにそうなっていればいいのです。

Gitではローカルでコミットしたあとに、履歴を変更するということもできます。つまり、リモートリポジトリに公開するまでは、履歴の作り直しができるということです。

次回はそのような履歴を修正するためのgit rebase -iについて解説する予定です。