.gitattributes: окончания строк, бинарники и атрибуты

Рейтинг: 70.2% · 15 голосов
Самый подробный курс по Git - от первого коммита до объектной базы и packfiles. Терминал и первый репозиторий, ветки и слияние, конфликты, удалённая работа и форджи (GitHub, GitLab), rebase и переписывание истории, спасение кода через reflog и fsck, stash и worktree, bisect и blame, подмодули и монорепо, хуки и подпись коммитов, внутреннее устройство и масштаб. Для новичков и профи. Актуально на 2026 (Git 2.54, дорожная карта 3.0).
Ответить
Аватара пользователя
Pavel_Git
Сообщения: 52
Зарегистрирован: 11 май 2026, 05:31

.gitattributes: окончания строк, бинарники и атрибуты

Сообщение Pavel_Git »

Оглавление курса (52)
  1. Что такое контроль версий и почему победил Git
  2. Установка Git на Windows, macOS и Linux и первая настройка
  3. Терминал и как спрашивать у Git: выживание в командной строке
  4. Ментальная модель Git: три состояния, коммит-снимок и граф
  5. Первый репозиторий: init, add, commit, status
  6. Просмотр изменений: status, diff и индексация по частям
  7. История проекта: git log, диапазоны и поиск
  8. .gitignore: что не должно попасть в репозиторий
  9. .gitattributes: окончания строк, бинарники и атрибуты (вы здесь)
  10. Ветки как указатели: branch, switch и detached HEAD
  11. Слияние веток: fast-forward против трёхстороннего merge
  12. Конфликты слияния: анатомия и уверенное разрешение
  13. Практикум: твой первый день с Git от нуля до push
  14. Удалённые репозитории: remote, fetch, базовый pull и refspec
  15. git push: безопасная отправка и защита от перезаписи
  16. Клонирование: shallow, partial clone, bundle и backfill
  17. GitHub, GitLab и форки: pull request, rulesets и защита веток
  18. Рабочие процессы команды: GitFlow, GitHub Flow, trunk-based
  19. Merge queue и merge trains: безопасное вливание в trunk
  20. Культура code review и Conventional Commits
  21. git reset: три дерева и отмена индексации и коммитов
  22. git revert: безопасная отмена в общей истории
  23. Правка последнего коммита: git commit --amend
  24. git rebase: пересадка истории и золотое правило
  25. Интерактивный rebase: переписать серию коммитов
  26. git cherry-pick: перенос отдельных коммитов
  27. Stacked diffs: цепочка зависимых PR
  28. Массовое переписывание истории: git filter-repo и git replace
  29. git reflog: журнал, который помнит всё
  30. Восстановление утерянных коммитов, веток и файлов
  31. Откат опасных операций: reset --hard, сломанный rebase, плохой merge
  32. git stash: тайник для незавершённой работы
  33. git worktree: несколько рабочих деревьев одного репозитория
  34. git bisect: бинарный поиск регрессии
  35. git blame, mailmap и археология кода
  36. Теги, релизы, archive и bundle
  37. Подмодули: внешние репозитории внутри проекта
  38. Subtree, монорепозитории и sparse-checkout
  39. Git LFS и большие файлы
  40. Вспомогательные команды: clean, mv, rm, restore
  41. Псевдонимы и кастомизация .gitconfig
  42. Хуки Git: автоматизация на клиенте и сервере
  43. Подпись коммитов и тегов: SSH, GPG и Sigstore
  44. Внутреннее устройство I: объектная база Git
  45. Внутреннее устройство II: ссылки, HEAD, notes и reftable
  46. Внутреннее устройство III: индекс, packfiles, gc и обслуживание
  47. От SHA-1 к SHA-256: переход на новый хеш
  48. Огромные репозитории: partial clone, FSMonitor, Scalar
  49. Git в CI/CD: shallow, кеш, merge_group и безопасность
  50. Куда движется Git: дорожная карта 3.0 и экосистема
  51. Профессиональный setup: собираем рабочее окружение Git
  52. 30+ типичных ошибок Git и как из них выбираться
Боль, которую решает .gitattributes

Ты открываешь diff и видишь, что "изменился" весь файл целиком - 400 строк, хотя ты поправил одну. Коллега на Windows закоммитил, ты на Linux подтянул, и git blame теперь показывает его имя на каждой строке. PR раздувается до тысяч "правок", ревью невозможно, history превращается в кашу. Знакомо? Это классика конфликта окончаний строк, и виновата невидимая разница: Windows ставит в конце строки два байта CR+LF (0x0D 0x0A), Unix и macOS - один байт LF (0x0A). Глазами ты этого не видишь, а Git видит каждый байт - для него файл с CRLF и тот же файл с LF это два РАЗНЫХ содержимого, значит другой blob, значит "изменено всё".

Долгое время команды лечили это настройкой core.autocrlf у каждого разработчика. Проблема в том, что это локальная настройка: она живёт в .git/config или в глобальном ~/.gitconfig конкретной машины. Новый человек в команде про неё не знает, забыл выставить - и снова приехал коммит с чужими окончаниями. Настройка не едет вместе с репозиторием.

Решение 2026 года однозначно: окончания строк описываются в файле .gitattributes, который лежит В репозитории, коммитится и едет ко всем. Это источник истины. core.autocrlf становится вторичным аварийным механизмом - если в репо есть .gitattributes с правилами, его указания приоритетнее autocrlf для перечисленных файлов. Один файл правит поведение для всей команды и для всех будущих клонов - в этом вся сила.

Изображение

Как Git вообще преобразует строки: чистый и рабочий вид

Запомни два мира. Рабочее дерево (working tree) - файлы у тебя на диске, в той кодировке и с теми окончаниями, что удобны твоей ОС и редактору. Репозиторий (blob внутри .git/objects) - канонический "чистый" вид. Git гоняет байты между ними через фильтры. Для текста работает встроенный фильтр eol: при check-in (когда ты делаешь git add - превращение в blob) CRLF нормализуется в LF; при checkout (когда blob раскладывается в файл) LF может разворачиваться обратно в CRLF, если так велено.

Ключевое правило: в репозитории текст ВСЕГДА хранится с LF. Это инвариант. Различаются только настройки checkout - что положить тебе на диск. Атрибут включает эту нормализацию, фиксирует, какие окончания класть в рабочее дерево.

Самый частый и правильный старт - значение auto:

Код: Выделить всё

* text=auto
Звёздочка - это паттерн (как в .gitignore), он матчит все пути. text=auto означает: "Git, сам реши - если файл выглядит текстовым, нормализуй его к LF при коммите; если в нём есть нулевые байты и он похож на бинарь, не трогай вообще". Это безопасно: бинарники не пострадают, текст приведётся к LF в истории. Что окажется на диске при checkout - LF на Unix, а на Windows зависит от core.autocrlf/core.eol, но в РЕПО уже всегда LF. История чистая, кросс-платформенные diff-ы перестают взрываться.

eol=lf и eol=crlf: когда нужна жёсткая рука

text=auto хорош как дефолт, но есть файлы, которым окончания критичны и менять их по настройке машины нельзя. Shell-скрипт с CRLF Linux просто не запустит - выдаст "bad interpreter: /bin/bash^M". Bat-файл Windows наоборот хочет CRLF. Тут мы перестаём доверять авто и прибиваем eol гвоздями:

Код: Выделить всё

* text=auto
*.sh text eol=lf
*.bash text eol=lf
*.py text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
Здесь

Код: Выделить всё

text eol=lf
значит: файл текстовый, нормализуй к LF в репо И клади LF на диск НЕЗАВИСИМО от ОС и любых core.autocrlf. То есть .sh-скрипт у разработчика на Windows тоже окажется с LF - именно то, что нужно, чтобы он работал в Docker-контейнере и в CI. eol=lf и eol=crlf побеждают autocrlf полностью - это самый детерминированный режим.

Тонкость: указывать

Код: Выделить всё

text eol=lf
правильнее, чем просто . Голый eol подразумевает text, но писать явно - читабельнее и страхует от краевых случаев. Когда хочешь явно объявить файл текстом без авто-детекта - пиши .

Бинарники: -text -diff, чтобы Git не лез грязными руками

PNG, JAR, шрифт, sqlite-база - всё это бинарь. Если Git по ошибке решит, что это текст, он попытается нормализовать окончания и испортит файл (внутри JPEG случайно встретится байт 0x0D - и Git его перепишет). Ещё он будет пытаться показывать diff бинаря в виде строк (бессмысленный мусор на полэкрана) и при merge - сливать построчно, гарантированно ломая файл. Поэтому бинарники надо маркировать явно.

Есть макрос-сокращение , это синоним

Код: Выделить всё

-text -diff
:

Код: Выделить всё

*.png binary
*.jpg binary
*.pdf binary
*.zip binary
*.jar binary
*.woff2 binary
*.sqlite binary
(минус перед именем - "снять атрибут") выключает любую нормализацию строк: байты blob = байты на диске, один-в-один. говорит "не пытайся текстово диффать": в выводе будет лаконичное "Binary files a/logo.png and b/logo.png differ" вместо километра кракозябр. На merge Git тоже не будет лезть строками - при конфликте честно скажет, что бинарь конфликтует, и оставит решение тебе.

Кастомные diff и merge драйверы: диффать недиффуемое

Иногда бинарь хочется диффать осмысленно. Git позволяет подключить внешний инструмент через атрибут

Код: Выделить всё

diff=ИМЯ
, а сам инструмент описать в конфиге. Классика - человекочитаемый diff для PDF, docx или sqlite. В .gitattributes:

Код: Выделить всё

*.docx diff=docx
*.pdf  diff=pdf
В .gitconfig (или .git/config) описываем, как из бинаря добыть текст для сравнения через textconv:

Код: Выделить всё

[diff "docx"]
    textconv = pandoc --to=plain
[diff "pdf"]
    textconv = pdftotext
    binary = true
Теперь git diff на .docx прогонит обе версии через pandoc и покажет осмысленный текстовый дифф абзацев, а не мусор. Аналогично есть драйверы merge для специфичных форматов (например, файлы локализаций или lock-файлы со своей логикой слияния) - они объявляются

Код: Выделить всё

merge=ИМЯ
и описываются секцией [merge "ИМЯ"] с командой driver. Помни: textconv-драйвер живёт в конфиге, который локален - его надо ставить каждому или раскатывать через скрипт настройки окружения. В .gitattributes едет только ссылка по имени.

export-ignore и linguist: атрибуты для archive и GitHub

.gitattributes управляет не только строками.

Код: Выделить всё

export-ignore
исключает путь из тарбола, который делает git archive (так формируют релизные архивы и пакеты для composer/npm). Незачем тащить в дистрибутив тесты, CI-конфиги и доки:

Код: Выделить всё

/tests       export-ignore
/.github     export-ignore
.gitattributes export-ignore
*.md         export-ignore
GitHub поверх Git читает свой набор атрибутов семейства linguist - они правят статистику языков и подсветку.

Код: Выделить всё

linguist-vendored
прячет сторонний код из счётчика языков,

Код: Выделить всё

linguist-generated
помечает сгенерённое (его сворачивают в diff PR),

Код: Выделить всё

linguist-language
форсит язык для нестандартного расширения:

Код: Выделить всё

vendor/**        linguist-vendored
dist/**          linguist-generated
*.tpl            linguist-language=Smarty
docs/**          linguist-documentation
Это чисто косметика GitHub, на сам Git не влияет, но в реальном проекте экономит нервы ревьюеров - сгенерённые портянки не раздувают diff.

Связка с Git LFS через filter

Большие бинарники (видео, датасеты, психд-макеты) не место в обычной истории - каждый их байт навсегда осядет в .git и раздует клон до гигабайтов. Git LFS подменяет их указателями. Технически это тоже атрибут - LFS прописывает себя как :

Код: Выделить всё

*.psd  filter=lfs diff=lfs merge=lfs -text
*.mp4  filter=lfs diff=lfs merge=lfs -text

Код: Выделить всё

filter=lfs
подключает clean/smudge фильтры LFS: при коммите файл заменяется текстовым указателем (clean), при checkout подтягивается из LFS-хранилища (smudge). в конце обязателен - чтобы Git не трогал окончания у того, что на самом деле бинарь. Обычно эти строки добавляет команда git lfs track сама, но понимать, что это просто атрибут filter, полезно.

Готовый шаблон .gitattributes

Скелет, который можно класть в большинство репозиториев и докручивать под стек:

Код: Выделить всё

# Источник истины по окончаниям. Авто-детект + нормализация к LF.
* text=auto

# Жёстко LF - всё, что исполняется в Unix/CI/Docker.
*.sh   text eol=lf
*.bash text eol=lf
*.py   text eol=lf
*.rb   text eol=lf
*.php  text eol=lf
*.js   text eol=lf
*.json text eol=lf
*.yml  text eol=lf
*.yaml text eol=lf
Dockerfile text eol=lf

# Жёстко CRLF - нативные виндовые скрипты.
*.bat text eol=crlf
*.cmd text eol=crlf

# Бинарники - руками не трогать.
*.png  binary
*.jpg  binary
*.gif  binary
*.ico  binary
*.pdf  binary
*.zip  binary
*.gz   binary
*.woff2 binary

# Чистка релизного архива.
/tests   export-ignore
/.github export-ignore
.gitattributes export-ignore
Грабли смешанных ОС-команд
  • Поздно поставили .gitattributes. Самая частая засада. Добавили правила в репозиторий, где уже лежат файлы с намешанными CRLF/LF - и думаете, что починилось. Нет. Атрибуты влияют только на check-in/checkout БУДУЩИХ операций. Уже закоммиченные blob-ы остаются как есть, пока их кто-то не перезапишет. Рецепт ниже.
  • Голый eol без text. Бывает пишут

    Код: Выделить всё

    *.sh eol=lf
    и удивляются, что autocrlf где-то всё равно вмешался. Пиши явный

    Код: Выделить всё

    text eol=lf
    , не оставляй двусмысленности.
  • Забыли -text у LFS/бинарей. filter=lfs без -text - и Git попытается нормализовать строки в указателе или самом файле. Всегда -text для бинарного.
  • Редактор молча ставит CRLF. .gitattributes починит репо, но VS Code/блокнот может сохранять файл с CRLF снова. Включи .editorconfig (end_of_line = lf) в пару к gitattributes - они закрывают разные слои: editorconfig правит редактор, gitattributes правит Git.
Рецепт renormalize: чиним уже испорченные окончания

Допустим, история уже грязная: половина файлов с CRLF. Ты добавил

Код: Выделить всё

* text=auto
и нужно одним махом привести всё к LF в репозитории, не переписывая старую историю. Команда для этого:

Код: Выделить всё

$ git add --renormalize .
$ git status --short
M  src/app.js
M  src/utils.js
M  README.md
... (изменены все файлы с CRLF)
$ git commit -m "Normalize line endings to LF"
Что делает

Код: Выделить всё

--renormalize
: Git берёт КАЖДЫЙ отслеживаемый файл, пропускает его через текущие .gitattributes так, будто ты только что его добавил, и кладёт нормализованную версию в индекс. Файлы, где ничего не поменялось, остаются в покое; те, где был CRLF, перезаписываются blob-ом с LF и попадают в status как Modified. Один коммит чинит весь репозиторий для всех будущих клонов - история не переписывается (старые коммиты как были, так и есть), просто добавляется один "выпрямляющий" коммит сверху.

Проверь, что нормализация реальная, до коммита:

Код: Выделить всё

$ git diff --cached --stat | tail -3
 src/utils.js | 120 ++++++++++++++++++------------------
 README.md    |  44 +++++++--------
 84 files changed, 0 insertions(+), 0 deletions(-)
Видишь "0 insertions, 0 deletions" при куче изменённых файлов - это и есть подпись чистой ренормализации: содержимое строк то же, поменялись только невидимые байты окончаний. Если бы тут были реальные insertions - значит что-то ещё подмешалось, стоп и разбирайся.

Важно: ренормализация - это коммит, трогающий все файлы. Делай его отдельным коммитом без других правок, предупреди команду, и лучше прямо перед ним влей или закрой открытые ветки/PR - иначе будет много шумных конфликтов. После - все на одной странице, окончания едины.

Мини-лаба (повтори руками)
  • Создай тестовый репо:

    Код: Выделить всё

    git init lab-eol && cd lab-eol
    (поставь main:

    Код: Выделить всё

    git switch -c main
    ).
  • Создай файл с CRLF принудительно:

    Код: Выделить всё

    printf 'a\r\nb\r\nc\r\n' > crlf.txt
    , закоммить его БЕЗ .gitattributes.
  • Посмотри байты в blob:

    Код: Выделить всё

    git cat-file -p HEAD:crlf.txt | xxd | head
    - увидишь 0d 0a (CRLF попал в историю, плохо).
  • Создай .gitattributes c единственной строкой

    Код: Выделить всё

    * text=auto
    , закоммить его.
  • Выполни

    Код: Выделить всё

    git add --renormalize .
    и

    Код: Выделить всё

    git status
    - crlf.txt отметится Modified.
  • Закоммить, снова глянь

    Код: Выделить всё

    git cat-file -p HEAD:crlf.txt | xxd | head
    - теперь только 0a (LF). Окончания нормализованы в репо.
  • Добавь правило

    Код: Выделить всё

    *.bin binary
    , создай *.bin с нулевыми байтами, проверь, что git diff пишет "Binary files differ", а не мусор.
Контрольные вопросы
  • Почему .gitattributes надёжнее core.autocrlf для команды - в чём принципиальная разница их области действия?
  • В каком виде (LF или CRLF) текст всегда лежит ВНУТРИ репозитория при включённом text=auto, и от чего зависит вид на диске?
  • Что именно делает git add --renormalize и почему этой команде не нужно переписывать старую историю?
  • Зачем бинарному файлу атрибут -text -diff (или binary) и что сломается без него на merge?
Итог

.gitattributes - это договор о байтах, который едет вместе с кодом.

Код: Выделить всё

* text=auto
как дефолт,

Код: Выделить всё

text eol=lf
для исполняемого, для бинарей, filter=lfs для тяжёлого, export-ignore для архива. Один файл в репо отменяет лотерею локальных настроек у каждого. А если окончания уже намешаны - git add --renormalize и один коммит выпрямляют всё разом. Поставь это в начале проекта - и diff-ы навсегда перестанут взрываться на ровном месте.
👍4 ❤️ 🔥1 😄 🤔
Аватара пользователя
clickhouse13
Сообщения: 1
Зарегистрирован: 13 май 2026, 12:46

Re: .gitattributes: окончания строк, бинарники и атрибуты

Сообщение clickhouse13 »

Долго не мог понять, почему PR с одной правкой показывал весь файл. text=auto + renormalize и всё встало на места, спасибо.
👍 ❤️1 🔥 😄 🤔
Аватара пользователя
roch
Сообщения: 1
Зарегистрирован: 14 май 2026, 05:57

Re: .gitattributes: окончания строк, бинарники и атрибуты

Сообщение roch »

Вопрос: editorconfig с end_of_line=lf и gitattributes text eol=lf - это дублирование или они реально про разные слои? Из урока понял что про разные, но хочу убедиться.
👍3 ❤️ 🔥1 😄 🤔
Ответить
← Предыдущая глава
.gitignore: что не должно попасть в репозиторий
Следующая глава →
Ветки как указатели: branch, switch и detached HEAD

Все главы курса «Git профессионально: от первого коммита до внутреннего устройства»

Поделиться темой: ✈ Telegram VK
Похожие запросы: .gitignore: как игнорировать файлы

Вернуться в «Git профессионально: от первого коммита до внутреннего устройства»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость