もはや私のお気に入りのGitコミットではない
6年前、David Thompsonは、同僚が書いた妙に細部まで凝ったコミットメッセージを称賛する「My favourite Git commit」という人気ブログ記事を書きました。当時この投稿を楽しく読み、良いコミットメッセージの手本として何人かのチームメイトに送りました。
役立つコミットメッセージの書き方について自分なりのガイドを作っていたとき、最近Thompsonの記事を読み返しました。Thompsonの記事がこれほど効果的な例である理由を説明しようとすると、説明できないことに驚きました。外部の観察者として読む分には面白かったのですが、優れたソフトウェアエンジニアリングの手本として正当化することはできませんでした。
Thompsonのお気に入りのコミット
当時、私を含むThompsonやほかの人々をこれほど魅了したコミットメッセージはこちらです。
エラーを修正するためテンプレートをUS-ASCIIに変換
機能ブランチで、
/etc/nginx/router_routes.confの内容と照合するテストをいくつか追加しました。bundle exec rake specまたはbundle exec rspec modules/router/specで実行すると問題なく動きました。しかし、bundle exec rakeとして実行すると、各shouldブロックが次のエラーで失敗しました。ArgumentError: invalid byte sequence in US-ASCII最終的に、
.with_content(//)マッチャーを外すとエラーが消えることがわかりました。specファイルにはおかしな文字はありませんでした。また、同じインタープリタで次のようにPuppetをrequireすると再現できました。rake -E 'require "puppet"' specこの特定のテンプレートは、コードベース内でエンコーディングが
utf-8と判定される唯一のファイルのようです。ほかはすべてus-asciiです。dcarley-MBA:puppet dcarley$ find modules -type f -exec file --mime {} \+ | grep utf modules/router/templates/routes.conf.erb: text/plain; charset=utf-8そのファイルをUS-ASCIIに戻そうとすると、問題の文字は空白のように見えるものであると判明しました。
dcarley-MBA:puppet dcarley$ iconv -f UTF8 -t US-ASCII modules/router/templates/routes.conf.erb 2>&1 | tail -n5 proxy_intercept_errors off; # Set proxy timeout to 50 seconds as a quick fix for problems # iconv: modules/router/templates/routes.conf.erb:458:3: cannot convert(手作業で)置き換えると、ファイルは再び
us-asciiと判定されます。dcarley-MBA:puppet dcarley$ file --mime modules/router/templates/routes.conf.erb modules/router/templates/routes.conf.erb: text/plain; charset=us-asciiこれでテストが動きます! 取り戻せない人生の1時間です..
この長い前置きのあとにThompsonが実際の差分を見せる、というのが「オチ」です。
そうです。このコミットメッセージは、1文字の空白の変更を説明するためだけに、6つの段落と5つのコードスニペットを使っています。
お気に入りと最良は違う
このコミットのどこに惹かれるのかは、すぐにわかります。
ほとんどの開発者なら、この変更は単に「空白文字を修正」と記録するでしょう。そのため、誰かがバグを調査して修正するまでの過程をここまで丁寧に説明しているのは、うれしい驚きです。
Thompsonが挙げた理由のすべてから見て、これは良いコミットメッセージです。検索可能な記録を作り、開発者のツールやプロセスに関する有用な知見を共有しています。
これはThompsonを、あるいは元のコミットの作者を攻撃するものではありません。Thompsonはこれが「最高の」コミットメッセージだとは一度も言っておらず、ただ自分のお気に入りだと言っただけです。
とはいえ今では、これを手本となるコミットメッセージとして使えない欠点が見えています。
最も重要な情報を最後に埋め込んでいる
Thompsonが最初にブログ記事を公開したとき、このコミットメッセージは冗長すぎるという批判が最も多く寄せられました。私はその批判を的外れだと感じました。
コミットメッセージの詳細な記述は、関連性がある限り有用です。Thompsonの詳細には関連性がありました。経験の浅いチームメイトが、作者のデバッグプロセスやツールセットを学ぶ助けになります。また、より経験豊富なチームメイトには、開発者が何かを見落としていないか、関連するツールを知らないのではないかを確認する機会も与えます。
Thompsonの例が冗長すぎると受け取られた理由は、最も重要な情報がコミットメッセージの奥深くに埋め込まれているからです。
最初の段落を読み直してください。
機能ブランチで、
/etc/nginx/router_routes.confの内容と照合するテストをいくつか追加しました。bundle exec rake specまたはbundle exec rspec modules/router/specで実行すると問題なく動きました。しかし、bundle exec rakeとして実行すると、各shouldブロックが次のエラーで失敗しました。ArgumentError: invalid byte sequence in US-ASCII
このコミットメッセージは3文とコードスニペットを読んでも、変更が実際に何をするのかについての情報がまだありません。
コミットメッセージでは、最も重要な情報を最初に示し、徐々に細かい詳細へ移るべきです。ジャーナリストはこれを逆ピラミッド型の文章スタイルと呼びます。
ジャーナリストは、最も多くの人に関連する情報を最上部に置く逆ピラミッド型でニュース記事を構成します。
コミット履歴をスクロールしているなら、各コミットが関連するかをすぐに知りたいはずです。コミットメッセージは最初から、変更の概要を示すべきです。
問題を最後まできちんと説明していない
Thompsonの例のコミットメッセージを最後まで読んで、変更内容を本当に理解できますか。
問題の説明に最も近い箇所はこちらです。
この特定のテンプレートは、コードベース内でエンコーディングがutf-8と判定される唯一のファイルのようです。ほかはすべてus-asciiです。
dcarley-MBA:puppet dcarley$ find modules -type f -exec file --mime {} \+ | grep utf modules/router/templates/routes.conf.erb: text/plain; charset=utf-8
メッセージではroutes.conf.erbがUTF-8エンコーディングであると言っていますが、その理由は説明していません。幸い、このプロジェクトはオープンソースなので、自分で調査できます。
問題はroutes.conf.erbの463行目にあります。
$ cat modules/router/templates/routes.conf.erb | head -n 463 | tail -n 1
# where civica QueryPayments calls are taking too long.
通常のテキストエディタやWebブラウザでは問題を見られませんが、xxdのようなツールで生のファイルバイト列をダンプすると、問題が見えます。
$ cat modules/router/templates/routes.conf.erb \
| head -n 463 | tail -n 1 \
| xxd | head -n 1
00000000: 2020 23c2 a077 6865 7265 2063 6976 6963 #..where civic
^^ ^^
US-ASCIIとUTF-8の表を暗記していない場合に備え、この行の先頭の数文字を示します。
| バイト表現 | テキスト表現 |
|---|---|
0x20 | ' '(スペース) |
0x20 | ' '(スペース) |
0x23 | '#' |
0xC2 0xA0 | ' '(UTF-8のノーブレークスペース) |
つまり、このファイルには0xC2 0xA0というバイト列がありました。0xC2と0xA0はいずれもUS-ASCIIのバイト範囲外にあるため、このファイルをUS-ASCIIファイルとすることはできません。
0xC2 0xA0という並びがある以上、routes.conf.erbを読み取るアプリケーションはUTF-8エンコーディングで解釈しなければなりません。UTF-8は、より新しく、より国際化に対応したテキストエンコーディング方式です。
ThompsonのコードベースではRuby 1.9.3が使われていました。これはUTF-8エンコーディングをサポートしていましたが、ファイルが明示的に別の指定をしていなければデフォルトはUS-ASCIIでした。
ソース履歴を掘り下げると、UTF-8文字を最初に導入したのはコミット5a8607だとわかりました。このコミットメッセージにはUTF-8文字を導入した理由への言及がないため、おそらく偶然だったのでしょう。
Hacker Newsのコメント投稿者は、なぜこの迷い込んだUTF-8文字がroutes.conf.erbに現れたのかについて、もっともらしい仮説を提示しました。
この不正な文字が入り込んだ原因として最も考えられるのは、#がOption-3(AltGr-3)、ノーブレークスペースがOption-Space(AltGr-Space)であるApple Ireland/UKキーボード配列を誰かが使っていたことです。
- Hacker Newsのmesse
コードに言及しているのにリンクしていない
Thompsonの例のコミットは、外部コードへの言及で始まります。
機能ブランチで、
/etc/nginx/router_routes.confの内容と照合するテストをいくつか追加しました。bundle exec rake specまたはbundle exec rspec modules/router/specで実行すると問題なく動きました。
しかし、このコミットメッセージはブランチ名もコミットハッシュも示していないため、読者には開発者の発見を再現する方法がありません。
後半で、コミットメッセージはこう述べています。
最終的に、
.with_content(//)マッチャーを外すとエラーが消えることがわかりました。specファイルにはおかしな文字はありませんでした。
コミットハッシュもリンクもなければ、読者には開発者がどのマッチャーやどのspecファイルを指しているのかわかりません。
コミットメッセージで外部コードに言及するなら、コードレビュー担当者や将来のメンテナーが変更の正確な文脈を確認できるよう、明示的にリンクすべきです。
私ならこう書き直す
Thompsonのお気に入りのgitコミットについて、私が提案する改訂版はこちらです。
routes.conf.erbテンプレートをUS-ASCIIに変換
routes.conf.erbには、5a8607でおそらく誤って導入された、余計なUTF-8文字があります。
rakeはUS-ASCII形式を想定しているため、routes.conf.erbにある1文字のUTF-8文字がrakeでのテスト失敗を引き起こします。この変更では、
rakeでのテスト失敗を防ぐため、UTF-8文字を同等のUS-ASCII文字に置き換えます。余計なUTF-8文字
問題は
modules/router/templates/routes.conf.erbの463行目にあります。$ cat modules/router/templates/routes.conf.erb \ | head -n 463 | tail -n 1 \ | xxd | head -n 1 00000000: 2020 23c2 a077 6865 7265 2063 6976 6963 #..where civic ^^ ^^
0xC2 0xA0は有効なUS-ASCIIバイト列ではありませんが、UTF-8のノーブレークスペース文字です。US-ASCIIエンコーディングを想定してこのファイルを読み取るツールは失敗します。これを見つけた経緯
機能ブランチで、
/etc/nginx/router_routes.confの内容と照合するテストをいくつか追加しました(abcd123を参照)。bundle exec rake specまたはbundle exec rspec modules/router/specで実行すると問題なく動きましたが、bundle exec rakeとしてテストを実行すると、各shouldブロックが次のエラーで失敗しました。ArgumentError: invalid byte sequence in US-ASCII最終的に、
.with_content(//)マッチャーを外すとエラーが消えることがわかりました。specファイルにはおかしな文字はありませんでした。同じインタープリタで次のようにPuppetをrequireすると、エラーを再現できました。rake -E 'require "puppet"' specこの特定のテンプレートは、コードベース内で
fileがutf-8と判定する唯一のファイルのようです。ほかはすべてus-asciiです。$ find modules -type f -exec file --mime {} \+ | grep utf modules/router/templates/routes.conf.erb: text/plain; charset=utf-8そのファイルをUS-ASCIIに戻そうとすると、問題の文字は空白のように見えるものであると判明しました。
$ iconv -f UTF8 -t US-ASCII modules/router/templates/routes.conf.erb 2>&1 \ | tail -n5 proxy_intercept_errors off; # Set proxy timeout to 50 seconds as a quick fix for problems # iconv: modules/router/templates/routes.conf.erb:458:3: cannot convert(手作業で)UTF-8文字を置き換えると、
fileは再びroutes.conf.erbをus-asciiと判定します。$ file --mime modules/router/templates/routes.conf.erb modules/router/templates/routes.conf.erb: text/plain; charset=us-asciiこれでテストが動きます! 取り戻せない人生の1時間です..
私が加えた変更は以下のとおりです。
- メッセージの早い段階に概要を加えました。
- UTF-8文字と、その由来についてより明示的な説明を加えました。
- 作者の元の内容の大半を「これを見つけた経緯」セクションに移し、追加で読む内容であることを明確にしました。
- 軽微な文法修正を行いました。
- 曖昧さを減らすため、受動態を取り除きました。
- 前者の大半はノイズなので、ターミナルプロンプトを
dcarley-MBA:puppet dcarley $から単に$へ簡略化しました。
重要なのは、詳細を削除していないことです。問題は冗長さではなく、開発者が情報をどう整理し、提示したかにありました。
自分自身の原則を定める価値
Thompsonの投稿を読み返したことで、ソフトウェアエンジニアリングの原則を自分で定義することには大きな価値があると改めて感じました。
私はThompsonが挙げた強みには同意していたため、このコミットを良い例として受け入れていました。コミットメッセージにとって最も重要な性質は何かを自分で定義して初めて、Thompsonの例の短所が見えました。
長年にわたり、いくつかの異なるソフトウェアエンジニアリングの実践について自分の考えを説明してきました。そうするたびに、より良い開発者になれます。当たり前だと思っている考えを批判的に考えるよう促され、常に実現できるとは限らなくても、自分の理想像を覚えておく助けになるからです。
さらに読む
- 「役立つコミットメッセージの書き方」 - 良いコミットメッセージを構成する要素についての、より詳しい私の説明です。
govuk-puppetプロジェクトからの抜粋の著作権はCrown Government Digital Serviceに帰属し、MIT Licenseに基づいて使用しています。
記事をランダムに読む
