
.gitignore が効かない時の原因と対処 - 追跡済みファイルと書き方の落とし穴を確認する
当サイトは Amazon.co.jp を宣伝しリンクすることで紹介料を得る手段を提供する、Amazonアソシエイト・プログラムの参加者です。価格・在庫はリンク先の最新情報をご確認ください。
.gitignoreにファイル名を書いたのに、git statusに変更が出続ける。逆に、コミットしたいファイルがなぜか無視されている。どちらもよくあるトラブルですが、原因は数種類に絞れます。この記事では、.gitignoreが期待どおりに動かない原因を早見表で整理し、原因ごとの対処、パターンの書式、無視設定の優先順位、切り分けに使うコマンドを順に見ていきます。仕様の説明はGitの公式マニュアルを出典にし、出力例はgit version 2.52.0(macOS)の一時リポジトリで実際に実行した結果です。
まず結論 - 原因の早見表
.gitignoreが効かない原因は、大きく次の5つに分けられます。
| 症状 | 主な原因 | 対処 |
|---|---|---|
| 書いたのに変更が出続ける | すでに追跡されている(コミット済み、またはgit add済み) | git rm --cachedで追跡を外してコミットする |
| 一部のパスだけ無視されない、または無視されすぎる | パターンの書き方(スラッシュの位置、行頭の空白、行末のコメントなど) | パターンの書式を見直す |
!で除外を解除したのに無視されたまま | 親ディレクトリごと除外している | ディレクトリではなく中身を除外する形に書き換える |
.gitignoreに書いていないのに無視される | .git/info/excludeやcore.excludesFileなど別の場所の設定 | git check-ignore -vで出どころを確認する |
| どのパターンも一切効かない | ファイルの文字コード(UTF-16など)や置き場所、ファイル名の誤り | UTF-8で保存し直す |
最初に確認するとよいのは、次の1コマンドです。
$ git check-ignore -v <調べたいパス>何か出力されれば、そのパスは無視の対象で、どのファイルの何行目のパターンに一致したかが分かります。何も出力されず終了コードが1なら、どのパターンにも一致していないか、すでに追跡されているかのどちらかです。以降の節で、それぞれの見分け方を説明します。
原因1: すでに追跡されているファイル
もっとも多い原因です。公式マニュアルのgitignoreのページは、冒頭で次のように説明しています。
gitignoreファイルは、Gitに無視させたい「意図的に追跡しないファイル」を指定するものである- すでにGitが追跡しているファイルには影響しない
つまり.gitignoreは「未追跡のファイルを未追跡のままにしておく」ための仕組みで、一度コミットしたファイルを後から無視させる機能ではありません。
症状を再現する
.envとlogs/をコミットしたあとで.gitignoreを追加し、両方を書き換えてみます。
$ cat .gitignore
.env
logs/
$ git status --short
M .env
M logs/app.log
?? .gitignore.gitignoreに書いてあるのに、変更として表示されています。このときgit check-ignore -vは何も出力しません。
$ git check-ignore -v .env
$ echo $?
1公式マニュアルによると、git check-ignoreは既定では追跡済みのファイルを表示しません。追跡済みのファイルは除外ルールの対象ではないためです。インデックスを見ずにパターンだけを評価する--no-indexを付けると、パターン自体は一致していることが分かります。
$ git check-ignore -v --no-index .env logs/app.log
.gitignore:1:.env .env
.gitignore:2:logs/ logs/app.log「--no-indexを付けると一致し、付けないと何も出ない」なら、原因は追跡済みであることです。追跡されているのに無視パターンに一致するファイルは、git ls-filesでまとめて列挙できます。
$ git ls-files -ci --exclude-standard
.env
logs/app.log
logs/error.log-cは追跡済みのファイル、-iは無視パターンに一致するものだけ、--exclude-standardは通常のコマンドと同じ無視設定(各ディレクトリの.gitignore、.git/info/exclude、ユーザー全体の無視ファイル)を使う指定です。マニュアルには、-iは-cか-oと一緒に使うこと、そして--exclude系のオプションが少なくとも1つ必要であることが書かれています。
git rm --cached で追跡を外す
対処は、公式マニュアルのgitignoreのNOTESと、公式FAQ(gitfaq)の両方に書かれているとおり、git rm --cachedでインデックスから外すことです。--cachedを付けると、インデックスからだけ削除され、ワーキングツリーのファイルは変更の有無にかかわらず残ります。
$ git rm --cached .env
rm '.env'
$ git rm -r --cached logs
rm 'logs/app.log'
rm 'logs/error.log'
$ git status --short
D .env
D logs/app.log
D logs/error.log
?? .gitignoreディレクトリを指定するときは-rが必要です。付けないと次のエラーになります。
$ git rm --cached logs
fatal: not removing 'logs' recursively without -rこのあと.gitignoreと一緒にコミットします。
$ git add .gitignore
$ git commit -m "chore: stop tracking .env and logs"
[main 04f5295] chore: stop tracking .env and logs
4 files changed, 2 insertions(+), 3 deletions(-)
delete mode 100644 .env
create mode 100644 .gitignore
delete mode 100644 logs/app.log
delete mode 100644 logs/error.log
$ ls -A
.env
.git
.gitignore
logs
src
$ git status --short --ignored
!! .env
!! logs/ファイルは手元に残ったまま、git statusには出なくなり、--ignoredを付けると無視されたファイルとして!!で表示されます。
対象が多いときは、先ほどのgit ls-filesの結果をそのまま渡す方法と、いったん全部をインデックスから外して追加し直す方法があります。どちらも実機で同じ結果になりました。
# 無視パターンに一致する追跡済みファイルだけを外す
$ git ls-files -ci --exclude-standard -z | xargs -0 git rm --cached
# 全部を外してから追加し直す(.gitignore に一致するものは追加されない)
$ git rm -r -q --cached .
$ git add .
$ git status --short
D .env
A .gitignore
D logs/app.log後者は、作業中の変更がある場合にそれも一緒にステージされる点に注意してください。ステージした内容を取り消す方法はgit reset と revert と restore の違いで扱っています。
他の人の作業ツリーで起きること
git rm --cachedは、自分の手元ではファイルを残しますが、コミットとしては「そのファイルを削除した」という記録になります。そのため、このコミットを他の人が取り込むと、その人のワーキングツリーからはファイルが削除されます。別のクローンで確認した結果です。
$ ls -A
.env
.git
logs
src
$ git pull
Updating 4c6cab9..04f5295
Fast-forward
.env | 1 -
.gitignore | 2 ++
logs/app.log | 1 -
logs/error.log | 1 -
4 files changed, 2 insertions(+), 3 deletions(-)
delete mode 100644 .env
create mode 100644 .gitignore
delete mode 100644 logs/app.log
delete mode 100644 logs/error.log
$ ls -A
.git
.gitignore
src.envもlogsも消えました。相手がそのファイルを書き換えていた場合は、削除されずにpullが中止されます。
$ git pull
error: Your local changes to the following files would be overwritten by merge:
.env
Please commit your changes or stash them before you merge.
Aborting設定ファイルのように各自が値を書き込んでいるファイルの追跡を外すときは、取り込む前に退避しておくよう、チームに伝えておくと安全です。
履歴には残り続ける
git rm --cachedでやめられるのは「これから先の追跡」だけで、過去のコミットにはファイルの内容が残っています。
$ git log --oneline -- .env
04f5295 chore: stop tracking .env and logs
4c6cab9 initial commit
$ git show HEAD~1:.env
API_KEY=dummyコミットがファイルの内容をどう保持しているかはGitの内部構造(オブジェクトとpackfile)の解説を参照してください。
パスワードやAPIキーのような秘密情報をpushしてしまった場合、やるべきことの順番が変わります。GitHub Docsの「Removing sensitive data from a repository」は、最初のステップとしてその秘密情報を失効またはローテーションすることを挙げています。失効させれば、その値ではもうアクセスできなくなるためです。履歴の書き換えはその後の話で、同ページは次の点を注意として挙げています。
- 履歴を書き換えてforce pushしても、他のクローンやフォークにはコミットが残りうる
- 古いクローンを持つ人が、消したはずのデータを再びpushしてしまうことがある
- コミットのハッシュが変わり、開いているプルリクエストや署名に影響する
履歴から消す道具として同ページが案内しているのはgit-filter-repoです。Git本体のgit filter-branchのマニュアルも、filter-branchの使用は推奨せず、git filter-repoのような別の道具を使うよう書いています。GitHub Docsに載っているコマンドは次の形です(この記事では実行していません)。
$ git-filter-repo --sensitive-data-removal --invert-paths --path <秘密情報を含むファイルのパス>秘密情報の置き方そのものは環境変数と.envの扱い方で、Gitに秘密情報が混ざる典型的な経路はGitの秘密情報漏えいパターン集でまとめています。
原因2: パターンの書き方
追跡済みではないのに無視されない場合は、パターンの書式を確認します。ここからは公式マニュアルの「PATTERN FORMAT」の内容を、実機の結果と合わせて見ていきます。
スラッシュの位置で意味が変わる
マニュアルの説明をまとめると、次のようになります。
| スラッシュの位置 | 意味 |
|---|---|
| 先頭または途中にある | その.gitignoreがあるディレクトリからの相対パスとして一致する |
| どこにもない | .gitignoreより下のどの階層でも一致する |
| 末尾にある | ディレクトリだけに一致する(ファイルには一致しない) |
末尾のスラッシュは「先頭または途中」には数えません。そのためbuild/は、どの階層のbuildディレクトリにも一致します。次のファイル構成で確認します。
build/out.js
src/build/out.js# /build と書いた場合
$ git check-ignore -v build/out.js src/build/out.js
.gitignore:1:/build build/out.js
# build と書いた場合
$ git check-ignore -v build/out.js src/build/out.js
.gitignore:1:build build/out.js
.gitignore:1:build src/build/out.js
# build/ と書いた場合
$ git check-ignore -v build/out.js src/build/out.js
.gitignore:1:build/ build/out.js
.gitignore:1:build/ src/build/out.js
# src/build と書いた場合
$ git check-ignore -v build/out.js src/build/out.js
.gitignore:1:src/build src/build/out.js「リポジトリ直下のbuildだけを無視したい」なら/build、「どの階層でも」ならbuildまたはbuild/です。途中にスラッシュがあるsrc/buildは、先頭に/がなくても.gitignoreの位置からの相対パスとして扱われます。マニュアルにも、doc/frotzと/doc/frotzはどの.gitignoreでも同じ効果だと書かれています。
よくある失敗は、サブディレクトリの名前を付けたつもりのパターンが、どこにも一致していないケースです。たとえば.gitignoreをsrc/の中に置いているのにsrc/buildと書くと、src/src/buildを探すことになります。
ワイルドカードと **
マニュアルの定義は次のとおりです。
| 記法 | 意味 |
|---|---|
* | スラッシュ以外の任意の文字列に一致する |
? | スラッシュ以外の任意の1文字に一致する |
[a-zA-Z] | 範囲内の1文字に一致する |
先頭の**/ | すべてのディレクトリで一致する。**/fooはfooと同じ |
末尾の/** | そのディレクトリの中身すべてに一致する(深さの制限なし) |
途中の/**/ | 0個以上のディレクトリに一致する。a/**/bはa/b、a/x/b、a/x/y/bに一致 |
これら以外の位置に連続したアスタリスクを書いた場合は、通常の*として扱われます。
*はスラッシュをまたがない点が重要です。
# logs/*/debug.log と書いた場合
$ git check-ignore -v logs/debug.log logs/2026/debug.log logs/2026/11/debug.log
.gitignore:1:logs/*/debug.log logs/2026/debug.log
# logs/**/debug.log と書いた場合
$ git check-ignore -v logs/debug.log logs/2026/debug.log logs/2026/11/debug.log
.gitignore:1:logs/**/debug.log logs/debug.log
.gitignore:1:logs/**/debug.log logs/2026/debug.log
.gitignore:1:logs/**/debug.log logs/2026/11/debug.log一方、スラッシュを含まない*.logはどの階層でも一致します。直下だけにしたいときは/*.logと書きます。
# *.log と書いた場合
$ git check-ignore -v app.log src/app.log logs/2026/11/debug.log
.gitignore:1:*.log app.log
.gitignore:1:*.log src/app.log
.gitignore:1:*.log logs/2026/11/debug.log
# /*.log と書いた場合
$ git check-ignore -v app.log src/app.log
.gitignore:1:/*.log app.log空白・コメント・エスケープ
1行の中の細かい書き方でも、パターンが効かなくなることがあります。マニュアルの規則は次のとおりです。
- 空行は何にも一致しない(区切りとして使える)
#で始まる行はコメント。#で始まるファイル名を指定するときは\#と書く- 行末の空白は無視される。空白を含めたいときはバックスラッシュでエスケープする
- バックスラッシュは任意の文字をエスケープできる。パターンの末尾がバックスラッシュで終わるものは無効で、何にも一致しない
実機で確認した結果です。cat -eは行末を$で表示します。
# 行末の空白は無視される(一致する)
$ cat -e .gitignore
*.tmp $
$ git check-ignore -v b.tmp
.gitignore:1:*.tmp b.tmp
# 行頭の空白はパターンの一部になる(一致しない)
$ cat -e .gitignore
*.tmp$
$ git check-ignore -v b.tmp
$ echo $?
1
# 行の途中からコメントは書けない(一致しない)
$ cat -e .gitignore
*.tmp # temp files$
$ git check-ignore -v b.tmp
$ echo $?
1マニュアルがコメントとして定めているのは「#で始まる行」だけです。パターンの後ろに# ...を続けると、その部分まで含めて1つのパターンとして扱われ、実機でも一致しませんでした。コメントは必ず独立した行に書きます。
#で始まるファイル名は、エスケープしないとコメント扱いになります。
$ cat .gitignore
\#memo
$ git check-ignore -v '#memo'
.gitignore:1:\#memo #memo改行コードがCRLFの.gitignoreについては、マニュアルに記述を見つけられませんでした。手元のgit version 2.52.0(macOS)では、CRLFで保存した.gitignoreのパターンも一致しました。すべての環境とバージョンで同じかどうかは未確認です。
大文字と小文字
パターンが大文字小文字を区別するかどうかは、core.ignoreCaseの値で変わります。git configのマニュアルによると、この変数はAPFS、HFS+、FAT、NTFSなど大文字小文字を区別しないファイルシステムでGitをうまく動かすための内部変数で、既定値はfalseですが、git cloneやgit initがリポジトリを作るときにファイルシステムを調べて、必要ならtrueに設定します。
macOSで作ったリポジトリではtrueになっていて、*.logがFile.LOGにも一致しました。
$ git config core.ignorecase
true
$ git check-ignore -v File.LOG
.gitignore:1:*.log File.LOG実験のために一時的にfalseへ変えると、同じパターンは一致しなくなりました。Linuxなど大文字小文字を区別する環境では、*.logと*.LOGは別のパターンとして書く必要があります。なお、マニュアルにはこの変数を書き換えると予期しない動作になることがあると書かれているので、無視パターンを通すために値を変えるのは避けてください。
原因3: 否定パターン(!)の制約
!で始まるパターンは、それより前のパターンで除外されたファイルを再び対象に戻します。ここで引っかかりやすいのが、マニュアルに書かれた次の制約です。
- ファイルの親ディレクトリが除外されている場合、そのファイルを再包含することはできない
- Gitは性能上の理由から、除外されたディレクトリの中を一覧しない。そのため中のファイルに対するパターンは、どこに書いても効果がない
logs/を除外して、その中の1ファイルだけ戻そうとした例です。
$ cat .gitignore
logs/
!logs/debug.log
$ git check-ignore -v logs/debug.log
.gitignore:1:logs/ logs/debug.log
$ git status --short --ignored logs
!! logs/2行目の!logs/debug.logは効かず、ディレクトリごと無視されています。ディレクトリそのものではなくディレクトリの中身を除外する形(logs/*)に変えると、再包含できます。
$ cat .gitignore
logs/*
!logs/debug.log
$ git check-ignore -v logs/debug.log logs/2026
.gitignore:2:!logs/debug.log logs/debug.log
.gitignore:1:logs/* logs/2026
$ git status --short --ignored logs
?? logs/
!! logs/2026/git check-ignore -vの出力に!で始まるパターンが表示されたときは、「このパターンに一致したので無視されない」という意味です。マニュアルにも、一致したパターンが!で始まる場合は除外されないことを表すと書かれています。
深い階層のファイルだけを残したい場合は、階層ごとに「中身を除外して、次のディレクトリだけ戻す」を繰り返します。マニュアルの例(foo/bar以外をすべて除外する)は次の形です。
/*
!/foo
/foo/*
!/foo/barパターンの順番
同じファイルの中では、最後に一致したパターンが結果を決めます。否定パターンを除外パターンより前に書くと、後ろの除外に上書きされます。
$ cat .gitignore
!app.log
*.log
$ git check-ignore -v app.log
.gitignore:2:*.log app.log
$ cat .gitignore
*.log
!app.log
$ git check-ignore -v app.log
.gitignore:2:!app.log app.log原因4: 別の場所の無視設定
.gitignoreに書いた覚えがないのに無視される場合や、書いたのに別の設定に打ち消される場合は、無視パターンの置き場所が複数あることを思い出してください。マニュアルは、優先順位の高い順に次の4つを挙げています。
| 優先順位 | 置き場所 | 用途 |
|---|---|---|
| 1 | コマンドラインで渡すパターン(対応しているコマンドのみ) | 一時的な指定 |
| 2 | 各ディレクトリの.gitignore | リポジトリに含めて全員で共有する |
| 3 | $GIT_COMMON_DIR/info/exclude(通常は.git/info/exclude) | そのリポジトリだけで使い、共有しない |
| 4 | core.excludesFileで指定したファイル | そのユーザーのすべてのリポジトリで使う |
.gitignoreどうしでは、下の階層にあるファイルのパターンが、上の階層のパターンを上書きします。同じ優先順位の中では、最後に一致したパターンが結果を決めます。
core.excludesFileの既定値は$XDG_CONFIG_HOME/git/ignoreで、$XDG_CONFIG_HOMEが未設定か空の場合は$HOME/.config/git/ignoreが使われます。GitHub Docsの「Ignoring files」も、すべてのリポジトリに適用する設定の置き場所として~/.config/git/ignoreを案内しています。エディタのバックアップファイルやOSが作る管理ファイルのように、プロジェクトではなく自分の環境に由来するものは、ここに置くのが向いています。
また、.git/info/excludeやcore.excludesFileのようにワーキングツリーの外にあるファイルのパターンは、ワーキングツリーの最上位に書かれたものとして扱われます。先頭の/はリポジトリの最上位を指します。
実際に4種類を混ぜた例です。
$ cat .gitignore
*.bak
$ cat sub/.gitignore
!keep.bak
$ cat .git/info/exclude
local.memo
$ git config --show-origin core.excludesFile
file:/tmp/gi.siag8f/globalconfig /tmp/gi.siag8f/global_ignore
$ git check-ignore -v a.bak sub/a.bak sub/keep.bak local.memo .DS_Store
.gitignore:1:*.bak a.bak
.gitignore:1:*.bak sub/a.bak
sub/.gitignore:1:!keep.bak sub/keep.bak
.git/info/exclude:1:local.memo local.memo
/tmp/gi.siag8f/global_ignore:1:.DS_Store .DS_Store出力の左端がパターンの出どころです。マニュアルによると、core.excludesFileのファイルは絶対パスで、.git/info/excludeと各ディレクトリの.gitignoreはリポジトリの最上位からの相対パスで表示されます。「なぜか無視される」ときは、この左端を見れば原因のファイルが特定できます。
優先順位の違いも確認できます。.git/info/excludeでx.txtを除外し、.gitignoreに!x.txtを書くと、優先順位の高い.gitignoreが勝ち、無視されなくなりました。
$ git check-ignore -v x.txt
.gitignore:2:!x.txt x.txt原因5: 文字コードと置き場所
どのパターンもまったく効かない場合は、ファイルそのものを疑います。
UTF-16で保存された.gitignore
Windowsで.gitignoreをコマンドから作ったときに起きやすい問題です。Microsoftのドキュメント「about_Character_Encoding」(PowerShell 5.1版)には、次のように書かれています。
- Windows PowerShell(5.1)では、
Out-Fileとリダイレクト演算子の>、>>はUTF-16LEのファイルを作る Set-ContentとAdd-Contentは、対象のファイルが空か存在しない場合、システムのANSIコードページを使う- PowerShell(6以降)は、すべての出力で既定が
utf8NoBOM(BOMなしのUTF-8)
つまり、Windows PowerShell 5.1で次のように.gitignoreを作ると、UTF-16LEのファイルになります。
echo "*.log" > .gitignoreUTF-16LEの.gitignoreをGitがどう扱うかについて、公式マニュアルには記述を見つけられませんでした。そこで、macOS上でiconvを使ってBOM付きUTF-16LEの.gitignoreを作り、挙動を確認しました。
$ file .gitignore
.gitignore: Unicode text, UTF-16, little-endian text
$ xxd .gitignore
00000000: fffe 2a00 2e00 6c00 6f00 6700 0a00 ..*...l.o.g...
$ git check-ignore -v app.log
$ echo $?
1*.logと書いてあるはずなのに、app.logは無視されませんでした。1文字ごとに00のバイトが挟まっているため、Gitからは別のパターンに見えていると考えられます。同じ内容をBOM付きのUTF-8で保存した場合は、パターンが一致しました。
$ file .gitignore
.gitignore: Unicode text, UTF-8 (with BOM) text
$ git check-ignore -v app.log
.gitignore:1:*.log app.logこの節の検証について、確認できた範囲とできていない範囲は次のとおりです。
- 確認済み:
git version 2.52.0(macOS)で、BOM付きUTF-16LEの.gitignoreは効かず、BOM付きUTF-8の.gitignoreは効いた - 未確認: Windowsの実機で
echoのリダイレクトを使って作ったファイルでの挙動、Git for Windowsでの挙動、古いバージョンのGitでのBOMの扱い
対処は、エディタで文字コードをUTF-8(BOMなしが無難)にして保存し直すことです。PowerShellから作る場合は、-Encodingを明示します。前述のドキュメントによると、Windows PowerShell 5.1の-EncodingにはAsciiやUTF8(BOM付き)などの値があり、パターンが英数字と記号だけならAsciiで足ります。次のコマンドはドキュメントの記述に基づく例で、Windowsの実機では実行していません。
"*.log" | Out-File -Encoding ascii .gitignoreファイル名と置き場所
あわせて、次の点も確認しておきます。
- ファイル名が
.gitignoreそのものになっているか(ls -Aで確認する。別の拡張子が付いていると読み込まれない) - 途中にスラッシュがあるパターンは、その
.gitignoreが置かれたディレクトリからの相対パスになっているか - マニュアルによると、Gitはワーキングツリーの
.gitignoreにアクセスするときシンボリックリンクをたどらない。.gitignore自体をシンボリックリンクにしている場合は実ファイルに置き換える
切り分けに使うコマンド
ここまでに出てきたコマンドを、用途別にまとめます。
| 知りたいこと | コマンド |
|---|---|
| このパスはなぜ無視されるのか | git check-ignore -v <パス> |
| 追跡済みかどうかを無視して、パターンだけ評価したい | git check-ignore -v --no-index <パス> |
| 一致しなかったパスも含めて表示したい | git check-ignore -v -n <パス> |
| 無視されているファイルの一覧 | git status --ignored |
| 無視されている未追跡ファイルを1つずつ列挙 | git ls-files -o -i --exclude-standard |
| 追跡済みなのに無視パターンに一致するファイル | git ls-files -ci --exclude-standard |
| 追跡されているファイルの一覧 | git ls-files |
git check-ignore
-vを付けたときの出力は、マニュアルによると次の形式です。
<source>:<linenum>:<pattern><TAB><pathname>終了コードは、指定したパスのうち1つ以上が無視されていれば0、どれも無視されていなければ1、致命的なエラーなら128です。-n(--non-matching)を-vと併用すると、どのパターンにも一致しなかったパスも、パス以外の欄が空の行として出力されます。
$ git check-ignore -v -n sub/keep.bak .gitignore
sub/.gitignore:1:!keep.bak sub/keep.bak
:: .gitignoregit status --ignored と git ls-files
git status --ignoredは、無視されているファイルを通常の表示に加えて出力します。
$ git status --ignored
On branch main
Ignored files:
(use "git add -f <file>..." to include in what will be committed)
.env
logs/
nothing to commit, working tree cleanマニュアルによると、モードを省略した場合はtraditionalで、無視されたファイルとディレクトリが表示されます。ディレクトリごと無視されている場合はlogs/のようにディレクトリ単位でまとめられます。中のファイルを1つずつ見たいときはgit ls-filesを使います。
$ git ls-files -o -i --exclude-standard
.env
logs/app.log
logs/error.log-oは未追跡のファイルを対象にする指定です。こうした確認用のコマンドを短い名前で呼べるようにしておく方法は、Gitのよく使うコマンドとエイリアスで紹介しています。
逆の悩み: 無視されているファイルを追加したい
git add -f
無視されているファイルをgit addしようとすると、次のように拒否されます。
$ git add logs/keep.log
The following paths are ignored by one of your .gitignore files:
logs
hint: Use -f if you really want to add them.メッセージのとおり、-f(--force)を付けると追加できます。マニュアルの説明は「本来は無視されるファイルの追加を許可する」です。
$ git add -f logs/keep.log
$ git status --short
A logs/keep.log一度追加すると、そのファイルは追跡済みになります。原因1で見たとおり、追跡済みのファイルに.gitignoreは効かないので、以後の変更は通常どおりgit statusに表示されます。実際に、追加したあとで書き換えるとAM logs/keep.logと表示されました。
ただし、-fでの追加は他の人から見ると「.gitignoreに書いてあるのに追跡されている」状態になり、理由が伝わりにくくなります。継続的に管理するファイルなら、否定パターンで.gitignoreに明示するほうが意図が残ります。大きなバイナリを無視から外して管理したい場合は、Git LFSの解説も参考にしてください。
空のディレクトリを残したい
Gitのインデックスが記録するのはファイルで、空のディレクトリはgit statusにも表示されません。
$ mkdir -p uploads
$ git status --short
$ touch uploads/.gitkeep
$ git status --short
?? uploads/空のディレクトリをリポジトリに残したいときに.gitkeepという名前のファイルを置くやり方は、広く使われている慣習です。この記事で参照した公式マニュアルの各ページに.gitkeepについての記述はなく、Gitがこの名前を特別扱いするわけではありません。ファイルが1つあればディレクトリが残る、というだけなので、名前は何でも構いません。
ディレクトリは残しつつ中身は無視したい場合は、そのディレクトリに.gitignoreを置く方法があります。
$ cat uploads/.gitignore
*
!.gitignore
$ git check-ignore -v uploads/photo.jpg
uploads/.gitignore:1:* uploads/photo.jpg
$ git status --short
?? uploads/uploads/.gitignore自身だけが追跡対象になり、ほかのファイルは無視されます。
追跡済みファイルの手元の変更だけ無視したい
「設定ファイルはリポジトリに置いたまま、自分の手元で書き換えた分だけgit statusに出したくない」という要望もよくあります。検索するとgit update-index --assume-unchangedや--skip-worktreeが見つかりますが、公式の見解は明確です。
git update-indexのマニュアルのNOTESには、次の内容が書かれています。
- ユーザーはよく、追跡済みファイルへの変更を無視させるためにassume-unchangedビットやskip-worktreeビットを使おうとする
- これは期待どおりには動かない。Gitは特定の操作の際に、ワーキングツリーのファイルをインデックスと照合することがあるためである
- 一般に、Gitは追跡済みファイルへの変更を無視する方法を提供していない。別の解決策を勧める
公式FAQ(gitfaq)の「How do I ignore changes to a tracked file?」も同じ結論で、理由として「Gitがそのファイルを上書きする必要が出たとき、手元の変更が大事なものか捨ててよいものか判断できないので、常に保持する側に倒すしかない」と説明しています。
2つのビットの本来の用途
マニュアルの説明をまとめると、本来の用途は次のとおりです。
| ビット | 本来の用途 |
|---|---|
| assume-unchanged | lstat(2)が遅いファイルシステムで、変更していないファイルの変更チェックを省かせる性能対策。ユーザーが「このファイルは変更しない」と約束するもの |
| skip-worktree | スパースチェックアウト(一部のパスだけをワーキングツリーに置く)を実現するための仕組み。ファイルが存在しないことを変更として扱わせない |
skip-worktreeについては、マニュアルがgit sparse-checkoutコマンドの利用を強く勧めています。
実際に試すとどうなるか
config.iniを手元で書き換えてから、assume-unchangedビットを立ててみます。
$ git update-index --assume-unchanged config.ini
$ git status --short
$ git ls-files -v
H README
h config.inigit statusには出なくなりました。git ls-files -vでは、ビットが立っているファイルが小文字(h)で表示されます。ここで、他の人が同じファイルを変更したコミットを取り込もうとすると、次のようになります。
$ git pull --no-rebase
error: Your local changes to the following files would be overwritten by merge:
config.ini
Please commit your changes or stash them before you merge.
Abortingskip-worktreeビットでも同じ結果でした。さらに、メッセージに従ってgit stashしようとしても、変更が検出されません。
$ git update-index --skip-worktree config.ini
$ git ls-files -v
H README
S config.ini
$ git pull --no-rebase
error: Your local changes to the following files would be overwritten by merge:
config.ini
Please commit your changes or stash them before you merge.
Aborting
$ git stash
No local changes to save「変更があるから取り込めない」と言われるのに、git statusにもgit stashにも変更が出てこない状態です。ビットを立てたことを忘れていると、原因にたどり着くまでに時間がかかります。解除するには--no-skip-worktree(または--no-assume-unchanged)を使います。
$ git update-index --no-skip-worktree config.ini
$ git status --short
M config.ini取り込みで衝突したあとの進め方はgit merge と rebase の違いを参照してください。
公式が勧める方法
マニュアルとFAQが勧めているのは、ひな形のファイルを追跡し、実際に使うファイルは無視する構成です。
config.sample.ini <- リポジトリで追跡する(既定値やひな形)
config.ini <- .gitignore に書く(各自が sample をコピーして編集する)サンプルのファイルをコピーして無視対象の名前で使い、必要ならコピーと書き換えを自動で行うスクリプトをリポジトリに含める、というのがマニュアルの提案です。.env.exampleと.envの組み合わせも同じ考え方です。すでにconfig.iniを追跡してしまっている場合は、原因1の手順(git rm --cached)で追跡を外してから、この構成に切り替えます。
まとめ
.gitignoreは未追跡のファイルにしか効かない。追跡済みのファイルはgit rm --cached(ディレクトリは-r付き)で外してコミットする- 追跡を外したコミットを他の人が取り込むと、その人の手元のファイルは削除される。書き換えていた場合は
pullが中止される - 過去のコミットには内容が残る。秘密情報をpush済みなら、履歴の書き換えより先に失効とローテーションを行う
- 先頭か途中にスラッシュがあるパターンは
.gitignoreの位置からの相対パス、スラッシュがないパターンはどの階層でも一致、末尾のスラッシュはディレクトリ限定 - 親ディレクトリを除外していると
!で再包含できない。logs/ではなくlogs/*のように中身を除外する - 行頭の空白と行の途中のコメントは、パターンを壊す
- 無視設定は
.gitignore、.git/info/exclude、core.excludesFileの順に優先される。出どころはgit check-ignore -vの左端で分かる - UTF-16で保存された
.gitignoreは、手元の検証では効かなかった。UTF-8で保存し直す - 追跡済みファイルの手元の変更を無視する方法をGitは提供していない。ひな形を追跡し、実ファイルを無視する構成にする
迷ったときは、まずgit check-ignore -vを実行し、出力がなければ--no-indexを付けてもう一度実行します。この2回で「追跡済みなのか、パターンが一致していないのか、別の設定に由来するのか」が切り分けられます。
参考
- gitignore(Git公式マニュアル)
- git-check-ignore(Git公式マニュアル)
- git-rm(Git公式マニュアル)
- git-ls-files(Git公式マニュアル)
- git-status(Git公式マニュアル)
- git-add(Git公式マニュアル)
- git-update-index(Git公式マニュアル)
- git-sparse-checkout(Git公式マニュアル)
- git-config(Git公式マニュアル。core.excludesFile、core.ignoreCase)
- gitfaq(Git公式FAQ)
- Ignoring files(GitHub Docs)
- Removing sensitive data from a repository(GitHub Docs)
- git-filter-repo(GitHubリポジトリ)
- about_Character_Encoding(Microsoft Learn、PowerShell 5.1)


