なぜ粒度の細かいstagingが重要なのか
よく作られたGitの履歴は物語を語ります。各commitは単一の論理的な変更を表します:バグ修正、リファクタリング、新機能。しかし、実際の開発はめったにそれほど整理されていません。バグを修正し、近くのタイポに気づき、フォーマットを調整し、小さなリファクタリングを始める──すべて同じファイル内で。ファイル全体を一度にstagingしてcommitすると、無関係な変更が混在したcommitになります。それはコードレビューを難しくし、bisectでのバグ追跡を難しくし、commit履歴のドキュメントとしての有用性を低下させます。
粒度の細かいstagingを使えば、同じファイル内で変更が入り交じっていても、作業を意味のあるcommitに分割できます。すべてを一度にcommitする代わりに、どの変更をまとめるかを正確に選べます。
Stagingの3つのレベル
ファイルレベルのstaging
最も基本的なレベルです。git addでファイル全体をstagingします:
git add src/auth.ts src/utils.ts
各ファイルに1つの論理的な変更しか含まれていない場合はこれで十分です。しかし、ファイルに複数の無関係な変更が含まれるようになると、ファイルレベルのstagingは粗すぎます。
Hunkレベルのstaging
Gitはファイルの変更を「hunk」に分割できます──変更のない行で区切られた、変更された行の連続したブロックです。git add -p(パッチモード)を使用すると、Gitは各hunkを1つずつ提示し、stagingするかどうかを尋ねます:
git add -p src/auth.ts
各hunkに対して、y(staging)、n(スキップ)、s(より小さなhunkに分割)、またはe(手動で編集)を選択します。これはファイルレベルのstagingからの大きな改善ですが、制限があります。hunkは近接性によって定義されます:2つの無関係な変更が近くにある場合(3行未満のコンテキストで区切られている場合)、Gitはそれらを1つのhunkにまとめます。それらを分離するにはパッチの手動編集が必要で、面倒でエラーが起きやすいです。
行レベルのstaging
行レベルのstagingは最も精密なオプションです。ファイルやhunkではなく、stagingする個々の行を選択します。12行目と15行目はstagingしたいが14行目はしたくない?できます。ファイル内で変更がどれだけ近接していても、各commitに何を含めるかを完全にコントロールできます。
コマンドラインでは、行レベルのstagingはgit add -pとeオプションを使ってパッチを手動で編集することを意味します。unified diffフォーマットを理解し、パッチを注意深く修正し、エラーを導入しないことを願う必要があります。機能しますが、ほとんどの開発者が定期的にやりたいことではありません。
GitSquidでの行レベルstaging
GitSquidは行レベルのstagingを視覚的でわかりやすくします。変更されたファイルを選択すると、diffビューアが変更されたすべての行を表示します。特定の行をstagingするには:
- 1行をクリックして選択します。
- Cmd+Click(Windows/LinuxではCtrl+Click)で複数の個別行を選択します。
- Shift+Clickで行の範囲を選択します。
必要な行を選択したら、stagingします。選択した行だけがインデックスに追加されます。残りは作業ディレクトリのstagingされていない変更として残り、別のcommitに含める準備ができています。
同じワークフローは逆方向でも機能します。すでにstagingした行をstagingエリアから除外したい場合は、stagingされたdiffでそれらを選択し、それらの行だけをstagingから除外できます。
内部での動作
個々の行をstagingすると、GitSquidはサーバー側で選択された変更のみを含むパッチを生成します。このパッチはGitのパッチメカニズムを使用してインデックスに適用されます。クライアント側でdiffを操作するのではなくサーバー側でパッチ生成を処理することで、結果は信頼性が高く、Gitが期待するものと正確に一致します。パッチを手動で編集する際に遭遇する可能性のあるフォーマットの問題やオフバイワンエラーがありません。
実践的な例
バグ修正とリファクタリングを分離する
リファクタリング中にバグに気づきました。修正が明白なのですぐに修正します。ファイルには2種類の変更が含まれるようになりました:リファクタリングとバグ修正。行レベルのstagingを使えば、バグ修正の行だけを選択し、「fix null check in auth validation」のような明確なメッセージでcommitし、リファクタリングは別途commitします。結果は、1つの混在したcommitではなく、2つのクリーンでレビューしやすいcommitになります。
デバッグコードとプロダクションコードを分離する
デバッグ中に実際のコード変更のすぐ隣にconsole.log文を追加しました。どの行がデバッグでどの行が本物かを思い出そうとする代わりに、プロダクションの変更のみを視覚的に選択し、stagingしてcommitします。デバッグ行は作業ディレクトリに残り、引き続き使用したり後で破棄したりできます。
レビュー用の部分的なcommitを準備する
大量の変更がありますが、一部の作業だけがレビューの準備ができています。行レベルのstagingを使えば、完成した部分をcommitしながら作業中のコードはcommitされないままにできます。pull requestは焦点を保ち、レビュアーは不完全なコードに目を通す必要がありません。
Commitをアトミックに保つ
アトミックなcommitとは、単一の完全な論理的変更を含むものです。テストに合格し、ビルドを壊さず、単体で理解できます。アトミックなcommitは単なる整理整頓の問題ではありません。実用的な利点があります:
- コードレビュー。レビュアーは各commitを独立して理解できます。「fix input validation」というタイトルのcommitでバリデーションロジックだけに触れているものはレビューしやすいです。「various changes」というタイトルのcommitでバリデーション、スタイリング、設定に触れているものはそうではありません。
- バグの追跡。何かが壊れたとき、
git bisectは問題を導入した正確なcommitを特定できます。アトミックなcommitは結果を意味あるものにします:バグを引き起こした特定の変更を見つけられ、無関係な変更の寄せ集めではありません。 - Revert。commitをrevertする必要がある場合、アトミックなcommitはきれいにrevertできます。混在したcommitは、実際には保持したかった変更までrevertすることを強いるかもしれません。
- ドキュメントとしての履歴。数ヶ月後、誰かがコードが存在する理由を理解するためにcommit履歴を読むとき、明確なメッセージのアトミックなcommitは有用な物語を語ります。
行レベルのstagingは、アトミックなcommitを実用的にするツールです。それがなければ、実際の乱雑な開発からクリーンなcommitを作成するには規律と手作業が必要です。それがあれば、プロセスは一緒に属する行を選択するだけのシンプルさです。
最高のcommitは、正確に1つのことを行うcommitです。粒度の細かいstagingは、作業ディレクトリが異なる物語を語っていても、それを実現するコントロールを提供します。