Git LFS и большие файлы

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

Git LFS и большие файлы

Сообщение 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 и как из них выбираться
Почему один большой файл превращается в вечную боль

Представь: дизайнер кладёт в репозиторий макет 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
Три строки. oid - это SHA-256 от содержимого файла (LFS адресует контент своим хешем, независимо от хеша самого Git-объекта). size - размер в байтах. Вот этот текст и лежит в .git/objects вместо 180 мегабайт. На push LFS отдельным протоколом заливает настоящие байты на LFS-хранилище, на checkout - подтягивает их обратно по oid.

Магия происходит через чистящий и размазывающий фильтры (clean и smudge), которые ты уже встречал в уроке про атрибуты. Логика зеркальная:
  • clean срабатывает, когда файл идёт из рабочей копии в индекс (git add). Фильтр забирает большой файл, считает его sha256, отправляет содержимое в локальный LFS-кеш, а в индекс отдаёт тот самый трёхстрочный указатель. В историю попадает stub.
  • smudge срабатывает в обратную сторону - при checkout, когда указатель из индекса разворачивается в рабочую копию. Фильтр видит указатель, по oid находит реальные байты (в кеше или тянет с сервера) и кладёт в рабочую директорию настоящий файл.
Поэтому в рабочей копии ты видишь обычный psd и работаешь с ним как всегда, а в .git живёт лёгкий указатель. Контент материализуется только при необходимости.

Практика: install, track, commit, push

Ставим расширение (бинарь git-lfs - отдельный пакет, не входит в Git) и регистрируем фильтры в конфиге:

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

$ git lfs install
Updated Git hooks.
Git LFS initialized.
Эта команда не трогает репозиторий с файлами - она прописывает в твой ~/.gitconfig секцию filter "lfs" (тот самый clean/smudge/process) и ставит хуки. Делается один раз на машину. Внутри конкретного репозитория проверь, какие шаблоны отслеживаются:

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

$ git lfs track "*.psd"
Tracking "*.psd"
Что реально произошло: git lfs track не настраивает магию, а просто дописывает строку в .gitattributes. Открой файл:

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

$ cat .gitattributes
*.psd filter=lfs diff=lfs merge=lfs -text
Разберём по словам. filter=lfs - применять lfs-фильтр (clean/smudge). diff=lfs - не пытаться показывать текстовый дифф бинарника. merge=lfs - не лезть в трёхстороннее слияние байтов. -text - не делать конверсию переводов строк. .gitattributes обязательно коммитится - именно он, а не твой локальный конфиг, говорит всем участникам, что эти файлы идут через LFS. Забыл закоммитить - у коллеги psd уедет в историю целиком.

Дальше всё буднично:

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

$ 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.
...
Видишь отдельную строку Uploading LFS objects - это LFS своим протоколом залил тушу, и только потом пошёл обычный git push с лёгкими объектами. Проверим, что именно ушло в LFS:

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

$ git lfs ls-files
4d7a214614 * design/layout.psd
Звёздочка между oid и путём значит, что файл реально присутствует локально (материализован). Минус на этом месте означал бы, что в рабочей копии пока только указатель. Команда git lfs track "*.psd" без аргументов покажет все активные шаблоны - удобно для аудита чужого .gitattributes.

Миграция: затащить уже вшитые большие файлы в 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%
Режим info ничего не меняет - только считает, сколько байт по каким типам сидит в истории. Теперь сама миграция:

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

$ 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.
Что делает import: проходит по коммитам, заменяет реальные байты подходящих файлов на указатели, сам дописывает .gitattributes в нужных точках истории - как будто ты запускал git lfs track ещё тогда, когда файл впервые появился. --everything обрабатывает все ветки и теги, иначе мигрирует только текущую. Хеши коммитов сменились (f3a1c0d -> 9b2e4f1) - история переписана, это force-push и согласование со всей командой. Если паттерны лень перечислять, а .gitattributes уже лежит правильный, есть git lfs migrate import --fixup - он подтянет под LFS то, что по атрибутам должно идти через LFS, но в истории сидит сырыми байтами. После import обязательно git push --force-with-lease и предупреждение коллегам перезабрать ветки.

Лимиты, форджи и альтернативы в 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 постепенно ненужным.
👍4 ❤️1 🔥1 😄 🤔2
Аватара пользователя
denopro
Сообщения: 1
Зарегистрирован: 13 май 2026, 06:18

Re: Git LFS и большие файлы

Сообщение denopro »

А я как раз неделю мучился: склонировал чужой репо с моделями для блендера, а там вместо .blend какие то три строки с oid. Думал репозиторий битый, чуть автору багрепорт не написал. Оказалось просто git lfs install забыл и pull. Спасибо, теперь ясно почему.
👍2 ❤️1 🔥 😄 🤔1
Аватара пользователя
summer11
Сообщения: 1
Зарегистрирован: 20 май 2026, 00:04

Re: Git LFS и большие файлы

Сообщение summer11 »

Вопрос про migrate import: если я --everything прогнал и хеши поменялись, а у двух коллег уже свои ветки от старых коммитов - им как восстанавливаться? Просто заново клонировать или есть способ переехать аккуратнее? Тема force-with-lease понятна, но интересны именно их ветки.
👍2 ❤️ 🔥1 😄 🤔
Ответить
← Предыдущая глава
Subtree, монорепозитории и sparse-checkout
Следующая глава →
Вспомогательные команды: clean, mv, rm, restore

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

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

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

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

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