Git でプロジェクトを管理していると、git status の結果に node_modules/ や .DS_Store がずらりと並んで、肝心の変更ファイルが埋もれてしまうことがあります。これらはコミットする必要がないどころか、コミットしてはいけないファイルです。
そこで使うのが .gitignore です。この記事では、.gitignore に何を書けばよいのか、パターンの書き方(ワイルドカード、ディレクトリ指定、否定など)、Node.js や WordPress テーマ開発での実例、そして「書いたのに無視されない」ときの原因と対処まで解説します。
目次
.gitignore は何のためにあるのか
.gitignore は、Git の管理対象から外したいファイルをパターンで指定するテキストファイルです。ここに書かれたパターンに一致するファイルは、git status に「未追跡ファイル」として表示されなくなり、git add . でまとめて追加したときにも対象から外れます。
ファイル名はそのまま .gitignore(先頭のドットを含む)で、リポジトリのルートに置くのが基本です。.gitignore 自体は無視の対象ではなく、Git で管理してコミットするファイルです。チーム全員で同じ設定を共有するためのものなので、これを無視してしまっては意味がありません。
では、具体的にどんなファイルを除外するのでしょうか。代表的なものは次の3種類です。
再生成できるファイル
node_modules/ がその典型です。中身は package.json と package-lock.json があれば npm install でいつでも復元できます。にもかかわらずファイル数は数万に及ぶことも珍しくなく、コミットすればリポジトリのサイズも git clone にかかる時間も跳ね上がります。
同じ理由で、ビルド成果物の dist/ や build/、Sass がコンパイルしたあとの CSS、PHP の vendor/(Composer の依存)なども除外対象になりやすいファイルです。ソースからいつでも作り直せるものは、原則としてリポジトリに入れません。
公開してはいけない情報
データベースのパスワードや API キーを書いた .env、WordPress の wp-config.php、SSH の秘密鍵などがこれにあたります。GitHub の公開リポジトリに上げてしまえばそのまま流出しますし、非公開リポジトリであっても、リポジトリにアクセスできる全員に認証情報が見えることになります。
この種のファイルは、書き方の見本となる .env.example のようなファイルだけをコミットし、実際の値が入ったファイルは各自の環境に置く、という運用が一般的です。
環境ごとに勝手に作られるファイル
macOS がフォルダを開くたびに作る .DS_Store、Windows の Thumbs.db、エディタの設定フォルダ(.vscode/、.idea/)、ログファイルなどです。開発者ごとに中身が違うため、コミットすると毎回のように差分が出て、無意味なコンフリクトの原因になります。
基本の書き方
.gitignore は1行に1パターンを書くだけのシンプルなファイルです。まずは最小の例を見てみましょう。
# 依存パッケージ(npm install で復元できる)
node_modules/
# 環境変数ファイル
.env
# macOS が作るファイル
.DS_Store
# で始まる行はコメントとして扱われ、空行は無視されます。どのファイルをなぜ除外しているのかをコメントで残しておくと、あとから読む人(数か月後の自分を含む)が助かります。
ファイル名をそのまま書いた場合、その名前に一致するファイルが、どの階層にあっても除外されます。上の例の .DS_Store なら、ルートにあるものも assets/img/.DS_Store も、まとめて対象になります。階層を限定したいときは、次に説明する / の使い方が関わってきます。
パターンの書き方
.gitignore のパターンでは、いくつかの特殊な記号が使えます。まず一覧で確認してから、ひとつずつ見ていきます。
| パターン | 意味 |
|---|---|
config.php | どの階層にある config.php も除外する |
*.log | 拡張子が .log のファイルをすべて除外する(* は / 以外の0文字以上) |
draft?.txt | draft1.txt や draftA.txt のように、任意の1文字に一致する |
file[0-9].txt | file0.txt 〜 file9.txt に一致する(角括弧は文字の範囲・候補) |
/config.php | ルート直下の config.php だけを除外する(サブディレクトリのものは対象外) |
logs/ | logs というディレクトリとその中身を除外する(同名のファイルは対象外) |
src/config.php | 途中に / があるので、ルートから見た src/config.php だけを除外する |
src/**/*.map | src 以下の何階層下でも .map ファイルを除外する |
!important.log | 直前までのパターンで除外されたもののうち、これに一致するものを除外しない |
# コメント | 行頭の # はコメント |
* と ? と [ ] でまとめて指定する
* はディレクトリの区切り文字 / 以外の任意の文字(0文字以上)に一致します。ログファイルやキャッシュのように、拡張子だけ決まっていて名前が定まらないファイルをまとめて指定するのに便利です。
# 拡張子が .log のファイルをすべて除外
*.log
# .env.local や .env.production も除外(.env.example は残したい場合は後述)
.env.*
# 任意の1文字に一致する ? を使う
draft?.txt
# 角括弧は「このうちのどれか1文字」
file[0-9].txt
image[abc].png
? は任意の1文字、[abc] は a・b・c のいずれか1文字に一致します。[0-9] や [a-z] のようにハイフンで範囲を書くこともできます。実務で使う機会は * ほど多くありませんが、連番のファイルを扱うときなどに役立ちます。
/ を先頭に付けてルートからの位置を固定する
ここが .gitignore でいちばん誤解されやすいところです。パターンの途中や先頭に / が含まれているかどうかで、意味が変わります。
/ を含まないパターンは、どの階層にあっても一致します。一方、先頭に / を付けると、.gitignore が置かれているディレクトリから見た位置に固定されます。
# どの階層の config.php も除外される
# config.php → 除外
# src/config.php → 除外
# src/lib/config.php → 除外
config.php
# ルート直下の config.php だけを除外する
# config.php → 除外
# src/config.php → 除外されない
/config.php
# 途中に / があるパターンも、ルートからの指定として扱われる
# src/config.php → 除外
# app/src/config.php → 除外されない
src/config.php
3つ目の src/config.php のように、先頭に / がなくても途中に / が含まれていればルートからの指定になる点に注意してください。「スラッシュを1つでも含むパターンはルート基準、まったく含まないパターンは全階層が対象」と覚えると整理しやすくなります。
/ を末尾に付けてディレクトリだけを指定する
末尾の / は「これはディレクトリである」という指定です。logs/ と書けば logs ディレクトリとその中身すべてが除外されますが、logs という名前のファイルがあった場合は除外されません。
# logs ディレクトリだけを除外(logs という名前のファイルは対象外)
logs/
# 末尾に / がないと、ファイルでもディレクトリでも一致する
cache
# ルート直下の build ディレクトリだけを除外
/build/
ディレクトリを除外するときは、末尾に / を付けておくのが安全です。意図しない同名ファイルを巻き込まずに済みますし、読む側にも「これはディレクトリだ」と伝わります。
なお、Git はディレクトリそのものを追跡しません。管理しているのはあくまでファイルなので、中身が空のディレクトリはコミットできず、.gitignore に書く必要もありません。
** で階層をまたいで指定する
* は / をまたげないので、src/*.map は src 直下の .map ファイルにしか一致しません。階層の深さを問わずに指定したいときは ** を使います。
# src 直下の .map だけ(src/lib/app.js.map は対象外)
src/*.map
# src 以下なら何階層下でも対象
src/**/*.map
# 先頭の ** は「どの階層の」の意味。node_modules/ と同じ結果になる
**/node_modules
# 末尾の ** は「その中のすべて」
logs/**
** は、前後を / で挟んだときだけ特別な意味を持ちます。a/**/b は a/b、a/x/b、a/x/y/b のいずれにも一致し、0階層も含むのがポイントです。先頭の **/ はどの階層でもという意味になりますが、これはスラッシュを含まないパターンと同じ挙動なので、**/node_modules と書かずに node_modules/ と書けば十分です。
! で除外を打ち消す
「このディレクトリは全部無視したいけれど、この1ファイルだけは残したい」というときに使うのが ! です。行の先頭に付けると、それまでのパターンで除外されたファイルを除外の対象から戻します。
# .env で始まるファイルをすべて除外
.env*
# ただし .env.example は共有したいので除外しない
!.env.example
ここで重要なのが書く順番です。.gitignore は後に書かれたパターンが優先されるため、!.env.example を .env* より前に書いてしまうと、あとから .env* に一致して結局除外されてしまいます。否定は必ず、打ち消したいパターンよりあとに書いてください。
もうひとつ、! には重要な制限があります。親ディレクトリが除外されている場合、その中のファイルを ! で戻すことはできません。Git は除外されたディレクトリの中を探索しないため、中のファイルを指定する機会自体がないからです。
# 効かない書き方:assets/ の中は探索されないので ! が働かない
assets/
!assets/logo.svg
# 正しい書き方:ディレクトリ自体は除外せず、中身を除外してから戻す
assets/*
!assets/logo.svg
assets/* であれば assets ディレクトリ自体は除外されず、その中のファイルが個別に判定されるので、!assets/logo.svg が期待どおりに働きます。階層が深い場合は、途中のディレクトリを1段ずつ戻していく必要があります。
# すべて除外したうえで、必要なものだけを段階的に戻す
/*
!/src
!/package.json
!/.gitignore
実用的な .gitignore の例
ここまでのパターンを踏まえて、よくあるプロジェクト構成での .gitignore を見てみます。そのまま使うのではなく、自分のプロジェクトに合わせて増減させてください。
Node.js プロジェクト
# 依存パッケージ
node_modules/
# ビルド成果物
/dist/
/build/
*.tsbuildinfo
# 環境変数(サンプルだけ共有する)
.env
.env.*
!.env.example
# ログ
*.log
npm-debug.log*
yarn-error.log*
# テストのカバレッジレポート
/coverage/
# OS が作るファイル
.DS_Store
Thumbs.db
# エディタの設定
.vscode/
.idea/
迷いやすいのがロックファイル(package-lock.json や yarn.lock)ですが、これは除外せずにコミットします。依存パッケージのバージョンを全員でそろえるためのファイルなので、共有されていないと環境によって入るバージョンが変わってしまいます。
/dist/ のように先頭に / を付けているのは、node_modules の中などにある同名ディレクトリまで巻き込まないためです。ただし node_modules/ 自体を除外していれば結果は変わらないので、ここは好みの問題でもあります。
WordPress のテーマ開発
WordPress では、リポジトリの範囲をどこに取るかで書き方が変わります。テーマのディレクトリだけを Git 管理するのがもっとも扱いやすく、その場合はビルド関連とエディタ関連を除外すれば十分です。
# wp-content/themes/my-theme/ に置く .gitignore
# npm / Composer の依存
node_modules/
vendor/
# ビルド成果物(wp-scripts の出力先)
/build/
# Sass のコンパイル結果とマップ
/assets/css/*.css
/assets/css/*.css.map
.sass-cache/
# ローカル用の設定
.env
# OS・エディタ
.DS_Store
.vscode/
Sass のコンパイル結果を除外するかどうかは、デプロイ方法によって判断が変わります。サーバー上でビルドしない運用(FTP で上げるだけなど)なら、コンパイル済みの CSS もコミットしておく必要があります。「本番環境で誰がビルドするのか」を基準に決めてください。
WordPress のインストール全体をリポジトリにする場合は、認証情報を含む wp-config.php と、ユーザーがアップロードした画像が入る wp-content/uploads/ を必ず除外します。
# WordPress のインストール全体を管理する場合
# データベース接続情報が書かれている
/wp-config.php
# アップロードされたメディア
/wp-content/uploads/
# キャッシュ系プラグインの生成物
/wp-content/cache/
/wp-content/upgrade/
# デバッグログ
/wp-content/debug.log
.DS_Store
なお、よく使われる .gitignore のテンプレートは GitHub が gitignore リポジトリとして公開しています。言語やフレームワークごとにファイルが分かれているので、書き始めるときの土台として便利です。
.gitignore に書いたのに除外されないとき
「正しく書いたはずなのに git status に出続ける」というのは、.gitignore でもっとも多いつまずきです。原因はいくつかありますが、ほとんどは次の3つのどれかにあてはまります。
すでに追跡されているファイルには効かない
これが圧倒的に多い原因です。.gitignore が効くのは「まだ Git に追跡されていないファイル」だけです。一度でもコミットしたファイルは追跡対象(tracked)になっているので、あとから .gitignore に書き足しても、変更するたびに差分として出続けます。
解決するには、git rm --cached でそのファイルを追跡対象から外します。--cached を付けると、Git の管理からは外れるが、作業ディレクトリのファイルは残るという動きになります。
# ファイル1つを追跡から外す(ファイル自体は消えない)
git rm --cached .env
# ディレクトリごと外すときは -r を付ける
git rm --cached -r node_modules/
# 外したことをコミットする
git commit -m "Remove ignored files from tracking"
--cached を付け忘れると実ファイルまで削除されるので、コマンドを打つ前に必ず確認してください。特に .env のように手元にしか存在しないファイルを消してしまうと、復元できません。
.gitignore をまとめて整理したあとなど、対象が多いときは、いったん全ファイルをインデックスから外して追加し直す方法もあります。
# 未コミットの変更がないことを確認してから実行する
git rm -r --cached .
git add .
git commit -m "Apply .gitignore"
この方法は、.gitignore に一致するファイルだけがインデックスから外れ、それ以外は git add . で戻ってくるという仕組みです。未コミットの変更が残っていると混乱するので、実行前にコミットを済ませておいてください。
ひとつ注意しておきたいのは、この操作で消えるのはこれから先の追跡だけだという点です。過去のコミットの中にはファイルが残り続けるため、パスワードや API キーを一度でも push してしまった場合は、その認証情報を無効化して作り直すのが唯一確実な対処になります。履歴から完全に消すには git filter-repo などで履歴を書き換える必要があり、共有ブランチでは影響が大きい作業です。
パターンが意図した範囲を指していない
追跡されていないファイルなのに無視されない場合は、パターン自体を疑います。よくあるのは、先頭に / を付けたせいでルート直下しか対象になっていない、* が / をまたげないことを忘れている、といったケースです。
そんなときは git check-ignore -v を使うと、どのファイルのどの行が効いているのかを直接確認できます。
$ git check-ignore -v src/app.log
.gitignore:8:*.log src/app.log
出力は左から順に「パターンが書かれたファイル名」「行番号」「パターン」、そして調べたパスです。この例なら、.gitignore の8行目にある *.log によって除外されている、と分かります。
何も出力されず、終了ステータスが 1 で終わる場合はどのパターンにも一致していないということです。つまり「.gitignore に書いたのに効かない」のではなく、「書いたパターンがそのファイルに当たっていない」状態なので、パターンの書き方を見直します。
逆に「無視されるはずのないファイルが消えている」ときにも使えます。意図しない ! の順番や、親ディレクトリに置いた .gitignore が効いていることに気づけるはずです。
別の場所の .gitignore が効いている
.gitignore はリポジトリのルート以外にも置けます。サブディレクトリに置いた場合、そのパターンはそのディレクトリ以下にだけ適用され、より深い階層のファイルには、ルートのものとサブディレクトリのものの両方が影響します。同じファイルに複数のパターンが当たるときは、より深い階層の .gitignore が優先されます。
「ルートで除外しているのに、特定のディレクトリだけ無視されない」という場合は、そのディレクトリに .gitignore が置かれていて、! で打ち消されている可能性があります。前述の git check-ignore -v はどのファイルの何行目かまで教えてくれるので、こうした切り分けにそのまま使えます。
共有しないローカル専用の除外設定
.gitignore はコミットして共有するファイルなので、自分の環境だけの都合をそこに書き込むのは行儀が良くありません。たとえば自分が使っているエディタの設定フォルダや、動作確認用に置いた個人的なメモファイルなどです。こうした自分だけの除外設定には、2つの置き場所が用意されています。
そのリポジトリだけで使う .git/info/exclude
.git/info/exclude は、.gitignore とまったく同じ書式で書けるファイルです。.git ディレクトリの中にあるためコミットされることがなく、そのリポジトリの、自分の環境でだけ効きます。
# このリポジトリの自分の環境でだけ除外する
memo.md
scratch/
チームで使っているリポジトリに手元の作業用ファイルを置きたいけれど、.gitignore を汚したくない、というときに向いています。
全リポジトリで使うグローバルな .gitignore
.DS_Store や Thumbs.db のように、プロジェクトではなく自分の OS が作るファイルは、リポジトリごとに書くよりもグローバル設定にまとめてしまうほうが合理的です。core.excludesfile に任意のファイルパスを指定すると、そのファイルがすべてのリポジトリで参照されます。
# 使うファイルのパスを登録する
git config --global core.excludesfile ~/.gitignore_global
# 設定を確認する
git config --global core.excludesfile
# macOS
.DS_Store
.AppleDouble
# Windows
Thumbs.db
Desktop.ini
# エディタ
.idea/
*.swp
ファイル名は ~/.gitignore_global でなくても構いません。core.excludesfile を設定していない場合でも、~/.config/git/ignore があれば既定で読み込まれます。
ただし、グローバル設定は自分の環境にしか存在しないことを忘れないでください。自分の手元では .DS_Store が見えなくても、設定していないメンバーがコミットしてしまうことはあります。チーム共有のリポジトリでは、プロジェクトの .gitignore にも書いておくほうが確実です。
まとめ
.gitignore はリポジトリのルートに置き、再生成できるファイル・公開してはいけない情報・環境ごとに作られるファイルを除外するために使います。パターンの要点は、スラッシュを含まない指定は全階層が対象、先頭や途中に / があればルートからの指定、末尾の / はディレクトリ限定、** は階層をまたぐ、という4つです。
そして何より覚えておきたいのが、すでに追跡されているファイルには .gitignore が効かないということ。その場合は git rm --cached で追跡を外してからコミットします。思ったとおりに動かないときは git check-ignore -v でどのルールが効いているかを確かめれば、原因はすぐに絞り込めます。
プロジェクトを作ったら、最初のコミットの前に .gitignore を用意しておくのが理想です。あとから追加するより手間がかからず、認証情報を誤って push してしまう事故も防げます。