Git基礎 #2 add・commit・status — ステージングエリアの意味

読了 8分

前回の記事でGitの2つの核心モデルを立てました。コミットはスナップショットであり、ファイルはworking directory、staging area、repositoryの3つの領域を通って記録されます。今回はそのモデルを手を動かして確認する回です。前回作ったhello-gitリポジトリに実際のファイルを作り、status、add、commit、diffを順に実行して、最後に.gitignoreで記録しないファイルを整理します。

全6回で構成します。

  • #1 Gitとは — スナップショットモデルと3つの領域
  • #2 add・commit・status — ステージングエリアの意味 ← この記事
  • #3 ブランチとマージ — fast-forwardと3-way
  • #4 リモートリポジトリ — clone・fetch・pull・pushとoriginの正体
  • #5 取り消し — restore・reset・revertの使い分け
  • #6 GitHubと初めてのPull Request

この記事を終えると、Gitの最も基本のループである修正して、選んで、記録するという1サイクルを自分で回せるようになります。

最初のファイルを作る #

hello-gitディレクトリから始めます。ファイルを1つ作ります。

最初のファイルを作る
cd hello-git
echo "print('hello git')" > app.py

ファイルを作っただけで、Gitコマンドはまだ何も実行していません。いまGitがこの状況をどう見ているのかから確認します。

git status — 状態の読み方 #

git statusは3つの領域の現在の状態を要約してくれるコマンドです。今後どんな作業をするにしても、最も頻繁に実行することになります。

git status — untracked状態
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	app.py

nothing added to commit but untracked files present (use "git add" to track)

app.pyUntracked filesの下にあります。untrackedは、working directoryには存在するもののGitがまだ追跡していない新しいファイルという意味です。Gitは指示なしにどんなファイルも勝手に記録しないので、新しいファイルはすべてuntrackedから出発します。

親切なことに、出力の中に次のコマンドが書いてあります。git addでこのファイルを追跡対象に上げます。

ステージング
git add app.py
git status
git status — staged状態
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   app.py

今度はChanges to be committedの下に移りました。staged状態、つまり次のコミットに含まれるリストであるstaging areaに上がった状態です。まだ記録されたわけではなく、記録される予定の状態です。

3つ目の状態であるmodifiedはコミットを一度作った後に登場するので、先にコミットを作ってから戻ってきます。

git commit — 最初のスナップショット #

最初のコミット
git commit -m "Add greeting script"
# [main (root-commit) 3f2b1a9] Add greeting script
#  1 file changed, 1 insertion(+)
#  create mode 100644 app.py

-mの後ろの文字列がコミットメッセージです。このコミットで、staging areaにあった内容がスナップショットとしてrepositoryに永久に記録されました。出力の3f2b1a9はコミットを指す固有の識別子(ハッシュ)の先頭部分で、実行するたびに違う値になります。

コミットメッセージは最初から習慣を作っておくのがよいです。基本原則は2つです。

  • 件名1行で何をしたのかを要約します。「修正」「作業中」のようなメッセージは、履歴を照会するときに何の情報も与えません。
  • コミット1つには1つの変更だけを入れます。バグ修正と新機能が1つのコミットに混ざると、後から片方だけ取り消すのが難しくなります。#5でこの原則の価値をそのまま体感することになります。

次に、ファイルを一度修正して3つ目の状態を確認します。

コミット済みファイルの修正
echo "print('bye git')" >> app.py
git status
git status — modified状態
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   app.py

no changes added to commit (use "git add" and "git commit -a" to use)

Changes not staged for commit、つまりmodified状態です。Gitが追跡中のファイルが最後のコミット以降に変わったものの、まだstaging areaには上がっていないという意味です。整理すると、ファイルの状態はこう循環します。新しいファイルはuntrackedから始まり、git addを通るとstagedになり、git commitで記録されるとクリーンな状態に戻ります。もう一度修正するとmodified、もう一度addするとstagedになる循環です。

注記
1つのファイルがstagedでありながら、同時にmodifiedでもあるという状態もあり得ます。addした後に同じファイルをまた修正すると、addした時点の内容はstaging areaに、その後の修正はworking directoryに残り、statusに2行で表示されます。staging areaがファイルではなくaddした瞬間の内容を保持しているという事実を示す良い例です。

ステージングエリアはなぜ存在するのか #

直した内容を全部記録すればよさそうなのに、なぜaddという中間段階があるのでしょうか。答えはコミットの単位を設計するためです。

実際の作業では、working directoryには性格の違う変更が混ざりがちです。バグを直している途中に目についたタイポも一緒に修正し、設定ファイルにも手を入れたとすると、この3つを1つのコミットに入れた瞬間、「1コミットに1変更」の原則が崩れます。ステージングエリアがあれば、バグ修正のファイルだけaddしてコミットし、タイポ修正だけaddしてコミットするという選んで詰めるやり方ができます。履歴が変更単位できれいに分かれ、取り消し(#5)もレビューもずっと楽になります。

ファイル単位よりさらに細かく選ぶこともできます。git add -pは1つのファイルの中の変更をかたまり(hunk)単位で見せながら、1つずつ確認してきます。

変更をかたまり単位で選んで詰める
git add -p app.py
# @@ -1 +1,2 @@
#  print('hello git')
# +print('bye git')
# (1/1) Stage this hunk [y,n,q,a,d,e,?]? y

yは詰める、nはスキップです。1つのファイルに2種類の作業が混ざったときに便利です。

git diff — 何が変わったのか #

statusがどの領域に何があるかを教えてくれるとすれば、diffは内容がどう変わったのかを見せてくれます。比較対象が違う2つの形を区別する必要があります。

コマンド比較対象答える質問
git diffworking directory ↔ staging areaまだaddしていない修正は何か
git diff --stagedstaging area ↔ 最後のコミットいまコミットしたら何が記録されるか

コミット直前にgit diff --stagedで記録される内容を目で確認する習慣をつけると、意図しない変更が紛れ込む事故をほとんど防げます。

残りの修正をコミットして履歴を確認します。

コミットと履歴照会
git add app.py
git commit -m "Add farewell line"
git log --oneline
# 8c4d2e7 (HEAD -> main) Add farewell line
# 3f2b1a9 Add greeting script

git logはコミットの連鎖を新しい順に表示します。--onelineは1コミットを1行に要約するオプションで、前回描いたスナップショットの連鎖が実際に積み上がっていることをこの出力で確認できます。

.gitignore — 記録しないファイル #

プロジェクトフォルダには、履歴として残す必要がないファイルや、残してはいけないファイルも生まれます。代表的には3種類です。

  • 作り直せるもの — ビルド成果物(dist/build/)、キャッシュ(__pycache__/
  • ダウンロードし直せるもの — 依存関係のディレクトリ(node_modules/.venv/
  • 絶対に共有してはいけないもの — 秘密鍵、トークン、ローカル環境設定(.env

プロジェクトの最上位に.gitignoreというファイルを作ってパターンを書いておくと、該当ファイルはuntrackedの一覧にも現れず、git add .のような一括コマンドにも巻き込まれません。

.gitignoreの例
# 依存関係
node_modules/
.venv/

# ビルド成果物とキャッシュ
dist/
build/
__pycache__/

# 秘密鍵とローカル環境
.env
*.pem

# OS、エディタの副産物
.DS_Store

.gitignore自体はコミットしてチーム全体で共有するファイルです。1つ注意点があります。すでに追跡中のファイルは、後から.gitignoreに追加しても追跡され続けます。この場合はgit rm --cached <ファイル>で追跡だけ解除してからコミットします。

ヒント
秘密鍵は一度でもコミットされると、後のコミットで消しても履歴の中にそのまま残ります。コミットする前に.gitignoreへ先に登録するという順序を習慣にしておくのが、最も確実な予防です。

まとめ #

今回でGitの基本ループを1サイクル完走しました。

  • git status — untracked、staged、modifiedの3つの状態を読みます。
  • git add — 次のコミットに入れる変更を選んでstaging areaに上げます。ステージングエリアの存在理由はコミット単位の設計です。
  • git commit — staging areaの内容でスナップショットを記録します。1コミットには1つの変更だけを入れます。
  • git diffgit diff --staged — addの前とコミットの前に内容を確認します。
  • .gitignore — 成果物、依存関係、秘密鍵を履歴から除外します。

次回の「Git基礎 #3 ブランチとマージ — fast-forwardと3-way」では、Gitのスナップショットモデルが真価を発揮するブランチを扱います。ブランチがなぜポインタ1つにすぎないのか、二手に分かれた作業がどのように再び合流するのかを確認します。

X