Синтаксис .gitignore: glob, отрицание, иерархия
.gitignore — самый часто используемый и при этом самый часто непонятый файл в Git. Junior DE открывает его, видит десяток строк типа *.pyc и .venv/, что-то добавляет, что-то комментирует — и не понимает, почему .env всё равно попадает в коммит, а data/raw/ — нет.
В этом уроке мы разберём весь синтаксис .gitignore с нуля до уровня “пишу свой шаблон под Python ETL и понимаю каждый символ”. Покажем три ключевые тонкости, на которых ломаются все: trailing / для директорий, leading / для root-only, и ! для отрицания. Закроем уроком про иерархию из трёх уровней (global -> repo -> per-subdir) и debugging-команду git check-ignore -v.
Зачем .gitignore вообще
Git по умолчанию следит за всеми файлами в working tree. Это правильное поведение — Git не должен решать за тебя, что важно, а что нет. Но на практике в любом проекте есть файлы, которые не должны оказаться в репозитории:
Каждая категория — это отдельная боль. Сгенерированное замусоривает diff (каждый запуск Python создаёт новые __pycache__). Секреты — это утечки в публичный репо (см. модуль 18). OS-мусор раздражает коллег с другой ОС. А большие данные раздувают репо до гигабайт (это тема модуля 15 про LFS).
.gitignore — это white-list исключений: “Git, не следи за этим, даже если я случайно сделаю git add .”.
Базовый синтаксис: pattern -> match
Файл .gitignore — это список паттернов, по одному на строку. Пустые строки и строки, начинающиеся с # — комментарии.
# Это комментарий — Git его игнорирует
# Просто имя файла — игнорировать ВЕЗДЕ в дереве
config.local.py
# Расширение — игнорировать все .pyc
*.pyc
# Имя директории с trailing slash — игнорировать директорию
node_modules/
Под капотом это glob-паттерны (как в shell, но с нюансами). Главные мета-символы:
| Символ | Значение |
|---|---|
* | любое количество символов, кроме / |
? | один любой символ, кроме / |
[abc] | один символ из набора |
** | любое количество директорий (рекурсия) |
! | отрицание паттерна |
/ | разделитель пути; имеет смысл в начале и конце |
# | комментарий |
Звучит сухо. Давайте на примерах посмотрим, как именно это работает, потому что каждое из этих правил имеет хитрость, на которой ломаются джуны.
Хитрость 1: trailing slash — это директория
# Без слеша — игнорировать ЛЮБОЙ file или directory с именем "logs"
logs
# Со слешем — игнорировать ТОЛЬКО directory с именем "logs"
logs/
Разница важна. В Python-проекте у тебя может быть и файл logs.py, и директория logs/. Если ты напишешь просто logs, Git будет игнорировать оба. Если напишешь logs/ — только директорию.
Привычка: для директорий всегда ставь trailing slash. Это явное намерение и защищает от случайного матчинга файлов.
Хитрость 2: leading slash — это якорь к корню
# Без leading slash — игнорировать "config.py" ВЕЗДЕ
config.py
# С leading slash — только в корне репо
/config.py
Это критично для проектов с подобными именами в разных местах. Например:
project/
├── config.py ← хотим игнорировать (root config с секретами)
├── data/
│ └── config.py ← НЕ хотим игнорировать (data layer config)
└── api/
└── config.py ← НЕ хотим игнорировать (api config)
Если ты напишешь config.py — Git проигнорирует все три. Если напишешь /config.py — только корневой.
Хитрость 3: ** для глубокой рекурсии
* в Git не пересекает границы директорий. То есть *.log найдёт app.log в корне и в любой поддиректории, но не nested/deep/app.log через многоуровневую структуру в специфических случаях. Для явной рекурсии используется **.
# Любые .log файлы где угодно (на самом деле работает по дефолту)
*.log
# Все файлы в logs/ независимо от глубины
logs/**
# Все .py файлы в любой subdirectory под src/
src/**/*.py
# Все __pycache__ на любом уровне
**/__pycache__/
В реальности конкретно *.pyc будет матчиться рекурсивно и без ** — потому что паттерн без / означает “имя файла где угодно”. Но если паттерн содержит /, Git становится строже:
# Только prod_data на самом верху data/
data/prod_data/
# prod_data на любой глубине под data/
data/**/prod_data/
Простое правило: если у тебя в паттерне есть / где-то кроме начала и конца — Git трактует его как путь относительно .gitignore. Без ** он не уйдёт глубже одной директории. Когда сомневаешься — поставь ** явно.
Хитрость 4: ! для отрицания
! отменяет предыдущий ignore. Это позволяет делать “игнорировать всё, кроме одного”:
# Игнорировать все .env файлы
.env*
# Но НЕ .env.example (он должен быть в репо как шаблон)
!.env.example
Порядок имеет значение. Git идёт по паттернам сверху вниз, и каждый следующий может переопределить решение предыдущего. Если поменять местами:
!.env.example
.env* ← теперь это перекроет !.env.example, .env.example БУДЕТ игнорироваться
Самый частый use case — “игнорируй всё в директории, кроме одного файла”:
# Игнорировать всю data/
data/*
# Но не data/README.md (объясняет структуру)
!data/README.md
Ограничение: если родительская директория уже игнорируется, файлы внутри нельзя “де-игнорировать” обратно. Например, data/ (с trailing slash) игнорирует всю директорию, и !data/README.md уже не сработает. Чтобы было можно отменить — игнорируй содержимое, а не саму директорию: data/* + !data/README.md.
Иерархия .gitignore: три уровня
В одном проекте может быть несколько .gitignore, и они работают вместе. Git ищет правила в трёх местах:
1. Global .gitignore — твоя машина, личные привычки. Здесь должно быть только то, что специфично для твоего окружения, а не для проекта.
# Настроить глобальный .gitignore (Git подхватит автоматически)
git config --global core.excludesfile ~/.gitignore_global
# Положить туда то, что специфично для тебя
echo ".DS_Store" >> ~/.gitignore_global
echo ".idea/" >> ~/.gitignore_global
echo "*.swp" >> ~/.gitignore_global
Что сюда: OS-мусор (.DS_Store на macOS), IDE-конфиги (.idea/, .vscode/), редактор swap-файлы. То, что зависит от твоей машины, а не от проекта.
2. Repo .gitignore — главный файл, который видят коллеги. Сюда — всё, что специфично для проекта: язык, фреймворк, build-артефакты.
3. Per-subdir .gitignore — для специфики поддиректории. Например, data/.gitignore может игнорировать все CSV, но позволять README.md. Это удобнее, чем громоздить data/**/*.csv в корневом.
Правило: что должно быть у всех в команде — в repo .gitignore. Что должно быть у тебя — в global .gitignore. Не клади .idea/ в repo .gitignore коллективного проекта (это твой выбор IDE, не их). Не клади .env в global .gitignore (это правило проекта, а не твоё личное).
Debugging: почему файл игнорируется?
Самая частая ситуация: добавил файл в .gitignore, всё равно попадает в коммит. Или наоборот: думал, что игнорируется, а Git его не видит.
Команда git check-ignore -v <file> показывает, какое именно правило матчит файл:
$ git check-ignore -v .env
.gitignore:5:.env* .env
$ git check-ignore -v data/raw/dataset.csv
.gitignore:12:data/raw/ data/raw/dataset.csv
$ git check-ignore -v src/main.py
# Тишина — значит файл НЕ игнорируется
Расшифровка: file:line:pattern matched. То есть для .env совпадение нашлось в .gitignore строка 5 — паттерн .env*.
Без -v команда просто скажет, игнорируется или нет (по exit code). С -v — покажет, почему.
Главный gotcha: если файл уже зафиксирован в Git (был добавлен через git add до того, как попал в .gitignore), .gitignore его не уберёт. Нужно явно git rm --cached <file> — удаляет из индекса, оставляет в filesystem. После этого .gitignore начнёт работать.
Пример:
# Случайно закоммитил .env
$ git add .env && git commit -m "oops"
# Добавил в .gitignore — но Git всё равно его видит
$ echo ".env" >> .gitignore
$ git status
# .gitignore изменён, но .env не "ignored"
# Удалить из индекса
$ git rm --cached .env
$ git commit -m "remove .env from tracking"
# Теперь .gitignore работает
Hands-on: .gitignore для Python ETL проекта
Соберём .gitignore для типичного DE-репо — Python ETL c Jupyter notebooks и raw data:
# === Python ===
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
# Виртуальные окружения
.venv/
venv/
env/
.python-version
# Distribution / packaging
build/
dist/
*.egg-info/
*.egg
pip-log.txt
pip-delete-this-directory.txt
# Coverage и тесты
htmlcov/
.tox/
.coverage
.coverage.*
.pytest_cache/
.mypy_cache/
.ruff_cache/
# === Jupyter ===
.ipynb_checkpoints/
*.ipynb_checkpoints
# === Секреты ===
.env
.env.local
.env.*.local
!.env.example
secrets.yaml
credentials.json
*.pem
# === Данные ===
# Все raw данные — никогда в Git
data/raw/
data/interim/
data/processed/
# Но эти файлы — README — оставить
!data/raw/README.md
!data/interim/README.md
!data/processed/README.md
# Локальные дампы и большие файлы
*.parquet
*.csv
!data/samples/*.csv
*.duckdb
*.sqlite
# === Airflow ===
logs/
plugins/__pycache__/
airflow.db
airflow.cfg
# === DBT ===
target/
dbt_packages/
logs/
# === IDE (если команда договорилась) ===
.vscode/settings.json
.idea/
# === OS ===
.DS_Store
Thumbs.db
desktop.ini
Разберём логику секций:
- Python —
__pycache__/,.venv/, pyc-файлы. Стандарт. - Jupyter —
.ipynb_checkpoints/создаётся jupyter автоматически каждые N минут. В Git не нужен. - Секреты —
.env*всё, кроме.env.example. Это критично, мы вернёмся к этому в модуле 18. - Данные —
data/raw/целиком исключается, но README в каждой поддиректории остаётся. Альтернатива —data/**/*.csv+!data/samples/*.csv(sample-данные для тестов оставить). - Airflow/DBT — логи и кэши специфичных инструментов.
Для secret-файлов всегда держи .env.example в репо с пустыми значениями — это документация того, какие переменные ожидаются. Реальный .env — игнорируется.
Попробуй сам
Запусти этот сценарий — он показывает все ключевые точки за 3 минуты:
# Создай тестовый репо
mkdir gitignore-demo && cd gitignore-demo
git init
# Создай файлы и директории
mkdir -p data/raw data/samples logs src
touch .env .env.example
touch data/raw/big.csv data/samples/test.csv
touch logs/app.log src/main.py src/main.pyc
touch .DS_Store
# Посмотри — что Git видит
git status -s
# ?? .DS_Store
# ?? .env
# ?? .env.example
# ?? data/
# ?? logs/
# ?? src/
# Создай .gitignore
cat > .gitignore <<'EOF'
.DS_Store
.env
!.env.example
*.pyc
logs/
data/raw/
EOF
# Теперь Git видит только то, что нужно
git status -s
# ?? .env.example
# ?? .gitignore
# ?? data/samples/
# ?? src/main.py
# Проверь — почему .env игнорируется?
git check-ignore -v .env
# .gitignore:2:.env .env
# Почему data/raw/ игнорируется?
git check-ignore -v data/raw/big.csv
# .gitignore:6:data/raw/ data/raw/big.csv
# А почему data/samples/test.csv НЕ игнорируется?
git check-ignore -v data/samples/test.csv
# (тишина — не игнорируется)
Это базовая mental модель. В следующем уроке посмотрим на готовые шаблоны от GitHub, которые покрывают 90% случаев.
.gitignore для dbt-проекта: что не коммитить