Репозиторий, в который годами клали макеты интерфейсов, видеоинструкции и тестовые дампы, рано или поздно превращается в тяжёлый чемодан без ручки: любой git clone тянет сотни мегабайт истории, а CI собирается по пять минут только на этапе скачивания репозитория. Git хранит каждую версию каждого файла полностью, и для бинарников дедупликация через дельты не работает, поэтому десять правок файла на 80 мегабайт оставляют в истории полные 800 мегабайт. Git Large File Storage решает эту задачу, подменяя бинарные объекты в истории на текстовые указатели по 130 байт, а сами бинарники складывая в отдельное хранилище LFS на сервере и локальный кеш в каталоге .git/lfs. Далее разобрана полная настройка на существующем проекте, включая миграцию уже закоммиченных файлов и типичные грабли с квотами, о которых обычно узнают, когда интерфейс хостинга внезапно отклоняет push.

Установка клиента LFS и порядок его регистрации в репозитории

Клиент ставится пакетом ОС или с релизной страницы, после чего выполняется одна команда как объявление намерений:

git lfs install

Вывод лаконичен:

Updated git hooks.
Git LFS initialized.

Первая строка означает, что клиент записал фильтры clean, smudge и pre-push hook в глобальную конфигурацию. Вторая фиксирует готовность. Если открыть ~/.gitconfig, там появится секция:

[filter "lfs"]
    clean = git-lfs clean -- %f
    smudge = git-lfs smudge -- %f
    process = git-lfs filter-process
    required = true

Флаг required = true важен: без рабочего фильтра Git откажется обрабатывать такие файлы, а не молча запишет их напрямую. Обратите внимание, что опция -- в конце команд отделяет имя файла %f от опций клиента; это защита от файлов, чьи имена начинаются с дефиса.

Следующий шаг - выбрать шаблоны бинарников и зафиксировать их через track:

git lfs track "*.psd"
git lfs track "*.mp4" "*.zip" "*.onnx"

Команда ничего не меняет в самих файлах; она пишет правила в .gitattributes, который нужно закоммитить вместе с кодом:

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

Атрибут -text отключает нормализацию переносов строк. Ошибка частых новичков: сначала edit пишут правила чуть иначе, например забывая -text, и LFS иногда пытается обработать бинарник как текст. Проверить, какой файл реально попал под LFS, помогает:

git check-attr --all assets/logo.psd

Проверка подмены бинарников указателями и выборочная подтяжка объектов

После коммита файл в рабочей копии выглядит как обычный psd, но в истории лежит указатель размером примерно 130 байт. Убедится в этом можно, посмотрев blob напрямую:

git cat-file -p HEAD:assets/logo.psd

Вывод будет текстовым:

version https://git-lfs.github.com/spec/v1
oid sha256:4d7a2149ab8545a06b22a55067c4a4b7c1b18c4ccb0798b41be685f4c9c61b33
size 72683520

Первой идёт строка версии спецификации указателя. Вторая - oid, это SHA-256 содержимого бинарника, он же имя файла в хранилище LFS. Третья - реальный размер в байтах, здесь около 69 мегабайт. Именно поэтому клиент LFS при клоне скачивает реальные бинарники только для текущего чекаута, а не для всей истории.

Иногда бинарники нужны не все. Полный размер репозитория не обязан лежать на диске разработчика, который правит только CSS. Для этого служит выборочная подтяжка:

git lfs pull --include="assets/img/**" --exclude="assets/video/**"

Команда скачивает объекты для уже существующих указателей в рабочей копии, но только в пределах шаблонов. Похожее поведение у git lfs fetch, но fetch оставляет объекты в локальном кеше .git/lfs/objects, не обновляя файлы в чекауте; за это отвечает git lfs checkout. Парный тандем fetch и checkout удобен в скриптах CI, где сначала проверяют наличие объекта, а уже потом разворачивают его в рабочее дерево.

Если на новой машине вместо картинок появились файлы в 130 байт, значит, клиент LFS не установлен или smudge-фильтр не сработал. Лечение стандартное: git lfs install, затем git lfs pull. Разработчикам, которые клонируют репозиторий впервые, удобно честно написать это в README, иначе в канал поддержки прилетят скриншоты "битых" картинок.

Миграция существующей истории через git lfs migrate

Самая болезненная часть внедрения - файлы, которые уже много лет лежат в истории напрямую. Перед любой переписью истории полезно снять бэкап-клон и провести аудит:

git lfs migrate info --include="*.psd,*.mp4" --everything

Команда показывает, какие ветки и теги содержат подходящие бинарники и сколько места они занимают. Опция --everything обходит все ссылки, не только текущую ветку. Затем выполняется сама перезапись:

git lfs migrate import --include="*.psd" --include="*.mp4" --everything

Инструмент находит каждый blob по шаблонам, заменяет его в дереве на указатель, добавляет правила в .gitattributes (если правил там не было) и создаёт новые коммиты. SHA всех затронутых коммитов и всего, что идёт после них, меняется: это полноценная перепись истории. Поэтому последовательность в организации такая: уведомить команду, заблокировать пуши на ночь, выполнить миграцию локально, затем залить силой:

git push --force --all

Поскольку migrate трогает и теги, их нужно отправить отдельной командой git push --force --tags. После этого каждый участник должен пересоздать свою локальную копию: git fetch --all, затем git reset --hard origin/main, а тем, у кого есть незакоммиченное, - сначала сохранить патч в сторону. Да, это самая опасная операция во всей истории с LFS, и шанс нарваться на конфликт версий вычеркнуть невозможно: старые ветки с новыми указателями смешать нельзя, они расходятся историей.

Отдельный нюанс - старые теги выпусков. Если они должны остаться некорректно ссылающимися на старые коммиты, мигрируйте только main и активные ветки без --everything, но тогда старые релизы, клонированные по тегу, не содержат LFS-правил. В продакшен-командах компромисс выбирают явно.

Блокировка файлов и совместная работа с теми кто редактирует бинарники

Текстовый код git сливает через merge, а два изменённых копиями psd-файла или таблицы превратить в одну невозможно: merge tools для них просто не существует. LFS решает это через механизм блокировок, и про него стоит рассказать дизайнерам и аналитикам в первый же день. Команда git lfs lock assets/logo.psd ставит серверную блокировку на файл, и любой коллега, попытавшийся запушить свою версию, получит отказ с указанием владельца блокировки. После завершения работы блокировку снимают: git lfs unlock assets/logo.psd, а посмотреть текущее состояние помогает git lfs locks, который выводит список путей и имена держателей.

Чтобы блокировка не оставалась формальностью, файлам добавляют атрибут lockable в .gitattributes:

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

Атрибут lockable заставляет клиента выставлять на такие файлы права только на чтение, пока блокировки нет у текущего пользователя. Открыть файл в редакторе можно, а сохранить - уже нет, и это честный способ напомнить человеку про lock до того, как он потратит два часа на правку чужой версии. Для мультимедийных проектов и игровых репозиториев, где сотни бинарных ассетов редактируются параллельно, эта связка превращает хаос перезаписей в управляемую очередь.

Отдельная деталь инфраструктуры: единый LFS-сервер против разных. При форке репозитория на хостинге объекты LFS по умолчанию живут в общем пуле оригинала, и квота форка частично едет за счёт родителя. При самостоятельном размещении через любую реализацию LFS API адрес сервера задаётся в .lfsconfig в корне репозитория, и этот файл так же версионируется, так что перенос на своё железо не требует массовых писем коллегам: один коммит, и у всех клиентов новый endpoint. Проверить, куда ходит клиент на текущем клоне, позволяет git lfs env - он печатает Endpoint, локальный путь хранилища и настройки конкурентных загрузок, и его вывод экономит немало времени при отладке 403-ответов.

Квоты хранилища на хостингах и профилактика переполнения

LFS-хранилище у хостингов лимитировано и оплачивается отдельно от git-локащего места. Классическая схема: базовый мегабайт и гигабайт бесплатно, далее помегабайтная оплата пакетом. Когда квота заканчивается, пуш отклоняется примерно такой ошибкой:

batch response: This repository is over its data quota.
Purchase more data packs to restore access.

Ключевая строка - batch response: клиент LFS общается с сервером LFS-пакетами, и отказ приходит не из Git, а из LFS-эндпоинта вашего сервера. Разбор по пунктам перед покупкой новых гигов обязан начинаться с инвентаризации, что реально занимает место. Команды для локальной оценки:

  1. git lfs ls-files --size показывает список объектов LFS в текущей ветке с размерами; полезно для поиска локальных переростков;
  2. git count-objects -vH показывает общий размер .git, но не учитывает стороннее хранилище LFS на сервере;
  3. вывод панели хостинга по storage и bandwidth даёт фактический расход квоты, трафика и стоимости;
  4. git lfs prune --dry-run показывает, какие локальные объекты стали не нужны и могут быть удалены из кеша.

Профилактика переполнения здесь строится вокруг трёх принципов: не класть сырые видео и архивы в репозиторий вообще (для этого существуют артефакт-хранилища и пассажирские кэши), жать медиа до коммита, и держать авторитетный список шаблонов в .gitattributes, чтобы новый дизайнер не положил свой psd мимо правил. Трафик тоже стоит денег: каждый lfs pull объёмного файла расходует bandwidth-квоту, поэтому в CI бинарники лучше кэшировать.

Отдельно замечены обрывы больших загрузок по HTTP. Полезные настройки клиента:

git config lfs.concurrenttransfers 4
git config lfs.transfer.maxretries 5
git config http.postBuffer 524288000

Первый параметр снижает число параллельных соединений, что помогает в слабых каналах. Второй поднимает ретраи. Третий увеличивает буфер при пуше. Если сервер поддерживает basic-auth-режим в LFS-эндпоинте, gita uthentication подкладывают через .lfsconfig в корне репозитория или через креденшел-хелпер.

Типичные ошибки и отладочный workflow на каждый день

Реальный дежурный workflow на команду выглядит так: каждый день git lfs status перед коммитом, раз в спринт git lfs fsck для проверки целостности, раз в месяц ревизия .gitattributes и квот. git lfs fsck проверяет, что содержимое объектов в локальном кеше совпадает с их SHA-256, и помогает поймать битый файл до того, как его испортят синхронизацией с облачным диском.

Из частых ошибок: коммит бинарника до добавления правила в .gitattributes, попытка отменить LFS для файла, который уже лежит в истории (достаточно убрать правило - новые копии пойдут напрямую, история останется), smudge-фильтр отключён локальными настройками filter.lfs.process и клон приносит указатели вместо бинарников, непонимание того, что lfs pull не вернёт пропавший файл, если его нет на сервере: oid берётся из указателя, и отсутствие объекта означает окончательную потерю этого варианта файла.

Полезная диагностическая цепочка, если что-то пошло не так:

git lfs env

Она выводит эндпоинт сервера, версии, путь к кешу и поведение фильтров. Часто проблема видна уже здесь: например, репозиторий по какой-то причине указывает на не тот LFS-эндпоинт. Далее смотрят git lfs ls-files --size и .git/lfs/objects, чтобы понять, какой oid реально нужен и есть ли он локально. Если пропадает только в CI - проверяют, не чистит ли сборка кеш .git/lfs между джобами.

Внедрение LFS окупается, когда команда принимает правила игры: .gitattributes укреплён, миграция проведена один раз и аккуратно, CI не тянет всё подряд, а квота хранилища отслеживается как расходный ресурс. После этого клон занимает секунды, а бинарники перестают быть источником репозиторийного жира.