guide

Приоритет правил 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 читает правила из четырёх источников. Чем выше в списке, тем выше приоритет.

  1. Шаблоны, переданные в командной строке (в некоторых командах, например git ls-files --exclude)
  2. .gitignore в том же или в родительском каталоге пути — файл, лежащий глубже, перекрывает вышестоящий
  3. $GIT_DIR/info/exclude (обычно .git/info/exclude)
  4. Файл, на который указывает настройка 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: полный обзор