Line endings: CRLF vs LF, canonical 2026 setup
Если у тебя в команде есть один Windows-разработчик, рано или поздно ты столкнёшься со странным diff: открываешь PR, и Git показывает, что коллега “изменил все 500 строк файла”, хотя реально он поменял одну строку. Или ты запускаешь bash script.sh на Linux, и shell кидает \r: command not found — а коллега утверждает “у меня на Windows всё работает”.
Это line endings — самый недооценённый источник пропавшего времени в cross-platform командах. В этом уроке: что такое LF/CRLF, почему Windows отличается, что такое core.autocrlf, почему он сломан в 2026, и какой каноничный setup через .gitattributes.
История: откуда взялись LF и CRLF
Текстовый файл — это последовательность байт. Когда нужен перенос строки, в файл вставляется специальный символ. Проблема: в разных ОС этот символ — разный.
Корни в эпохе телетайпов: каретка возвращалась в начало строки (CR), потом бумага прокручивалась на строку вниз (LF). Это были два разных физических действия. Unix (и потом всё, что от него произошло — Linux, macOS) решили: “хватит, один символ означает перенос строки”. Microsoft (DOS, Windows) сохранили оба.
В 2026 это всё ещё актуально. Notepad на Windows по умолчанию пишет CRLF. Visual Studio — CRLF. Linux/macOS-инструменты — LF.
Почему это проблема для Git
Git хранит файлы байт-в-байт. Если кто-то закоммитил файл с CRLF, а другой добавил строку и сохранил с LF — Git видит “изменены все строки”, потому что байты в конце каждой строки разные.
- print("hello")\r\n ← было CRLF
+ print("hello")\n ← стало LF (всё та же логика, но байты другие)
- print("world")\r\n
+ print("world")\n
Открываешь PR — diff показывает 100 изменённых строк, хотя коллега всего лишь сохранил файл в другом редакторе. Code review невозможен.
Это первая боль. Вторая — shell scripts: если .sh имеет CRLF, Linux/macOS bash при чтении видит \r как часть имени команды и падает:
$ ./script.sh
./script.sh: line 2: $'\r': command not found
Третья — YAML/Python: иногда CRLF ломает парсинг строк в edge cases. Особенно в multi-line strings.
Решение №1 (старое, сломанное): core.autocrlf
Git исторически решал проблему через настройку core.autocrlf. Идея: “пусть Git автоматически конвертирует line endings при checkout и commit”.
# Для Windows: при checkout — CRLF, при commit — LF
git config --global core.autocrlf true
# Для macOS/Linux: при commit конвертировать в LF, при checkout не трогать
git config --global core.autocrlf input
# Не трогать вообще
git config --global core.autocrlf false
Почему этот подход сломан
Проблема core.autocrlf в том, что это per-машина настройка. У тебя input, у коллеги-Windows true, у третьего вообще false. Каждый что-то получает по-своему, плюс глобальная настройка может конфликтовать с per-репо.
Ещё хуже: autocrlf пытается угадать, какие файлы текстовые. Алгоритм — посмотреть первые несколько байт, если они выглядят как ASCII — текст. Это часто ошибается. Бинарные файлы вроде иконок могут “случайно выглядеть текстом” в первых байтах, и Git их испортит.
Результат: ты не знаешь, что у тебя получится. Может быть, нормализуется. Может быть, нет. Может быть, у коллеги выглядит иначе.
Если ты пользуешься только core.autocrlf без .gitattributes, ты строишь систему на shifting sand. Это работает для personal repo на одной машине. Для team-репо нужен .gitattributes.
Решение №2 (каноничное 2026): .gitattributes
Каноничный подход — указать line endings в .gitattributes, который commit-ится в репо. Тогда все участники получают одинаковое поведение, независимо от их core.autocrlf.
# === Универсальный fallback: все text-файлы хранятся как LF ===
* text=auto eol=lf
# === Конкретные текстовые файлы ===
*.py text eol=lf
*.sh text eol=lf
*.bash text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
*.json text eol=lf
*.md text eol=lf
*.sql text eol=lf
*.txt text eol=lf
*.html text eol=lf
*.css text eol=lf
*.js text eol=lf
*.ts text eol=lf
# === Windows-specific — должны быть CRLF ===
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# === Бинари — не трогать вообще ===
*.png binary
*.jpg binary
*.gif binary
*.ico binary
*.pdf binary
*.zip binary
*.tar.gz binary
*.parquet binary
*.duckdb binary
*.sqlite binary
*.so binary
*.dll binary
*.exe binary
Что это даёт:
- Все в команде получают одинаковое поведение — настройки локального
core.autocrlfигнорируются для файлов, перечисленных в.gitattributes. - Внутренний хранилище Git — всегда LF (этого требует
text=auto eol=lf). - При checkout на любой ОС — все text-файлы получают LF, кроме
.bat/.cmd/.ps1(CRLF — иначе Windows их не запустит). - Бинари — не трогаются, ни при commit, ни при checkout.
* text=auto eol=lf — главная строка
Разберём:
*— паттерн “все файлы”text=auto— Git сам определяет, текстовый файл или нет. Эвристика: посмотрит первые байты, если NUL — бинарь, иначе текст.eol=lf— для текстовых: при commit нормализуй в LF, при checkout оставь LF (на любой ОС).
Это базовый fallback для всех файлов в репо. Дополнительные правила (*.py text eol=lf явно, *.bat text eol=crlf) — для точности и читаемости.
Сначала пиши * text=auto eol=lf как первое правило. Потом — конкретные исключения и binary-файлы. Это безопасный default.
Что делать после изменения .gitattributes в существующем репо
Если ты добавил .gitattributes в существующий репо, где раньше line endings были смешанные, нужно перенормализовать всю историю (а точнее — рабочую копию для будущих commit-ов):
# 1. Commit .gitattributes
git add .gitattributes
git commit -m "chore: add .gitattributes for line endings"
# 2. Перенормализовать все файлы в working tree по новым правилам
git add --renormalize .
# 3. Если что-то изменилось — commit
git status
# Если есть modified — то normalize что-то поменял
git commit -m "chore: normalize line endings"
git add --renormalize . — спасительная команда. Она применяет правила из .gitattributes ко всем уже tracked файлам. Без неё .gitattributes действует только на новые/изменённые файлы, а историю не трогает.
После --renormalize все блобы файлов в Git могут измениться (если у них были CRLF). Это создаст один большой commit с “изменениями” во многих файлах. Лучше делать это до старта активной разработки в команде, чтобы не ломать чужие PR-ы.
Per-ОС шпаргалка
Для контекста, что вообще должна делать твоя локальная core.autocrlf:
Команды для применения:
# macOS / Linux
git config --global core.autocrlf input
git config --global core.eol lf
# Windows (legacy)
git config --global core.autocrlf true
# Windows (modern — LF everywhere, .gitattributes rules)
git config --global core.autocrlf false
git config --global core.eol lf
С modern setup + .gitattributes в репо core.autocrlf практически не имеет значения для tracked файлов — атрибуты переопределяют его.
Debugging: какие line endings в файле
Команды для проверки:
# 1. file — покажет, какой формат у файла
$ file script.sh
script.sh: Bourne-Again shell script, ASCII text executable, with CRLF line terminators
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
проблема
# 2. cat -A — покажет управляющие символы
$ cat -A script.sh
#!/bin/bash^M$ ← ^M это \r (CR), $ это конец строки. Значит CRLF.
# 3. hexdump для уверенности
$ head -1 script.sh | hexdump -C
00000000 23 21 2f 62 69 6e 2f 62 61 73 68 0d 0a |#!/bin/bash..|
^^ ^^
0d 0a = CR LF = CRLF
# Чистый LF был бы:
00000000 23 21 2f 62 69 6e 2f 62 61 73 68 0a |#!/bin/bash.|
^^
0a = LF only
Конвертация существующего файла:
# CRLF -> LF
$ dos2unix script.sh
$ sed -i 's/\r$//' script.sh # альтернатива через sed
# LF -> CRLF (редко нужно)
$ unix2dos script.bat
Если у тебя в репо запах CRLF (открыл shell-script в Notepad — он сохранил с CRLF), просто запусти dos2unix и закоммить.
VS Code и другие редакторы
Большинство современных редакторов умеют сохранять в нужном формате. Для VS Code — в правом нижнем углу есть индикатор CRLF/LF, кликаешь — выбираешь нужное. Можно настроить дефолт через settings.json:
{
"files.eol": "\n"
}
Это сделает LF дефолтом для всех новых файлов. Для существующих — нужно открыть файл, кликнуть индикатор и сменить.
.editorconfig — кросс-редакторный стандарт, который Git не учитывает напрямую, но многие редакторы используют:
# .editorconfig
root = true
[*]
end_of_line = lf
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.bat]
end_of_line = crlf
Положи .editorconfig в корень репо вместе с .gitattributes — оба работают на одну цель.
Полный canonical setup 2026: copy-paste
Вот два файла, которые покрывают 99% случаев. Скопируй в каждый новый проект:
.gitattributes (в корне репо):
# Universal fallback — все text-файлы хранятся как LF
* text=auto eol=lf
# Шелл-скрипты — всегда LF (иначе не запустятся на Linux/macOS)
*.sh text eol=lf
*.bash text eol=lf
# Windows-скрипты — должны быть CRLF
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# Бинарники — не трогать
*.png binary
*.jpg binary
*.pdf binary
*.zip binary
*.tar.gz binary
*.parquet binary
*.duckdb binary
*.sqlite binary
*.db binary
*.so binary
*.dll binary
*.exe binary
*.jar binary
# Diff drivers
*.py diff=python
.editorconfig (опционально, но рекомендуется):
root = true
[*]
end_of_line = lf
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true
[*.{bat,cmd,ps1}]
end_of_line = crlf
[*.{py,sh}]
indent_style = space
indent_size = 4
[*.{json,yaml,yml,js,ts}]
indent_style = space
indent_size = 2
[Makefile]
indent_style = tab
Эти два файла commit-ишь в репо и забываешь. Cross-platform работает как часы.
Попробуй сам
# Создай тестовый репо
mkdir lineending-demo && cd lineending-demo
git init
# Создай файл с CRLF (имитируем Windows)
printf "line1\r\nline2\r\nline3\r\n" > script.sh
file script.sh
# script.sh: ASCII text, with CRLF line terminators
# Без .gitattributes — Git сохранит как есть
git add script.sh
git commit -m "add script with CRLF"
# Посмотри, что в Git
git cat-file -p HEAD:script.sh | hexdump -C
# 0d 0a — есть, CRLF сохранилось
# Создай .gitattributes
cat > .gitattributes <<'EOF'
*.sh text eol=lf
EOF
git add .gitattributes
git commit -m "add .gitattributes"
# Перенормализуй
git add --renormalize .
git status
# modified: script.sh
git commit -m "normalize line endings"
# Проверь, что теперь в Git
git cat-file -p HEAD:script.sh | hexdump -C
# 0a без 0d — чистый LF в storage
# Working tree тоже должен быть LF (eol=lf и checkout даёт LF)
file script.sh
# script.sh: ASCII text ← без "CRLF" в выводе, значит LF
Это полный цикл: было CRLF -> добавили правило -> renormalize -> стало LF и в Git, и на диске.
TL;DR
- CRLF (Windows) vs LF (Unix) — главный footgun cross-platform Git.
core.autocrlf— старое решение, ненадёжно, per-машина..gitattributes+* text=auto eol=lf— каноничное решение 2026 года..bat/.cmd/.ps1— единственные исключения, оставляй CRLF.git add --renormalize .— применить новые правила к существующим файлам..editorconfig— дополнение, чтобы редакторы создавали правильные файлы изначально.
В любом серьёзном репо в 2026 году в корне должны лежать оба файла: .gitattributes (для Git) и .editorconfig (для редакторов). Это инфраструктурная гигиена.
Обработка текста: переносы строк, кодировки, форматы