Приоритет правил gitignore и отрицающие шаблоны (!)
Почему побеждает последнее правило, как распределяется приоритет между .gitignore, info/exclude и глобальной настройкой, почему ! не может вернуть файл из исключённого родительского каталога и как это обойти.
Когда правила gitignore ведут себя не так, как ожидалось, причина почти всегда в порядке или в родительском каталоге. В этой статье разобрано, какому из нескольких правил следует git.
Внутри одного файла: побеждает последнее совпавшее правило
Внутри одного .gitignore git читает строки сверху вниз, и результат определяет последний совпавший шаблон. Если он начинается с !, файл не игнорируется, иначе — игнорируется.
*.log
!important.log
important.log совпадает с обеими строками, но последняя — отрицание, поэтому файл отслеживается. Поменяйте порядок — и результат тоже поменяется.
!important.log
*.log
Теперь последнее совпадение для important.log — это *.log, поэтому файл игнорируется. Вот почему при объединении нескольких шаблонов в генераторе порядок выбора — это и есть порядок правил. Например, если вы используете *.jar из шаблона Java вместе с !gradle-wrapper.jar из шаблона Gradle, Gradle должен идти после Java.
Приоритет между несколькими файлами
git читает правила из четырёх источников. Чем выше в списке, тем выше приоритет.
- Шаблоны, переданные в командной строке (в некоторых командах, например
git ls-files --exclude) .gitignoreв том же или в родительском каталоге пути — файл, лежащий глубже, перекрывает вышестоящий$GIT_DIR/info/exclude(обычно.git/info/exclude)- Файл, на который указывает настройка
core.excludesFile(глобальный gitignore)
Например, если в корневом .gitignore есть *.tmp, а в sub/.gitignore — !*.tmp, то sub/y.tmp отслеживается по отрицающему правилу вложенного файла. И наоборот: даже если записать !x в .git/info/exclude, правило x из .gitignore имеет приоритет, и x по-прежнему игнорируется.
$ git check-ignore -v x y.tmp sub/y.tmp
.gitignore:1:x x
.gitignore:2:*.tmp y.tmp
sub/.gitignore:1:!*.tmp sub/y.tmp
Самое важное исключение: из исключённого родительского каталога файл не вернуть
В документации git сказано так: если родительский каталог файла исключён, этот файл нельзя снова включить. Ради производительности git вообще не заглядывает внутрь исключённых каталогов, поэтому правила ! для файлов внутри даже не читаются.
logs/
!logs/keep.txt
С этими правилами logs/keep.txt всё равно игнорируется. git check-ignore -v покажет решающим правилом logs/.
Решение — исключать не сам каталог, а его содержимое.
logs/*
!logs/keep.txt
logs/* не совпадает с самим каталогом logs, поэтому git заходит внутрь и применяет отрицающее правило к keep.txt. На этом принципе построены и /log/* + !/log/.keep в шаблоне Rails, и .vscode/* + !.vscode/settings.json в шаблоне VS Code.
Чтобы вернуть более глубокий путь, нужно по очереди вернуть и каждый промежуточный каталог.
config/*
!config/app/
config/app/*
!config/app/defaults.yml
Шаблон «белого списка» (allowlist)
Тот же принцип работает, когда вместо перечисления игнорируемого хочется перечислить только то, что нужно отслеживать.
# Игнорируем всё в корне
/*
# Возвращаем только нужное
!/.gitignore
!/src/
!/README.md
/* совпадает лишь с отдельными элементами корня и не исключает сам корень, поэтому !/src/ работает. При таком подходе сам .gitignore тоже попадает под /*, так что не забудьте !/.gitignore. Если без этой строки выполнить git status --ignored, .gitignore действительно появится в списке игнорируемых.
Повторяющиеся правила и порядок
Если одно и то же правило встречается несколько раз, более раннее не влияет на результат: более позднее всегда совпадает позже. А вот удаление более позднего дубликата может изменить результат, если между ними есть отрицающее правило.
*.log
!debug.log
*.log
Если здесь удалить последний *.log, debug.log станет отслеживаемым. Генератор на этом сайте распознаёт такие случаи и удаляет поздние дубликаты только если между ними нет отрицающего правила, остальные оставляет.
Источники
- Официальная документация gitignore: PATTERN FORMAT, NOTES
- Официальная документация git-check-ignore
- Дата проверки: 2026-09-23 (примеры проверены в git 2.50.1)