Ты открываешь 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 - что положить тебе на диск. Атрибут
Код: Выделить всё
textКод: Выделить всё
eolСамый частый и правильный старт - значение auto:
Код: Выделить всё
* text=auto
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Тонкость: указывать
Код: Выделить всё
text eol=lfКод: Выделить всё
eol=lfКод: Выделить всё
textБинарники: -text -diff, чтобы Git не лез грязными руками
PNG, JAR, шрифт, sqlite-база - всё это бинарь. Если Git по ошибке решит, что это текст, он попытается нормализовать окончания и испортит файл (внутри JPEG случайно встретится байт 0x0D - и Git его перепишет). Ещё он будет пытаться показывать diff бинаря в виде строк (бессмысленный мусор на полэкрана) и при merge - сливать построчно, гарантированно ломая файл. Поэтому бинарники надо маркировать явно.
Есть макрос-сокращение
Код: Выделить всё
binaryКод: Выделить всё
-text -diffКод: Выделить всё
*.png binary
*.jpg binary
*.pdf binary
*.zip binary
*.jar binary
*.woff2 binary
*.sqlite binary
Код: Выделить всё
-textКод: Выделить всё
-diffКастомные diff и merge драйверы: диффать недиффуемое
Иногда бинарь хочется диффать осмысленно. Git позволяет подключить внешний инструмент через атрибут
Код: Выделить всё
diff=ИМЯКод: Выделить всё
*.docx diff=docx
*.pdf diff=pdf
Код: Выделить всё
[diff "docx"]
textconv = pandoc --to=plain
[diff "pdf"]
textconv = pdftotext
binary = true
Код: Выделить всё
merge=ИМЯexport-ignore и linguist: атрибуты для archive и GitHub
.gitattributes управляет не только строками.
Код: Выделить всё
export-ignoreКод: Выделить всё
/tests export-ignore
/.github export-ignore
.gitattributes export-ignore
*.md export-ignore
Код: Выделить всё
linguist-vendoredКод: Выделить всё
linguist-generatedКод: Выделить всё
linguist-languageКод: Выделить всё
vendor/** linguist-vendored
dist/** linguist-generated
*.tpl linguist-language=Smarty
docs/** linguist-documentation
Связка с Git LFS через filter
Большие бинарники (видео, датасеты, психд-макеты) не место в обычной истории - каждый их байт навсегда осядет в .git и раздует клон до гигабайтов. Git LFS подменяет их указателями. Технически это тоже атрибут - LFS прописывает себя как
Код: Выделить всё
filterКод: Выделить всё
*.psd filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
Код: Выделить всё
filter=lfsКод: Выделить всё
-textГотовый шаблон .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. Бывает пишут и удивляются, что autocrlf где-то всё равно вмешался. Пиши явный
Код: Выделить всё
*.sh eol=lf, не оставляй двусмысленности.Код: Выделить всё
text eol=lf - Забыли -text у LFS/бинарей. filter=lfs без -text - и Git попытается нормализовать строки в указателе или самом файле. Всегда -text для бинарного.
- Редактор молча ставит CRLF. .gitattributes починит репо, но VS Code/блокнот может сохранять файл с CRLF снова. Включи .editorconfig (end_of_line = lf) в пару к gitattributes - они закрывают разные слои: editorconfig правит редактор, gitattributes правит Git.
Допустим, история уже грязная: половина файлов с CRLF. Ты добавил
Код: Выделить всё
* text=autoКод: Выделить всё
$ 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 diff --cached --stat | tail -3
src/utils.js | 120 ++++++++++++++++++------------------
README.md | 44 +++++++--------
84 files changed, 0 insertions(+), 0 deletions(-)
Важно: ренормализация - это коммит, трогающий все файлы. Делай его отдельным коммитом без других правок, предупреди команду, и лучше прямо перед ним влей или закрой открытые ветки/PR - иначе будет много шумных конфликтов. После - все на одной странице, окончания едины.
Мини-лаба (повтори руками)
- Создай тестовый репо: (поставь main:
Код: Выделить всё
git init lab-eol && cd lab-eol).Код: Выделить всё
git switch -c main - Создай файл с CRLF принудительно: , закоммить его БЕЗ .gitattributes.
Код: Выделить всё
printf 'a\r\nb\r\nc\r\n' > crlf.txt - Посмотри байты в blob: - увидишь 0d 0a (CRLF попал в историю, плохо).
Код: Выделить всё
git cat-file -p HEAD:crlf.txt | xxd | head - Создай .gitattributes c единственной строкой , закоммить его.
Код: Выделить всё
* text=auto - Выполни и
Код: Выделить всё
git add --renormalize .- crlf.txt отметится Modified.Код: Выделить всё
git status - Закоммить, снова глянь - теперь только 0a (LF). Окончания нормализованы в репо.
Код: Выделить всё
git cat-file -p HEAD:crlf.txt | xxd | head - Добавь правило , создай *.bin с нулевыми байтами, проверь, что git diff пишет "Binary files differ", а не мусор.
Код: Выделить всё
*.bin binary
- Почему .gitattributes надёжнее core.autocrlf для команды - в чём принципиальная разница их области действия?
- В каком виде (LF или CRLF) текст всегда лежит ВНУТРИ репозитория при включённом text=auto, и от чего зависит вид на диске?
- Что именно делает git add --renormalize и почему этой команде не нужно переписывать старую историю?
- Зачем бинарному файлу атрибут -text -diff (или binary) и что сломается без него на merge?
.gitattributes - это договор о байтах, который едет вместе с кодом.
Код: Выделить всё
* text=autoКод: Выделить всё
text eol=lfКод: Выделить всё
binary