.gitignoreを1行ずつ解剖
「書いたのに無視されない」を卒業できる。.gitignoreの構文を1行ずつ精読し、スラッシュ位置で決まる適用範囲、否定パターン、追跡済みファイルの罠と3つの置き場所の使い分けまで整理する。
- スラッシュを含まない*.logなどは配下のどこでも一致し、/dist/のような表記は.gitignoreの階層を起点に固定される。末尾スラッシュのnode_modules/などはディレクトリ限定だ。
- !.env.exampleなどの否定は除外を戻せるが、同じパスへ最後に一致した行が勝つので除外行より後へ置く。親ディレクトリ自体を除外すると、その中のファイルは!で再包含できない。
- 追跡済みファイルは.gitignoreへ書いても無視されず、git rm --cachedで追跡を外す。共有用.gitignore、個人用.git/info/exclude、全リポジトリ用core.excludesFileを使い分ける。
この設定ファイルは何者か
.gitignoreは、Gitに「このファイルは追跡候補から外してよい」と伝える設定ファイルだ。node_modulesのような再生成できる依存関係、ビルド成果物、ログ、秘密情報を含む.envなどをコミット対象から除外するために使う。リポジトリのルートに置いてコミットするのが基本形で、チーム全員が同じ除外設定を共有できる。
重要なのは、.gitignoreが影響するのは未追跡(untracked)のファイルだけという点だ。無視されたファイルもディスク上には残るし、既に追跡が始まっているファイルには一切効かない。この性質が「書いたのに無視されない」という定番のトラブルを生む。コミットと追跡の仕組みはGitのコミットグラフで詳しく解説している。
横にスクロール
1行ずつ解剖
NodeとPythonが混在するプロジェクトを想定した、現実的な.gitignoreを例に取る。
# 依存関係(コマンドで再生成できるもの)
node_modules/
.venv/
# ビルド成果物
/dist/
build/
*.egg-info/
# ログ・キャッシュ
*.log
npm-debug.log*
__pycache__/
*.py[cod]
# 環境変数・秘密情報
.env
.env.*
!.env.example
# エディタ・OS固有(本来はグローバル設定向き)
.vscode/
.DS_Store
コメント行と基本パターン
# で始まる行はコメント、空行は区切りとして無視される。*.log の * はスラッシュ以外の任意の文字列にマッチするワイルドカードで、拡張子でまとめて除外する定番の書き方になる。npm-debug.log* なら末尾に連番が付いたログも拾える。*.py[cod] の角かっこは1文字分の候補集合で、.pyc・.pyo・.pydの3種類を1行で表せる。
末尾スラッシュはディレクトリ限定
node_modules/ のように末尾へスラッシュを付けると、パターンはディレクトリだけにマッチする。付けなければ同名のファイルとディレクトリの両方が対象だ。ディレクトリがマッチすると、その中身は丸ごと無視される。
先頭スラッシュの有無で適用範囲が変わる
最も見落とされやすい規則だ。スラッシュを末尾以外の位置(先頭または途中)に含むパターンは、その.gitignoreが置かれた階層を起点に固定される。含まないパターンは、その階層以下のどの深さでもマッチする。
/dist/ # ルート直下の dist だけを無視する
build/ # どの階層の build ディレクトリも無視する
docs/api/ # 途中にスラッシュがあるので docs/api に固定される
サンプルの /dist/ はルート直下限定だが、build/ はサブプロジェクトの深い階層まで全部が対象になる。「別パッケージの同名フォルダまで消えた」という事故は、たいていこの違いを知らずに書いたのが原因だ。
2連アスタリスクは階層をまたぐ
サンプルには登場しないが、** も押さえておきたい。単独の * がスラッシュを越えられないのに対し、** はゼロ個以上のディレクトリにマッチして階層をまたげる。
**/logs # どの階層の logs にもマッチ(logs と同義)
logs/** # logs 以下のすべて(途中にスラッシュを含むので起点に固定)
a/**/b # a/b、a/x/b、a/x/y/b にマッチ
logs/** が前節の「途中にスラッシュを含むパターンは起点に固定」という規則にも従っている点まで読めると、構文の理解はほぼ完成だ。
否定パターンによる再包含と順序
.env.* で環境別の設定をまとめて無視しつつ、!.env.example でテンプレートだけをコミット対象に戻している。判定は上から順に照合され、同じパスにマッチした最後の行が結果を決める。だから否定行は必ず除外行より後に書く。順序を逆にすると、後続の .env.* が否定を上書きして効かなくなる。
最重要: 追跡済みファイルには効かない
.gitignoreは未追跡ファイル専用のフィルタであり、一度コミットして追跡が始まったファイルには何を書いても効かない。変更は差分として表示され続ける。ファイルを手元に残したまま追跡だけを外すには次のようにする。
git rm --cached .env # ファイルは残して追跡だけを外す
git rm -r --cached dist/ # ディレクトリには -r を付ける
git commit -m "stop tracking ignored files"
git rmを--cachedなしで実行すると、ワーキングツリーのファイル自体が削除される。追跡を外したいだけのときは必ず--cachedを付け、直後にgit statusで意図どおりかを確認したい。
複数の.gitignoreと3つの置き場所
.gitignoreはルート専用ではなく、サブディレクトリにも置ける。複数が競合したときは、対象パスに近い(深い)階層の.gitignoreが優先される。さらにリポジトリ外も含めると除外パターンの置き場所は主に3つあり、優先度は「対象に近い.gitignore、親の.gitignore、.git/info/exclude、core.excludesFileの指定先」の順で判定される。
| 置き場所 | 共有範囲 | 向いている用途 |
|---|---|---|
| .gitignore | コミットされチーム全員に共有 | node_modulesやdistなどプロジェクト固有の生成物 |
| .git/info/exclude | 自分のこのリポジトリだけ | 共有したくない個人メモ・実験用スクリプト |
| core.excludesFileの指定先 | 自分の全リポジトリ | .DS_Storeや*.swpなどOS・エディタ固有のファイル |
グローバル設定は次のコマンドで登録する。
git config --global core.excludesFile ~/.gitignore_global
サンプル末尾の.DS_StoreはmacOSが勝手に作るファイルで、プロジェクトの内容とは無関係だ。この種のOS・エディタ由来のファイルは、各プロジェクトの.gitignoreに繰り返し書くよりグローバル設定にまとめるほうが筋がよい。
つまずきやすい点
第一の定番は、やはり「書いたのに無視されない」だ。原因のほとんどは対象が追跡済みであることで、前節の手順で追跡を外せば解決する。切り分けには次の2つが役立つ。
git check-ignore -v build/app.log # どのファイルの何行目のパターンが効いたかを表示
git ls-files -- .env # 出力があればそのファイルは追跡済み
第二に、親ディレクトリごと除外した中身は否定で戻せない。Gitは無視されたディレクトリの中を性能上の理由で走査しないため、次の上側の書き方は機能しない。
logs/
!logs/.gitkeep # 効かない。親の logs/ 自体が除外されている
logs/*
!logs/.gitkeep # これは効く。中身だけを除外しているため
再包含したいものがあるなら、ディレクトリそのものではなく中身を除外対象にするのが定石だ。
第三に、大文字小文字の扱いはOS依存になる。core.ignoreCaseがtrue(WindowsやmacOSの既定)の環境では.gitignoreの照合も大文字小文字を区別しない。Windowsではthumbs.dbという記述でもThumbs.dbが無視されるが、Linuxでは区別されるためマッチせず、環境によって挙動が食い違う。パターンは実際のファイル名と大文字小文字まで一致させて書くのが安全だ。
最後に、.envのような秘密情報を誤ってコミットした後の対処だ。追跡を外しても過去のコミット履歴には内容が残り続ける。履歴から完全に消すにはgit filter-repoなどによる履歴書き換えと強制プッシュが必要になり、共有リポジトリでは全員に影響する大掛かりな作業になる。
一度でも履歴に載ったAPIキーやパスワードは、漏えいしたものとみなす。クローンやフォークに複製された可能性は履歴をどれだけ書き換えても消せないため、無効化と再発行(ローテーション)を必ず行う。
まとめ
.gitignoreの構文は小さいが、「スラッシュの位置で適用範囲が決まる」「同じパスにマッチした最後の行が勝つ」「追跡済みファイルには効かない」の3点を押さえれば、挙動はほぼ予測できるようになる。プロジェクト固有の生成物は共有の.gitignoreへ、個人の都合は.git/info/excludeかグローバル設定へ、という置き場所の使い分けも運用の型として覚えておきたい。同じ調子で設定ファイルを読み解くならpackage.jsonの解剖へ、Gitの内部構造はプログラミングの各記事へ進んでほしい。
設定ファイル解剖の記事ガイド
.gitignoreを1行ずつ解剖を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
設定ファイル
比較で見る軸
難易度: advanced / カテゴリ: 設定ファイル解剖 / タグ数: 3
導入後に効く点
!.env.exampleなどの否定は除外を戻せるが、同じパスへ最後に一致した行が勝つので除外行より後へ置く。親ディレクトリ自体を除外すると、その中のファイルは!で再包含できない。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- 設定ファイル解剖
- タグ数
- 3
判断チェックリスト
- 自社の用途が「設定ファイル / Git」に近いか確認する。
- 強みである「スラッシュを含まない*.logなどは配下のどこでも一致し、/dist/のような表記は.gitignoreの階層を起点に固定される。末尾スラッシュのnode_modules/などはディレクトリ限定だ。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。