0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

.gitignore に書いたのに毎回コミットされる — 追跡済みファイルに .gitignore は効かない

0
Last updated at Posted at 2026-07-18

.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"

ここまでで「.gitignoreconfig.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 は「そのプロジェクト固有の除外」だけになり、見通しがよくなる。

参考リンク

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?