Learning Platform
Глоссарий Troubleshooting
Урок 15.03 · 22 мин
Средний
GitgitattributesEOLmergelinguist

.gitattributes: per-file поведение, merge стратегии, linguist

.gitignore говорит “не следи за этим файлом”. .gitattributes — наоборот: “следи за этим файлом особенным образом”. Это конфиг, который меняет поведение Git для отдельных файлов или паттернов: как делать line endings, как мержить, как считать diff, как помечать в статистике GitHub.

Junior DE редко открывает этот файл, и часто из-за этого получает странные баги: ноутбуки взрываются при merge, скрипты не работают на Windows, репо считает 80% кода как JavaScript (хотя там 90% Python). В этом уроке разберём ключевые директивы и как они влияют на твою повседневную работу.


Где живёт .gitattributes

Как и .gitignore, .gitattributes — это текстовый файл с паттернами. Кладётся в:

  • .gitattributes в корне репо — общий, версионируется
  • .gitattributes в поддиректориях — для специфики
  • ~/.config/git/attributes — глобальный (редко используется)
  • .git/info/attributes — локальный для репо, не versioned

В 99% случаев тебе нужен один файл в корне репо. Этот файл commit-ишь, и его правила распространяются на всех, кто склонирует.


Базовый синтаксис

Каждая строка — паттерн + набор атрибутов:

# Паттерн         атрибут1=значение  атрибут2  -атрибут3

# Все .sh файлы — текст с LF
*.sh              text eol=lf

# Бинарные файлы — не текстовые, не делать diff
*.parquet         binary

# Notebooks — специальный merge driver
*.ipynb           merge=union

# Скрытые от языковой статистики GitHub
*.generated.py    linguist-generated=true

# Не включать в git archive
docs/             export-ignore
.github/          export-ignore

Паттерны работают по тем же правилам, что и в .gitignore: glob, **, leading/trailing slash. Атрибуты — это пары key=value или просто key (boolean true), или -key (boolean false).

TIP

.gitattributes — это конфигурация поведения Git, а не файлов. Файлы остаются теми же, меняется то, как Git их обрабатывает: checkout, diff, merge, export.


text и eol: line endings и текстовый режим

Самые важные атрибуты для cross-platform проектов. Подробно их разбираем в следующем уроке, тут — обзор.

# Авто-определение текста + всегда LF при checkout
*                 text=auto eol=lf

# Конкретно shell-скрипты — всегда LF
*.sh              text eol=lf

# Bat-файлы (только для Windows) — CRLF
*.bat             text eol=crlf

# Бинари — не трогать
*.parquet         binary
*.png             binary

Что делает:

  • text — Git считает файл текстовым, нормализует line endings при commit и при checkout
  • eol=lf — при checkout всегда LF, независимо от ОС
  • binary — синоним -text -diff, Git не трогает байты, не делает diff

Без .gitattributes все эти решения зависят от глобальной настройки core.autocrlf, которая у каждого может быть своя. Это рецепт катастрофы — об этом подробно следующий урок.


merge: стратегии слияния

По умолчанию Git мержит файлы по three-way merge (модуль 7). Для некоторых файлов это не работает — например, ноутбуки или generated файлы. .gitattributes позволяет указать другую стратегию.

# Notebooks — стратегия union (берёт обе версии, не пытается merge)
*.ipynb           merge=union

# Lock-файлы pip/poetry — пусть всегда переописывается из ours/theirs
poetry.lock       merge=theirs
package-lock.json merge=ours

# CHANGELOG — простое склеивание
CHANGELOG.md      merge=union

Доступные built-in стратегии:

СтратегияПоведение
textСтандартный three-way merge (default)
binaryНе мержить, конфликт всегда
unionВзять обе стороны, склеить без проверки логики

union особенно полезен для CHANGELOG, requirements.txt, добавляемых-only списков — Git берёт изменения от обоих и склеивает. Может дать дубликаты, но избегает конфликтов.

Стратегии merge: text vs union vs binary
text
union
binary

Кастомный merge driver

Для notebook-ов union — не идеал (получаешь сломанный JSON). Лучше — кастомный driver через nbmerge или nbdime. Это тема урока 14.04 (notebook workflows), но синтаксис:

*.ipynb           merge=jupyter
# Конфиг для driver — в .git/config или ~/.gitconfig
git config merge.jupyter.driver "nbdime merge --jupyter --input %A %O %B %A"
git config merge.jupyter.name "Jupyter notebooks merge driver"

%A — наш (HEAD), %B — их (incoming), %O — общий предок, %P — путь к файлу. Driver получает все три версии, должен оставить результат в %A и вернуть 0 (success) или ≠0 (конфликт).


diff: кастомный diff driver

Аналогично merge, можно настроить, как Git считает diff для определённых файлов. Это про человеко-читаемые diff-ы.

# Notebooks — diff через nbdime
*.ipynb           diff=jupyter

# Markdown — словесный diff (по словам, не символам)
*.md              diff=markdown

# CSV — табличный diff
*.csv             diff=csv

# Python — учитывать функции при контексте
*.py              diff=python

Конфигурация driver-а:

# Diff для notebooks через nbdime
git config diff.jupyter.command "nbdime diff"

# Markdown через difft (difftastic)
git config diff.markdown.command "difft"

Самый полезный встроенный driver — diff=python. Он умеет распознавать имена функций и классов в Python для контекста в hunk header:

@@ -10,7 +10,7 @@ def extract_data(source: str) -> DataFrame:
     conn = create_engine(source)
-    query = "SELECT * FROM users"
+    query = "SELECT id, name FROM users"

Видишь def extract_data(...) в header? Это работа diff=python. Без него было бы просто @@ -10,7 +10,7 @@ без подсказки, в какой функции изменение.

Git поддерживает встроенные diff для популярных языков: python, ruby, bash, go, rust, java, csharp, cpp, и пары других. Просто прописываешь в .gitattributes:

*.py              diff=python
*.go              diff=golang
*.rs              diff=rust

И функция в hunk header начинает работать сразу.


linguist: что GitHub считает за что

GitHub справа от репо показывает language statistics — какой процент кода на каком языке. Это делает linguist — open-source инструмент, который классифицирует файлы по расширению/содержанию.

Проблема: linguist считает всё. Включая сгенерированный код, vendored библиотеки и документацию. В DE-проекте может быть так:

  • Реальный код: 50 Python файлов, 5000 строк
  • Notebooks: 20 .ipynb, выглядят как JSON (huge), считаются как Jupyter Notebook
  • Generated SQL: 200 .sql файлов от DBT
  • Vendored .py от старого pip install: 100MB

Результат: GitHub утверждает, что репо на 80% Jupyter и 15% SQL, хотя реально — 95% твоей работы в Python. Это путает потенциальных контрибьюторов.

Решение — linguist-* атрибуты:

# Скрыть из статистики — generated файлы
**/migrations/*.py        linguist-generated=true
*.min.js                  linguist-generated=true
dist/                     linguist-generated=true

# Скрыть vendored библиотеки (третьесторонний код)
vendor/                   linguist-vendored=true
third_party/              linguist-vendored=true

# Сказать "это документация", не основной код
docs/                     linguist-documentation=true

# Сказать "это data, не код"
fixtures/                 linguist-detectable=false
*.sample.csv              linguist-detectable=false

После commit-а .gitattributes и push на GitHub статистика пересчитывается (с задержкой 10-30 мин).

NOTE

Для opensource проектов это очень важно. Например, какой-нибудь dbt-проект на 70% — это generated SQL, на 30% — модели и macros. Без linguist-generated в .gitattributes GitHub утверждает, что проект на 99% на SQL. С linguist-generated — показывает реальную картину: Python + YAML + SQL в нормальной пропорции.


export-ignore: что не попадает в git archive

git archive — команда, которая упаковывает репо в tar/zip без .git/ директории. Используется для релизов: “вот версия 1.2.0 как .tar.gz”.

По умолчанию архив содержит всё, что в working tree. Часто это не нужно: тесты, документация, CI-конфиги — лишнее для пользователей пакета.

# Не включать в архив релиза
.github/            export-ignore
.gitattributes      export-ignore
.gitignore          export-ignore
tests/              export-ignore
docs/               export-ignore
.pre-commit-config.yaml  export-ignore

Теперь:

$ git archive --format=tar.gz --output=release-1.0.tar.gz HEAD

Получишь tar.gz, в котором уже не будет tests/, .github/ и прочего. Только то, что нужно для использования продукта.

Для DE-проектов это особенно полезно при подготовке релизных Airflow DAG-bundle или dbt-пакета.


ident, filter, encoding: остальные атрибуты

Reference list для полноты:

  • ident — Git подставляет $Id: <sha>$ в файл при checkout. Редко используется в 2026.
  • filter=X — кастомный clean/smudge filter (используется LFS, см. модуль 15).
  • encoding=UTF-16 — для не-UTF-8 файлов (Windows проекты иногда).
  • working-tree-encoding=cp1251 — то же самое.

Для DE проектов в 2026 они почти не нужны. Самые важные — text/eol, merge, diff, linguist-*, export-ignore.


Полный пример .gitattributes для Python DE проекта

# === Line endings ===
* text=auto eol=lf

# Шелл-скрипты — всегда LF
*.sh        text eol=lf
*.bash      text eol=lf

# Windows batch — всегда CRLF
*.bat       text eol=crlf
*.cmd       text eol=crlf

# === Бинари ===
*.png       binary
*.jpg       binary
*.parquet   binary
*.duckdb    binary
*.sqlite    binary

# === Diff drivers ===
*.py        diff=python

# === Merge стратегии ===
# Notebooks — кастомный merge через nbdime (см. модуль 15)
*.ipynb     merge=jupyter

# Lock-файлы — пусть theirs перезаписывает (наш lock устарел при pull)
poetry.lock         merge=theirs
package-lock.json   merge=theirs
uv.lock             merge=theirs

# CHANGELOG — union для безконфликтного добавления
CHANGELOG.md        merge=union

# === Linguist (GitHub статистика) ===
*.sql               linguist-generated=true     # DBT generated SQL
docs/               linguist-documentation=true
tests/fixtures/     linguist-detectable=false
*.csv               linguist-detectable=false

# === export-ignore (git archive) ===
.github/                    export-ignore
.gitattributes              export-ignore
.gitignore                  export-ignore
tests/                      export-ignore
.pre-commit-config.yaml     export-ignore

Попробуй сам: linguist в действии

# Создай тестовый репо
mkdir attributes-demo && cd attributes-demo
git init

# Создай "реальный" Python код
mkdir src
cat > src/main.py <<'EOF'
def main():
    print("hello")
EOF

# Создай сгенерированные SQL (имитируем DBT)
mkdir target
for i in 1 2 3 4 5; do
    cat > target/model_$i.sql <<EOF
-- generated by dbt
SELECT * FROM model_$i WHERE created_at > '2026-01-01';
EOF
done

# Без .gitattributes — linguist будет считать SQL 90% репо
git add .
git commit -m "init"

# Создай .gitattributes — пометим target/ как generated
cat > .gitattributes <<'EOF'
target/    linguist-generated=true
EOF

git add .gitattributes
git commit -m "chore: mark target/ as generated"

# После push на GitHub — статистика пересчитается через 10-30 мин,
# и target/ перестанет учитываться. Python будет 100%.

# Проверь, что атрибуты применены к файлам
git check-attr -a -- target/model_1.sql
# target/model_1.sql: linguist-generated: set

git check-attr -a -- src/main.py
# (пусто — нет атрибутов)

git check-attr — аналог git check-ignore. Показывает, какие атрибуты Git применит к данному файлу.

# Какие атрибуты у конкретного файла?
git check-attr -a -- src/main.py
# src/main.py: diff: python

# Все файлы и их атрибуты
git ls-files | xargs git check-attr -a --

  • Модуль 14, урок 4 (notebook workflows) — конкретные merge=jupyter и driver для .ipynb
  • Модуль 14, урок 2 (Git LFS) — атрибут filter=lfs для больших файлов
  • Модуль 17 (секреты) — атрибут filter для git-crypt (опционально)

.gitattributes — это инфраструктурный файл. Один раз настроил под проект — годами работает в фоне.


.gitignore и .gitattributes в dbt: языковая статистика и diff
Проверка знанийKnowledge check
У тебя в репо DBT проект: 50 .sql моделей (написанных тобой), 500 generated .sql в target/ (DBT их генерит) и 30 .py macros. GitHub показывает language stats: 90% SQL, 10% Python. Что не так и как починить?
ОтветAnswer
GitHub считает все .sql файлы — и твои модели, и DBT-generated в target/. На самом деле generated файлы — не твой код, и они раздувают статистику. Решение — в `.gitattributes`: `target/ linguist-generated=true`. После commit + push на GitHub linguist пересчитает: твои 50 моделей + 30 .py macros. Статистика покажет нормальную пропорцию SQL/Python. Дополнительные полезные атрибуты для DBT: `*.yml diff=yaml` для лучшего diff yaml-конфигов, `dbt_packages/ linguist-vendored=true` для скрытия third-party packages. `target/` вообще обычно должен быть в .gitignore — но если по какой-то причине его всё же коммитят, linguist-generated спасает статистику.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. В чём принципиальная разница между .gitignore и .gitattributes?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4