Представь: дизайнер кладёт в репозиторий макет layout.psd на 180 МБ. Через неделю он его поправил, через две - ещё раз. Три версии файла, три коммита. Сколько весит твой .git? Не 180 МБ. И не 360. А все 540 с лишним.
Вот корень проблемы, из-за которой большие файлы git так бесят. Git хранит каждую версию каждого файла как отдельный объект в истории, и эта история не выбрасывается. Текст Git дельтит и сжимает отлично - в исходниках от версии к версии меняется пара строк, дельта крошечная. А бинарник (psd, mp4, sqlite-дамп, скомпилированный артефакт) при пересохранении меняется целиком, дельта между версиями практически нулевая по эффективности. Каждая правка - это новый полновесный blob в .git/objects. И он там навсегда: пока на коммит, который его содержит, есть путь от любой ветки или тега, объект достижим и не удаляется сборкой мусора.
Последствия копятся тихо, а бьют резко. git clone тянет всю историю - значит, новый разработчик качает не текущие 180 МБ, а исторические гигабайты дохлых версий. checkout между ветками ворочает огромные деревья. Форджи (GitHub, GitLab) ставят жёсткий лимит на размер пуша и на размер отдельного файла - и однажды push просто отлетает с ошибкой, а файл уже вшит в десяток коммитов. Вырезать его потом - отдельное приключение (см. урок про переписывание истории).
Главное, что надо усвоить про хранение бинарников git: проблема не в размере рабочей копии, а в том, что история несжимаема и неудаляема по дизайну. Git LFS - это аккуратный обход этого дизайна.

Git LFS: указатель в репозитории, туша на сервере
Git Large File Storage (LFS) - расширение Git от GitHub. Идея до неприличия простая: в саму историю Git кладётся не файл, а крошечный текстовый указатель (pointer, stub), а настоящее содержимое уезжает на отдельный LFS-сервер. История остаётся лёгкой - в ней только указатели, которые дельтятся и жмутся как обычный текст.
Указатель выглядит так:
Код: Выделить всё
version https://git-lfs.github.com/spec/v1
oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
size 188743680
Магия происходит через чистящий и размазывающий фильтры (clean и smudge), которые ты уже встречал в уроке про атрибуты. Логика зеркальная:
- clean срабатывает, когда файл идёт из рабочей копии в индекс (git add). Фильтр забирает большой файл, считает его sha256, отправляет содержимое в локальный LFS-кеш, а в индекс отдаёт тот самый трёхстрочный указатель. В историю попадает stub.
- smudge срабатывает в обратную сторону - при checkout, когда указатель из индекса разворачивается в рабочую копию. Фильтр видит указатель, по oid находит реальные байты (в кеше или тянет с сервера) и кладёт в рабочую директорию настоящий файл.
Практика: install, track, commit, push
Ставим расширение (бинарь git-lfs - отдельный пакет, не входит в Git) и регистрируем фильтры в конфиге:
Код: Выделить всё
$ git lfs install
Updated Git hooks.
Git LFS initialized.
Код: Выделить всё
$ git lfs track "*.psd"
Tracking "*.psd"
Код: Выделить всё
$ cat .gitattributes
*.psd filter=lfs diff=lfs merge=lfs -text
Дальше всё буднично:
Код: Выделить всё
$ git add design/layout.psd .gitattributes
$ git commit -m "add layout via lfs"
$ git push origin main
Uploading LFS objects: 100% (1/1), 188 MB | 12 MB/s, done.
Enumerating objects: 5, done.
...
Код: Выделить всё
$ git lfs ls-files
4d7a214614 * design/layout.psd
Миграция: затащить уже вшитые большие файлы в LFS
Самый частый реальный сценарий: ты подключаешь git lfs track, когда десяток psd уже вшит в историю. track действует только вперёд, на новые коммиты - старые версии так и останутся тяжёлыми blob'ами в .git. Лечит это git lfs migrate - инструмент, который переписывает историю, выдёргивая большие объекты из прошлых коммитов и подменяя их указателями.
Посмотрим сначала, кто вообще раздувает репозиторий:
Код: Выделить всё
$ git lfs migrate info --include="*.psd,*.mp4,*.zip"
*.psd 1.4 GB 23/23 files(s) 100%
*.mp4 612 MB 8/8 files(s) 100%
*.zip 210 MB 5/5 files(s) 100%
Код: Выделить всё
$ git lfs migrate import --include="*.psd,*.mp4" --everything
migrate: Sorting commits: ..., done.
migrate: Rewriting commits: 100% (47/47), done.
main f3a1c0d -> 9b2e4f1
migrate: Updating refs: ..., done.
Лимиты, форджи и альтернативы в 2026
LFS не бесплатен в прямом смысле. На GitHub бесплатная квота - 1 ГБ хранилища и 1 ГБ трафика в месяц на аккаунт, дальше платные паки data packs. GitLab считает LFS в общую квоту проекта. Зеркало или CI, дёргающие checkout по сто раз в день, легко выжигают месячный трафик за пару дней - и push/pull больших файлов начинает упираться в 403 по квоте. Это надо закладывать заранее, особенно для публичных репозиториев с активными форками.
Именно из-за этих трений в 2026 индустрия двигается прочь от LFS к нативной поддержке больших объектов внутри самого Git. Ключевое слово - large object promisors (промисор-ремоуты для крупных объектов): механизм, который даёт серверные плюсы LFS (большие файлы хранятся отдельно, клон их не тянет заранее), но без отдельного протокола, без stub-указателей в дереве и без обязательной установки git-lfs у каждого участника. Базируется это на давно стабильных кирпичах: partial clone (git clone --filter=blob:none, стабилен с 2.27, это не новинка) позволяет не качать blob'ы заранее и подтягивать их по запросу, а promisor-ремоут может стоять за CDN или S3. Куски large object promisors замержены в Git в 2025-м, документация large-object-promisors уже в git-scm, но это всё ещё work in progress - в проде на сегодня LFS остаётся рабочей лошадкой, а LOP - то, куда всё едет. Форджи параллельно пилят свою нативную поддержку крупных объектов как замену LFS.
Старая альтернатива, которую часто поминают рядом, - git-annex (именно так пишется, через дефис, никаких annannex). Это самостоятельный инструмент на Haskell: он тоже держит в репозитории не файлы, а симлинки-указатели на контент в отдельном хранилище, но устроен богаче LFS - умеет десятки бэкендов (rsync, S3, WebDAV, внешние диски), отслеживает, на скольких репликах лежит каждый файл, и заточен под сценарии "большой архив данных, синхронизируемый между машинами". Цена - заметно выше порог входа и своя ментальная модель. Для командной разработки с форджем LFS проще; git-annex силён там, где нужен распределённый дата-сет, а не центральный сервер.
Типичные грабли
- Клон без установленного LFS - в рабочей копии указатели вместо файлов. Самая частая жалоба. Коллега сделал git clone, но не ставил git-lfs - smudge-фильтр не зарегистрирован, и вместо psd он видит те самые три строки version/oid/size. Файл "побился", "не открывается". Лечение: git lfs install, затем git lfs pull (или git lfs checkout), чтобы развернуть указатели в реальные файлы. Для скриптов и CI, где LFS-контент не нужен, наоборот ставят переменную GIT_LFS_SKIP_SMUDGE=1, чтобы clone не тянул туши.
- Забыл закоммитить .gitattributes. У тебя локально track работает, файлы идут в LFS, а у коллеги без этого файла те же psd улетают в историю сырыми. .gitattributes - источник правды, он должен быть в репозитории и в том же коммите.
- track задним числом не чинит историю. Поставил track сегодня - тяжёлые объекты из вчерашних коммитов никуда не делись. Нужен git lfs migrate import.
- Force-push после миграции. migrate переписал хеши. Без согласования с командой ты разломаешь всем ветки. Только --force-with-lease и предупреждение.
- Утечка большого файла мимо LFS до настройки. Если секрет или жирный артефакт уже в истории - track его не уберёт, нужна полноценная чистка истории (filter-repo), а LFS только для будущего.
- Создай репозиторий: git init lfs-lab && перейди в него. Выполни git lfs install.
- Включи отслеживание: git lfs track "*.bin". Проверь, что появился .gitattributes с filter=lfs.
- Сделай "большой" файл: head -c 5000000 /dev/urandom > blob.bin (5 МБ случайных байт - текстом не сожмётся).
- git add blob.bin .gitattributes && git commit -m "binary via lfs".
- Посмотри git lfs ls-files - увидишь oid и путь со звёздочкой. Загляни в индекс: git show HEAD:blob.bin покажет указатель из трёх строк, а не мусор - доказательство, что в историю ушёл stub.
- Сравни git cat-file -s на blob - объект внутри Git весит сотни байт, а не 5 МБ.
- Для эффекта: GIT_LFS_SKIP_SMUDGE=1 git clone ./.git clone-test и посмотри, что в клоне blob.bin - это текстовый указатель. Потом git lfs pull в клоне развернёт настоящий файл.
- Почему три версии бинарника на 180 МБ раздувают .git примерно до 540 МБ, а три версии исходника на 180 КБ - почти нет?
- Что именно делает git lfs track "*.psd" - настраивает фильтры или пишет в файл? В какой?
- Коллега клонировал репо и видит вместо картинки три строки version/oid/size. Что произошло и какие две команды это чинят?
- Ты подключил LFS, когда psd уже год лежат в истории. Почему одного track мало и что запускать?
Git хранит каждую версию каждого файла навсегда, поэтому бинарники раздувают историю необратимо. Git LFS подменяет туши лёгкими указателями: содержимое уезжает на LFS-сервер, в .git остаётся текстовый stub, а clean/smudge-фильтры прозрачно разворачивают файлы туда-обратно. git lfs install включает фильтры, git lfs track пишет шаблон в .gitattributes (коммить его!), git lfs migrate import затаскивает в LFS уже вшитую историю и переписывает хеши. Помни про квоты форджей, про grаблю "клон без LFS = указатели", а на 2026 держи в голове, что индустрия движется к нативным large object promisors, делающим отдельный LFS постепенно ненужным.