コンテンツへスキップ

Gitのステージングエリアとは何か — git add と git commit の間に何が起きているか

ファイルを編集したら git add . して git commit -m "..." する——多くの人がこの2つのコマンドを、実質1セットの操作として覚えている。しかし git addgit commit はなぜ分かれているのか、git add した後に改めてファイルを編集したらどうなるのか、と聞かれると答えに詰まる人も多いはずだ。今回は Git の「ステージングエリア」という、2つのコマンドの間に挟まっている仕組みを整理する。

3つのエリアという考え方

補足: Git を理解する上でよく使われるのが「作業ディレクトリ(working directory)」「ステージングエリア(staging area / インデックス)」「リポジトリ(repository)」の3層モデルだ。ファイルの変更は、この3つを順番に経由してはじめて履歴として記録される。

作業ディレクトリは、エディタで実際にファイルを開いて編集している場所そのものだ。ここでの変更は、Git 目線ではまだ「ただのファイルシステム上の差分」でしかなく、履歴には一切影響しない。

リポジトリは git log で見える、確定済みのコミット履歴だ。一度コミットされた内容は、明示的に git commit --amendgit reset のような操作をしない限り書き換わらない。

その中間に位置するのがステージングエリアだ。作業ディレクトリの変更のうち「次のコミットに含めたい部分」だけを、一時的に置いておく場所という位置付けになる。git add は作業ディレクトリの変更をステージングエリアへコピーする操作であり、git commit はステージングエリアの中身を確定させてリポジトリに記録する操作、という役割分担になっている。

git add が実際にしていること

git add を実行すると、Git は内部で2つのことを行っている。1つは、変更したファイルの中身を「blob(ブロブ)」というオブジェクトとして .git/objects/ 以下にハッシュ値付きで保存すること。もう1つは、.git/index というバイナリファイルに「このパスは今このハッシュ値の blob を指す」という対応関係を書き込むことだ。

ここで重要なのは、.git/index に記録されるのはあくまで git add を実行した瞬間のファイルの中身のスナップショット であるという点だ。git add file.txt を実行した後にもう一度 file.txt を編集すると、作業ディレクトリのファイルは変わるが、ステージングエリアに記録されている内容(=インデックスが指している blob)は古いままになる。この状態で git status を実行すると、同じファイルが「ステージ済みの変更」と「未ステージの変更」の両方に同時に表示されることがある。これは Git のバグではなく、ステージングエリアが「時点を固定したコピー」であることの直接的な帰結だ。

$ echo "v1" > file.txt
$ git add file.txt
$ echo "v2" > file.txt
$ git status
Changes to be committed:
        modified:   file.txt      ← インデックス上は v1 のまま

Changes not staged for commit:
        modified:   file.txt      ← 作業ディレクトリは v2

git commit が実際にしていること

git commit はステージングエリア(インデックス)の中身をもとに「tree オブジェクト」を作り、それを親コミットへのポインタとメタ情報(作者・日時・コミットメッセージ)と一緒に「commit オブジェクト」としてまとめる操作だ。ここで参照されるのは常にインデックスの中身であり、作業ディレクトリの現在の状態ではない。git add していない変更や、git add した後にさらに書き換えた差分は、コミットに一切含まれない。

git commit -a はこの2段階をあたかも1段階であるかのように見せるショートカットで、内部的には「Git がすでに追跡しているファイル(新規作成ファイルは除く)」に限って自動で git add を先に実行してからコミットしている。裏で自動 add が起きていることを理解していないと、「新しく作ったファイルだけコミットに含まれない」という現象に遭遇して戸惑うことになる。

なぜわざわざ2段階に分かれているのか

作業ディレクトリの変更をそのままコミットする一段階の仕組みでも、バージョン管理としては成立する。それでもステージングエリアという中間層が存在するのは、「1回のコミットに何を含めるか」を編集の粒度とは独立にコントロールできるようにするためだ。

典型的なのが、1つのファイルの中で意味の異なる2つの変更(バグ修正と、無関係なフォーマット調整)を同時に行ってしまった場合だ。git add -p file.txt を使うと、ファイル全体ではなく変更の塊(hunk)単位で「この部分だけステージする/しない」を選べる。これにより、バグ修正だけを独立したコミットとして記録し、フォーマット調整は別のコミットに分ける、という後からの整理が可能になる。ステージングエリアがなければ、編集後にファイルをコピーして手作業で差分を分割するような手間が必要になっていたはずだ。

もう1つの実用的な使い方が git diff --cachedgit diff --staged と同義)による事前レビューだ。git diff が「作業ディレクトリとインデックスの差分」を見せるのに対し、git diff --cached は「インデックスと直前のコミットの差分」、つまり これから実際にコミットされる内容そのもの を見せてくれる。git add した後、コミットボタンを押す前にもう一度この差分を確認する習慣があれば、意図しないデバッグ用の print 文や、コミットに含めるべきでないファイルの混入を、コミットが確定する前の最後の地点で捕まえられる。

実務での小さな習慣

複数人、あるいは AI がコミットを作る運用では、「変更したファイルをまとめて git add -A する」よりも、「意図したファイルだけを名前で指定して git add」した上で git status で内容を確認してからコミットする方が事故が少ない。git add -A はカレントディレクトリ以下の変更を無差別にステージするため、意図せず一時ファイルや、うっかり作業ディレクトリに置いてしまった認証情報のようなファイルまで拾ってしまう可能性がある。ステージングエリアという「コミット直前の一時置き場」があるおかげで、git add の後・git commit の前というタイミングで、こうした取りこぼしや混入を機械的にチェックする余地が生まれている。段階を分けること自体が、レビューのための足場になっている。

まとめ

git addgit commit が別コマンドになっているのは、単なる歴史的経緯ではなく、「作業中の変更」と「次のコミットに含める内容」を意図的に切り離すための設計だ。ステージングエリアは、その切り離しを実現するための一時的なスナップショット置き場であり、git add -p によるコミット単位の細分化や、git commit 直前の git diff --cached によるレビューは、いずれもこの中間層があるからこそ可能になっている。「とりあえず add して commit」で済ませている作業でも、この2段階が何を意味しているかを知っておくと、いざ変更を整理し直したいときの選択肢が広がる。