言語:日本語
開発ツール

Gitignore Debugger

Git がそのファイルを無視しない理由を、正確に突き止めます。

ファイルパスと .gitignore のルールを貼り付けてください。どのルールが一致したのか、そしてその理由をお見せします。

すべてブラウザー内で実行されます。ファイルが送信されることはありません。

.gitignore ファイルとは

.gitignore ファイルは、どのファイルをバージョン管理の対象外にするかを Git に伝えるテキストファイルです。各行がパターンで — *.lognode_modules/.env など — パターンに一致する未追跡のパスは git status から消え、git add . でも飛ばされ、コミットされることはありません。

この説明の意味をほぼ一手に担っているのが 未追跡 という言葉です。そして「gitignore が効かない」という事例は、パターンの書き間違いすべてを合わせたよりも、この一語だけで説明できるものの方が多いのです。Git が .gitignore を参照するのは、まだ追跡していないファイルに対してだけです。いったんコミットされたファイルは、あとから .gitignore に何を書き足しても、Git がずっと追跡し続けます。

Gitignore Debugger でできること

ファイルパスと .gitignore の内容を貼り付けてください。この .gitignore デバッガーは、Git と同じ判定 — 無視される / 無視されない — を返し、その判定を下した正確なルールと行番号も示します。さらに、そのパスに一致するすべてのルールを Git が評価する順に並べ、実際に決着をつけたルールを指し示します。

この最後の部分が、単なる gitignore チェッカーには欠けているところです。ファイルが無視されていること自体は、めったに難所になりません。難しいのは、40 行あるルールの どれ がそうしたのか、そして期待していたルールがなぜ負けたのかを知ることです。これはどちらの向きでも役に立ちます。コミットしたいのに Git が無視してしまう理由も、消したいのに無視されない理由も、同じように調べられます。

エンジンは Git のパターン照合規則を再実装しています。ベース名で照合するパターンとアンカーされたパターンの違い、*/ を絶対に越えないこと、** の 3 つの形、ディレクトリ限定を意味する末尾の /、文字クラス、エスケープされた末尾の空白まで扱います。その挙動は git check-ignore の実際の出力と突き合わせて検証しており、推測ではありません。

使い方

  1. パスはリポジトリ内での表記どおりに、ルートからの相対パスで入力してください — src/config/local.json のように書き、C:\projects\app\src\config\local.json のようには書きません。区切りはスラッシュを使ってください。Windows のバックスラッシュはこちらで変換します。
  2. .gitignore全体 を貼り付けてください。怪しいと思っている 1 行だけに削らないこと。本当の原因はたいていルール間の優先順位にあり、ファイルを削ればその情報こそが失われます。
  3. まず判定を読み、その下のルールの連鎖を追ってください。効かない gitignore ルールが謎でなくなるのはここです。

ファイルではなくディレクトリに対して gitignore のルールを試すときは、パスの末尾をスラッシュにしてください — build/ のように。この区別は重要です。末尾が / のルールはディレクトリにしか一致しません。

gitignore のルールが効かないことがある理由

だいたい多い順に並べた原因はこちらです。

gitignore のルールの優先順位: 最後に一致したものが勝つ

これが誰もが引っかかるところです。同じ .gitignore の中では、あるパスに一致した 最後 のパターンがその運命を決めます。最初のパターンではありません。次の 2 つのファイルは同じルールを含んでいますが、important.log に対して正反対の結果になります。

*.log
!important.log        → important.log はコミットされる
!important.log
*.log                 → important.log は無視される

ですから、広く効くルールを先に、例外をあとに書いてください。ファイル同士の関係では、サブディレクトリにある .gitignore が、その配下のパスについては上位のものに勝ちます。Git はさらに、リポジトリ固有でコミットされない .git/info/exclude と、グローバルな core.excludesFile も読みます。どの .gitignore を探しても見つからないルールは、この 2 か所のどちらかに隠れていることがよくあります — git check-ignore -v なら、決着をつけたファイルと行がわかります。

否定ルールと、! がよく失敗する理由

パターンの先頭に ! を付けると、前のルールが無視したものを再び含められます。これには 2 つの限界があり、2 つ目が「gitignore の否定が効かない」というケースのほぼすべてを説明します。

1 つ目は、それより前で何かがそのパスを無視していなければならないこと。!important.log だけを書いても何も起きません。上書きすべき対象がもともと存在しないからです。

2 つ目は、はるかに意外な点です。無視されたディレクトリの中にあるファイルは、再び含めることができません。次の書き方は効かず、順序を入れ替えても直りません。

build/
!build/keep.txt

build/ が無視されていると、Git はそこへ入りません。build/keep.txt を調べないので、2 行目にはけっして到達しません。代わりにディレクトリの 中身 を無視してください。そうすればディレクトリ自体はたどれるままになります。

build/*
!build/keep.txt

もっと深いところにあるものを救い出したいときは、途中のディレクトリもすべてたどれるままにしておく必要があります — build/*、次に !build/assets/、次に !build/assets/logo.svg のように。

無視された親ディレクトリ

Git のディレクトリ走査は、最初に出会った無視対象のディレクトリで止まり、その配下はすべて中身を調べられることなく判定を受け継ぎます。だからこそ、無視された親ディレクトリは、ファイル自体に対して書いたどんなルールにも勝ちます。どれほど具体的に見えるルールであっても同じです。

その場合デバッガーは、単に「無視されています」と言うのではなく、走査を止めた親ディレクトリの名前を示します。そのうえで、ファイルのために書いた否定ルールも到達不能として表示します。Git がそこまで降りてこないため、検討さえされないからです。まずディレクトリを直せば、その配下のルールは自然と効くようになります。

.gitignore でよくある間違い

実例をいくつか

このルールに対して src/logs/debug.log無視されません。否定が最後の一致であり、走査を止める親ディレクトリもないので、否定に到達して勝ちます。

*.log
!src/logs/debug.log

このルールに対して build/keep.txt無視されます。1 行目の build/ が親ディレクトリに一致するためです。2 行目はけっして発動しません。

build/
!build/keep.txt

このルールに対して docs/api/notes.md無視されません。パターンがスラッシュによってアンカーされており、*api/notes.md のスラッシュを越えないからです。意図していたのは docs/**/*.md でしょう。

docs/*.md

上の gitignore テスターにこれらの例を貼り付ければ、ルールの連鎖をまるごと確認できます。ターミナルに戻れば、git check-ignore -v path/to/file が組み込みの同等機能です。決着をつけたソースファイル、行番号、パターンを教えてくれるので、覚えておく価値があります。

よくある質問

なぜ Git は私のファイルを無視するのですか?

何らかのパターンが一致しているからで、しかもそれは今見ているパターンとは別であることがよくあります。よくある原因は、サブディレクトリの .gitignore、無視された親ディレクトリ、そしてグローバルな除外ファイルです。git check-ignore -v path/to/file を実行すれば、決着をつけたソースファイル、行番号、パターンがそのまま出力されます。上のデバッガーにパスとルールを貼り付けても同じことがわかり、加えて一致したのに負けたルールも見えます。

.gitignore のルールが効かないのはなぜですか?

だいたい多い順に、次の 4 つでほぼすべての場合が説明できます。ファイルがすでに追跡されているためルールがまるごと無視されている。最後に一致したものが勝つので、あとのルールに上書きされている。親ディレクトリが無視されているため、Git がそのルールに到達していない。あるいはパターンが思っていたのと違う形でアンカーされている — 末尾以外の位置にスラッシュがあると、そのパターンは .gitignore のあるディレクトリにアンカーされます。

! でファイルが再び含まれないのはなぜですか?

それより前に誰もそのパスを無視していなかったため、否定に上書きするものがなかったか — あるいは可能性はこちらの方がはるかに高いのですが — そのファイルが無視されたディレクトリの中にあるかのどちらかです。Git は無視したディレクトリへ入らないので、ファイルを見ることもなく、あなたの ! ルールを評価することもありません。

ディレクトリ自体ではなく、その中身を無視してください。build/build/* に変え、そのあとに !build/keep.txt を残せば、否定に到達します。

.gitignore は最後のルールが勝つのですか?

はい。1 つのファイルの中では、あるパスに一致した最後のパターンがその運命を決めます。最初のものでも、いちばん具体的なものでもありません。*.log のあとに !important.log を書けば important.log は残りますが、この 2 行を入れ替えれば無視されます。広く効くルールを先に、例外をあとに書いてください。

無視されたフォルダーがあると、なぜ否定ルールが効かなくなるのですか?

Git のディレクトリ走査が、最初に無視対象となったディレクトリで止まるからです。build/ が一致すると、Git は中身を列挙せずにその部分木全体を無視扱いにします。つまり !build/keep.txt は上書きされたのではなく、単に到達されていないのです。順序を変えても何も変わりません。変わるのはディレクトリを再びたどれるようにしたときだけで、それを行うのが build/* です。

.gitignore のルールはどうテストすればよいですか?

このページの上部にあるデバッガーに、ファイルパスとルールを貼り付けてください。判定と、決め手になったルールと行、さらに途中で一致したすべてのルールが表示されます。もともと効くはずのなかった否定ルールも含まれます。

実際のリポジトリでは、git check-ignore -v path/to/file が原因となったソースファイル・行・パターンを示し、git status --ignored が現在無視されているものをすべて列挙します。

.gitignore はすでに追跡中のファイルにも効きますか?

いいえ。.gitignore が適用されるのは未追跡のパスだけです。いったんコミットされたファイルは Git が追跡し続け、ルールは無視されます — まったく正しいルールが何もしていないように見える原因として、これがいちばん多いものです。ファイルを削除せずに追跡だけをやめるには、git rm --cached path/to/file を実行し、その変更をコミットしてください。

紛らわしい点が 1 つあります。git check-ignore は追跡中のファイルについても一致を報告します。インデックスを見ずにパターンだけを試すからです。このコマンドの「はい、無視されます」は、Git がそのファイルの追跡をやめたことを意味しません。

.gitignore のルールに空白を含められますか?

途中であれば含められます。my logs/ は名前に空白を含むディレクトリに、エスケープなしで一致します。末尾の空白は別の話です。Git がそれを削るので、*.log のうしろに空白を置いても *.log とまったく同じように振る舞います。本当に空白で終わるファイル名に一致させたいときは、エスケープしてください — foo\ のように。

一方、行頭の空白には意味があり、削られることはありません。うっかりインデントしたルールは、その空白で始まるパスにしか一致しません。理由もなくルールが効いていないように見えるときは、余分なインデントがないか確認してください。

.gitignore 以外に、Git はどこで除外ルールを探しますか?

ほかに 3 か所あり、優先順位はこの順です。まず .gitignore ファイル自体で、より深いディレクトリのものが勝ちます。次に、リポジトリ固有でコミットされない .git/info/exclude。そして core.excludesFile が指す先、つまりグローバルな除外リストです。

ですからリポジトリのどこを探しても見つからないルールは、たいてい後ろの 2 つのどちらかにあります。git config --get core.excludesFile でグローバルなパスがわかり、git check-ignore -v は実際に決着をつけたファイルを示します。

*** の違いは何ですか?

*/ 以外の任意の文字列に一致するので、1 つのパスセグメントから外へ出ることはありません。src/*/test.js はちょうど 1 階層だけ降ります。

** は区切りを越えますが、それが効くのは Git が認識する 3 つの位置に限られます。任意の深さを表す先頭の **/、ディレクトリ内のすべてを表す末尾の /**、そして 0 個以上のディレクトリを表す中間の /**/ です。それ以外の位置、たとえば a**b では、余分なアスタリスクは単一の * と同じように振る舞います。