.gitignore не работает? Вот настоящее решение
.gitignore не работает? Отслеженные файлы его обходят. Убираем из индекса через git rm --cached, проверяем git check-ignore и рвём цикл коммитов.
TL;DR: если .gitignore не работает, файл почти наверняка уже отслеживается. Git игнорирует файлы, которые никогда не видел, — файл в индексе невосприимчив к любому вашему правилу. Проверка правдой: git check-ignore -v <path>; молчание означает, что путь отслежен и ни одно правило не действует. Лечение: git rm --cached <path>, коммит — и правило начинает работать с этого коммита и дальше. Порядок правил, ловушки с отрицанием и ложные улики от VS Code объясняют оставшиеся случаи.
Почему .gitignore не работает?
Один механизм покрывает большинство случаев: файл закоммитили раньше, чем появилось правило. .gitignore — не фильтр, прячущий файлы от git, а правило о том, что git add должен подбирать из неотслеженного состояния. Попав в индекс, файл отслеживается по содержимому вечно, пока вы явно не снимете его с учёта. Правка .gitignore задним числом на отслеженные файлы не влияет — поэтому классическая последовательность «закоммитил .env, испугался, добавил .env в .gitignore, закоммитил снова» продолжает публиковать секрет при каждом пуше.
Это кусает всех, потому что git почти повсюду: опрос Stack Overflow 2022 года показал использование Git более чем у 93 % профессиональных разработчиков (survey.stackoverflow.co) — и каждый из них рано или поздно пишет правило для файла, закоммиченного на прошлой неделе.
Дальше в посте — меньшинство случаев: ошибки порядка правил, ловушки отрицания, краевые случаи папок и вопрос про IDE. Но сначала прогоните проверку из следующей секции — больше чем в девяти случаях из десяти она закрывает расследование.
Как узнать, какое правило gitignore срабатывает для файла?
git check-ignore — диагностический инструмент, и его молчание — тоже диагноз:
# Печатает сработавшее правило + файл + номер строки, если правило действует
git check-ignore -v debug.log
# .gitignore:3:*.log debug.log
# Ничего не печатает, если правило не действует — файл отслежен (или правил нет)
git check-ignore -v src/.env
# (тишина = .gitignore этот путь не игнорирует, что бы вы ни написали)
# Коды выхода: 0 = игнорируется, 1 = не игнорируется — годится для скриптов
git check-ignore -q debug.log && echo "ignored" || echo "tracked or unruly"
Разбор вывода -v: источник правила (.gitignore, .git/info/exclude или ваш глобальный файл), затем номер-строки:шаблон, затем путь. Если последнее правило вас удивило, вспомните приоритет: побеждает последнее совпавшее правило, поэтому !important.log после *.log возвращает один файл.
Почему gitignore не работает для уже закоммиченных файлов?
Снимите файл с учёта, оставьте на диске, закоммитьте удаление:
# Снять один файл с учёта (локальная копия выживает — --cached трогает только индекс)
git rm --cached .env
git commit -m "stop tracking .env"
# Проверить, что правило теперь действует
git check-ignore -v .env
С этого коммита путь принадлежит .gitignore: правки больше не видны в git status, а git add . не подберёт файл снова. Но в истории файл остаётся — если это был секрет, удаления из последнего коммита мало. Единственное настоящее лекарство — ротация учётных данных; переписывание истории — косметика (а отмена последнего коммита помогает лишь пока плохой коммит ещё на кончике ветки).
Для репозитория, набитого ранее закоммиченным хламом — артефактами сборки, мусором редактора, ранним node_modules — массовое снятие с учёта умещается в две строки:
git rm -r --cached .
git add .
git commit -m "apply .gitignore to tracked files"
Это пересобирает индекс по текущим правилам: игнорируемые пути выпадают, всё прочее добавляется заново без изменений. Diff выглядит драматично (тысячи удалений), но с диска не стирает ничего. Если цель — именно удалить эти файлы, а не только снять с учёта, это территория git clean -fdx: см. git remove untracked files, безопасно.
Git игнорирует файлы, которые никогда не видел. Файл в индексе невосприимчив к
.gitignore— ни одно правило его не «развидит».
Почему gitignore не работает для папки?
Три папочные ловушки:
1. Слэш на конце важен для замысла, а не для совпадения. build и build/ оба совпадают с каталогом, но build/ документирует, что вы имеете в виду только каталог — файл с именем build переживёт его. Правда, на повторном включении симметрия ломается (следующая ловушка).
2. Отрицание не спасёт файлы внутри исключённого каталога. Документация git недвусмысленна: «невозможно снова включить файл, если исключён родительский каталог этого файла» (git-scm.com/docs/gitignore). Это решение ради производительности — git пропускает исключённые каталоги целиком, не заглядывая внутрь. Поэтому это не работает:
build/
!build/keep.me # мёртвое правило — git никогда не заглянет в build/
Лечение — исключать содержимое, а не каталог:
build/*
!build/keep.me # работает — сам build/ остаётся открыт для осмотра
3. Вложенные .gitignore побеждают в своей зоне. Правила из subdir/.gitignore перекрывают корневой файл для путей внутри subdir. Когда git check-ignore -v называет неожиданный источник игнора — обычно вот почему.
Почему gitignore не работает в VS Code?
Почти никогда по причинам VS Code. Панель Source Control читает тот же индекс, что и git, поэтому симптом тот же: файл уже закоммичен, и никакой перезапуск IDE индекс не изменит. Две реальности около VS Code знать стоит:
- Серый в Explorer = игнорируется; оранжевый/жёлтый = отслежен с изменениями. Файл, отображающийся как изменённый после того, как вы его игнорировали, — подтверждение, что он отслежен: применяйте
git rm --cachedвыше. - Отсутствие
.gitignoreв списке изменённых файлов Explorer — признак, что правило работает: файл просто никогда не появится как untracked. Часто жалуются «VS Code игнорирует мой gitignore», когда CLIgit statusрасходится с устаревшим видом SCM: перезагрузите окно (Cmd/Ctrl+Shift+P→ «Reload Window»), прежде чем винить git.
.gitignore против .git/info/exclude и глобального файла: что когда?
| Файл | Область действия | Коммитится? | Для чего |
|---|---|---|---|
.gitignore (репозиторий) |
Все, кто клонирует | Да | Артефакты сборки, зависимости, .env — общие правила |
.git/info/exclude |
Только ваш клон | Нет | Личный мусор: .scratch/, droppings редактора |
core.excludesFile (глобальный) |
Все ваши репозитории | Нет | Мусор ОС: .DS_Store, Thumbs.db, *.swp |
.gitignore + отрицание |
Репозиторий | Да | Повторное включение исключений в отслеживаемых конфигах |
Глобальный файл — тот, который большинство разработчиков не настраивают, а зря:
git config --global core.excludesFile ~/.gitignore_global
printf '.DS_Store
Thumbs.db
*.swp
' >> ~/.gitignore_global
Правило, которое стоит забрать себе: если правило полезно всей команде, оно принадлежит репозиторию; если только вам — exclude или глобальному файлу. Коммит личных игнор-правил — вот как .gitignore разрастается до 300 строк, в которых никто не помнит, какая половина ещё жива.
Как игнорировать изменения уже отслеженного файла?
Иногда отслеженный файл нужен (шаблон конфига, настройки IDE), но локальные правки должны перестать мелькать в git status. Два флага git update-index, ни один из которых не место в командном процессе:
git update-index --skip-worktree config/local.dev # локальные правки замолкают
git update-index --no-skip-worktree config/local.dev # ...и обратно
--skip-worktree — защищаемый вариант: он говорит «моя локальная версия расходится намеренно». Его кузен --assume-unchanged — обещание производительности («этот файл не изменится»), а не механизм игнора, и git может молча его нарушить. Оба флага громко падают при pull, если upstream тоже менял файл; прочные ответы — локальный конфиг через exclude с самого начала или файл-шаблон (config.example), который git отслеживает, а вы копируете.
Проверка gitignore за 30 секунд
git check-ignore -v <path> # какое правило? (тишина = отслежен, правила нет)
git ls-files --error-unmatch <path> # вообще отслеживается?
git rm --cached <path> # снять с учёта, оставить на диске
git commit -m "stop tracking <path>"
git check-ignore -v <path> # правило теперь видно
Диагностика, снятие с учёта, проверка. За каждой жалобой «gitignore not working» стоит один файл в двух ролях — отслеженный с одной стороны индекса, игнорируемый с другой, — и один флаг --cached снимает вторую роль.