.gitignore に書いたのに、そのファイルが毎回 git status に出てくる。原因はほぼ1つで、そのファイルが「もう追跡済み」だから。.gitignore は追跡していないファイルにしか効かない。
直し方は git rm --cached <file> の1コマンド。ファイルは手元に残したまま、Git の追跡だけ外れる。自分はこれを知らずに、.gitignore の書き方を何度も直して30分溶かした。書き方は合っていて、効く相手が違っただけだった。
TL;DR(30秒版)
- 症状:
.gitignoreに書いたのに、そのファイルがgit statusに出続ける・コミットされ続ける - 原因: そのファイルがすでに追跡済み(tracked)。
.gitignoreは未追跡ファイルにしか効かない - 直し方:
git rm --cached <file>でインデックスから外す → commit - デバッグ:
git check-ignore -v --no-index <file>でパターンが当たっているか確認
この記事は、.gitignore の書き方は合っているはずなのに無視されなくて詰まった人向け。Git を毎日触るけど内部の挙動までは追っていない、という層に刺さるはず。検証はすべて手元の git version 2.39.5 (Apple Git-154) で実際に動かした出力を貼っている。
.gitignore が効かないのは、書き方じゃなく「追跡状態」の問題
まず結論から。.gitignore に書いたのに無視されないとき、9割はパターンのミスじゃない。そのファイルがすでに Git の管理下に入っているのが原因。
.gitignore の役割は「まだ追跡していないファイルを、追跡の候補から外す」こと。裏を返すと、一度でも git add してコミットしたファイルには、あとから .gitignore に書いても何も起きない。公式ドキュメントにもはっきり書いてある。
An ignored file... will not be tracked. A file which is already tracked by Git is not affected.
(gitignore Documentation より)
「すでに追跡されているファイルには影響しない」。この1文を知っているかどうかで、詰まる時間が30分変わる。自分は知らなかった側だった。
再現:追跡済みファイルは .gitignore を素通りする
言葉だけだと信じにくいので、空のリポジトリで実際に再現する。よくある「うっかり config.env を先にコミットしてしまった」パターンをそのまま作る。
# 空のリポジトリを用意して、先に config.env をコミットしてしまう
git init -q
printf 'SECRET_KEY=abc123\n' > config.env # ダミーの秘密情報
git add config.env && git commit -q -m "add config.env"
# しまった、と気づいて後から .gitignore に追加
printf 'config.env\n' > .gitignore
git add .gitignore && git commit -q -m "add gitignore"
ここまでで「.gitignore に config.env と書いた」状態。普通の感覚だと、もう config.env は無視されると思う。だが変更を加えて git status を見ると、こうなる。
printf 'SECRET_KEY=changed\n' >> config.env # 値を書き換えてみる
git status --short
# M config.env ← しっかり "変更あり" として拾われる
.gitignore に書いたのに M config.env が出る。無視されていない。これが「書き方は合っているのに効かない」の正体で、原因はパターンではなく、config.env がすでに追跡済みだという一点にある。
git check-ignore の exit code を、たぶん誤読している
「本当にパターンが当たってないのか?」を確かめる公式ツールが git check-ignore。ただしこれが曲者で、追跡済みファイルにそのまま使うと、無言で「該当なし」を返してくる。
# 追跡済みの config.env をそのまま調べると…
git check-ignore -v config.env
echo "exit=$?"
# 出力なし、exit=1
出力ゼロ、終了コード 1。これを見て「やっぱりパターンが間違ってるんだ」と誤読して、.gitignore の書き方いじりに戻ってしまう。自分がハマった沼はここだった。
git check-ignore はデフォルトで、追跡済みのファイルを判定対象から外す。追跡済みなら「無視ルールは関係ない」ので黙る、という仕様。パターンが当たっているかを純粋に見たいなら --no-index を付ける。
# --no-index で「追跡状態を無視して」パターンだけ判定させる
git check-ignore -v --no-index config.env
echo "exit=$?"
# .gitignore:1:config.env config.env
# exit=0
.gitignore:1:config.env と出た。パターンはちゃんと1行目で当たっている。つまり書き方は正しく、追跡済みという状態だけが勝っていた、と確定できる。ここを切り分けられると、原因の当てずっぽうが止まる。
exit code の意味を、正しく読む
git check-ignore の終了コードは「無視されているか」ではなく「ルールに当たったか」を表す。ここも誤解しやすいので calibration しておく。
# *.log は無視 / !keep.log は再包含 / readme.md はどれにも当たらない
printf '*.log\n!keep.log\n' > .gitignore
git check-ignore -v a.log # .gitignore:1:*.log a.log → exit 0
git check-ignore -v keep.log # .gitignore:2:!keep.log keep.log → exit 0
git check-ignore -v readme.md # 出力なし → exit 1
a.log(無視される)も keep.log(否定で再包含=無視されない)も、どちらも exit 0。exit 0 = 「何かのルールに当たった」であって「無視されている」ではない。当たったパターンの先頭に ! が付いていたら、それは「無視しない」側のルール。ここを取り違えると、keep.log を「無視されている」と勘違いする。判定は exit code ではなく、表示されたパターンの ! の有無で読むのが正しい。
直し方:git rm --cached でインデックスから外す
原因が「追跡済み」なら、やることは追跡をやめさせるだけ。git rm の --cached を使う。--cached はワーキングツリーのファイルを消さず、Git のインデックス(追跡リスト)からだけ外すオプション。
# ファイルは手元に残したまま、Git の追跡だけ外す
git rm --cached config.env
# rm 'config.env' ← インデックスから外れただけ。手元のファイルは無事
git commit -q -m "stop tracking config.env"
これで完了。試しにもう一度 config.env を書き換えて git status を見ると、今度は静かになる。
printf 'SECRET_KEY=again\n' >> config.env
git status --short
# (何も出ない)← 追跡が外れたので .gitignore がやっと効く
git status --short の表示が 1件 → 0件に変わった。.gitignore の書き方は最初から1文字も変えていない。追跡を外しただけで効くようになった。ここが「書き方じゃなく状態の問題」だったことの証明になる。
git rm --cached の後は git check-ignore -v config.env も exit 0 でパターンを返すようになる(追跡済みという足かせが外れたから)。デバッグの答え合わせにも使える。
ディレクトリ丸ごとを、後から無視する手順
1ファイルなら上の通り。よくあるもう1つのケースが「logs/ を丸ごと .gitignore に入れたのに、中のファイルが追跡され続ける」パターン。ディレクトリ配下が複数追跡済みなので、再帰で外す。
# logs/ 配下がすでに複数コミット済みの状態から
printf 'logs/\n' >> .gitignore
# -r で再帰的に、logs/ 配下をまとめてインデックスから外す
git rm -r --cached logs/
git commit -q -m "untrack logs/"
git status --short
# (空)← logs/ 配下 2ファイル → 0ファイルで追跡解除
追跡ファイルが 2ファイル → 0ファイルになり、以後 logs/ 配下は無視される。リポジトリ全体で .gitignore を整理し直したいなら、いったん全部インデックスから外して入れ直す荒業もある。
# 全ファイルをインデックスから外し、.gitignore を反映して add し直す
git rm -r --cached . -q
git add .
git commit -m "apply .gitignore across repo"
ただしこれはリポジトリ全体に効くので、共有ブランチでやると差分が巨大になる。使うなら影響範囲を理解した上で。
否定パターン ! が効かない、典型ミス
.gitignore の書き方そのものでよくハマるのが否定パターン !。「build/ は全部無視、でも build/keep.txt だけは残したい」を、そのまま書くと効かない。
# よくある「効かない」書き方
printf 'build/\n!build/keep.txt\n' > .gitignore
git check-ignore -v build/keep.txt
# .gitignore:1:build/ build/keep.txt ← 1行目の build/ に飲まれて、! が無視される
!build/keep.txt を書いたのに、当たっているのは1行目の build/。理由は公式ドキュメントの注意書きにある。
It is not possible to re-include a file if a parent directory of that file is excluded.
(gitignore Documentation より)
親ディレクトリごと除外していると、Git はそのディレクトリの中を見に行かないので、中のファイルを ! で再包含できない。正しくは、ディレクトリ自体ではなく中身をワイルドカードで無視する。
# 親を丸ごとではなく「中身」を無視すれば ! が効く
printf 'build/*\n!build/keep.txt\n' > .gitignore
git check-ignore -v build/keep.txt
# .gitignore:2:!build/keep.txt build/keep.txt ← ! ルールに当たる=残せる
build/ と build/* は一見ほぼ同じだが、否定パターンと組むと結果が真逆になる。「例外だけ残したい」ときは dir/* + !dir/keep の形を反射で書けるようにしておくと事故らない。
留意点:git rm --cached の副作用と、共有リポの地雷
便利な git rm --cached にも、踏むと痛い注意点がある。整いすぎた手順だけ並べても嘘になるので、自分がヒヤッとした点を残す。
-
チーム全員の手元からは消えない。
git rm --cachedは追跡を外すだけで、コミット履歴には過去のファイルが残る。すでに push 済みの秘密情報を「なかったこと」にはできない。漏れたトークンは追跡解除ではなく**失効(ローテーション)**が正解。履歴からの完全削除はgit filter-repoなど別の重い作業になる。 - 他の人が pull すると、そのファイルが削除されることがある。追跡解除のコミットを共有ブランチに乗せると、相手の環境では「削除された」扱いになり、ローカルにあった同名ファイルが消える事故が起きうる。共有前に一声かけるのが無難。
-
グローバル設定の存在を忘れる。個人のエディタ設定などは各リポの
.gitignoreではなく、git config --global core.excludesFile ~/.gitignore_globalで一括除外できる。リポの.gitignoreをいくら直しても効かないときは、そもそも管理する場所を間違えている可能性もある。
要は、git rm --cached は「これから追跡しない」宣言であって「過去をなかったことにする」機能ではない。ここを混同すると、秘密情報を追跡解除しただけで安心してしまう。
ビフォーアフターと、今日/今週/今月やること
自分のケースでの変化を数値で残す。
- 詰まっていた時間: 切り分けを先に打っていれば 30分 → 5分 に縮んでいた。書き方を疑った時間はまるごと無駄だった。
-
git status --shortの変更表示: 1件 → 0件(追跡を外しただけ)。 -
logs/配下の追跡ファイル: 2件 → 0件(git rm -r --cached後)。 -
git check-ignore -v config.envの終了コード: exit 1(無言)→ exit 0(パターン表示)。
書き方をいじった時間はまるごと無駄で、切り分けの1コマンド(--no-index)を先に打っていれば済んでいた。
今日やること(5分)
手元で無視されないファイルがあるなら、まず切り分けを1回打つ。
# 追跡状態を無視して、パターンが当たっているかだけ確認する
git check-ignore -v --no-index <対象ファイル>
パターンが表示されたら書き方は正しい。あとは git rm --cached <対象ファイル> で追跡を外して commit。
今週やること(15分)
既存リポで「本当はコミットしたくないのに追跡済み」のファイルを棚卸しする。
# 追跡中のファイル一覧を出して、.env や logs/ が混じっていないか確認
git ls-files | grep -iE '\.env|\.log|/tmp/|\.DS_Store'
引っかかったものを git rm --cached で順に外していく。
今月やること(30分)
個人環境の除外はグローバルに寄せて、リポごとの .gitignore を薄く保つ。
# エディタ設定や OS 由来ファイルを全リポ共通で無視する
git config --global core.excludesFile ~/.gitignore_global
printf '.DS_Store\n.idea/\n*.swp\n' >> ~/.gitignore_global
これでリポの .gitignore は「そのプロジェクト固有の除外」だけになり、見通しがよくなる。
参考リンク
- gitignore Documentation(公式) — 追跡済みファイルには効かない、否定パターンの制約の原典
-
git-check-ignore Documentation(公式) —
--no-index-vの挙動と exit code -
git-rm Documentation(公式) —
--cachedの正確な意味 -
Pro Git: Recording Changes to the Repository —
.gitignoreの基本パターン集