Disclaimer: Эта история произошла несколько лет назад. Но кажется, что она и до сих пор не утратила актуальности.
… Мы разрабатывали Gardenscapes. В нём всё ещё оставались следы старого Gardenscapes под Windows. Он даже был не Match-3, а Hidden Object. И никто даже и представить не мог высот, которых достигнет игра.
И вот в один прекрасный день…
При обращении к репозиторию мы увидели следующее сообщение:
«This repository has been disabled. Access to this repository has been disabled by GitHub staff due to excessive use of resources, in violation of our Terms of Service. Please contact support to restore access to this repository. Read here to learn more about decreasing the size of your repository.»
Как вы уже поняли, мы используем github в качестве хостинга репозиториев git. И вот, внезапно и без объявления войны, github заблокировал наш репозиторий за превышение максимально допустимого размера. Точная цифра у них на сайте не была приведена. На момент блокировки размер папки .git был равен примерно 25 Гб. (Примечание 2020: сейчас лимиты стали побольше, и на сайте github явно указано, что размер репозитория не должен превышать 100 Гб).
Как же мы сумели сделать такой большой репозиторий? Причина понятна: мы храним в нём бинарные файлы. Везде написано, что так делать не рекомендуется, но нам так гораздо проще. Мы хотим, чтобы игра запускалась из репозитория сразу, без дополнительных усилий. Поэтому мы коммитим в репозиторий графику и другие игровые ресурсы.
Но это ещё полбеды. Важный урок, который мы извлекли из всей этой истории: никогданикому не рассказывай о Бойцовском клубе нельзя коммитить в репозиторий бинарные часто меняющиеся файлы. А мы так делали: коммитили исполняемый файл и текстурные атласы. Теперь мы очень сильно поумнели, и у нас есть Teamcity, который может скомпилировать бинарник и собрать атласы, плюс специальные скрипты, которые скачивают всё это добро пользователю. Но это совсем другая история. А для совсем больших файлов мы используем Git LFS, Google Drive и другие блага цивилизации.
Итак, ни у кого ничего не работает. Мы сказали команде, что день придётся поработать локально, но не очень стараться, а то потом конфликты разгребать (все очень расстроились и сразу ушли пить чай). И стали думать, что делать. Понятно, что нужен новый репозиторий, но что туда закоммитить? Простой способ — текущее состояние всех веток. Но нам так не понравилось, потому что будет утрачена история изменений, всеми любимая команда git blame сломается, и всё пойдёт кувырком. Поэтому мы решили сделать так: стереть историю бинарных файлов, а историю текстовых файлов сохранить.
У нас была полная локальная копия репозитория. Первым делом мы нашли отличную утилиту BFG Repo-Cleaner. Она очень простая и одновременно очень быстрая, и название хорошее.
Примерный сценарий выполнения:
В параметрах — все расширения бинарных файлов, которые мы смогли придумать. Из всех на свете коммитов удалятся сведения о файлах с этими расширениями. Утилита умная и при удалении истории файла оставляет его самую последнюю версию. Вдобавок эта последняя версия будет включена в самый последний коммит ветки. Мы хотели ещё удалить историю exe- и dll-файлов, но утилита выдавала ошибку. Видимо, по каким-то причинам обработка в виде *.exe запрещена. При этом если явно указать файл, например, gardenscapes.exe, то всё работает. (Примечание 2020: возможно, баг уже исправлен).
После выполнения первого шага размер репозитория всё ещё большой. Причина этому — особенности работы git. Мы удалили только ссылки на файлы, а сами файлы остались.
Чтобы файлы удалились физически, нужно выполнить команду git gc, а именно:
и потом:
Именно такая последовательность команд рекомендована автором утилиты. Вот gc действительно долго работает. Кроме того, при настройках репозитория по умолчанию клиенту git не хватает памяти, чтобы завершить операцию, и нужны некоторые пляски с бубном. (Примечание 2020: тогда у нас была 32-битная версия git. Скорее всего, в 64-битной версии этих проблем уже нет).
Это оказалось самой интересной частью квеста.
Чтобы понимать дальнейшее, нужно представлять, как работает git. Подробнее почитать про git можно много где, включая наш блог:
Итак, у нас локально есть очень-очень много коммитов, эти коммиты правильные, то есть без истории бинарных файлов. Казалось бы, достаточно выполнить git push и всё отработает само. Но нет!
Если выполнить просто команду git push -u master, то git бодро начинает процесс загрузки данных на сервер, но вылетает с ошибкой примерно на отметке в 2 Гб. Значит, так много коммитов за 1 раз залить не получится. Будем кушать слона по частям. Мы прикинули, что 2 000 коммитов наверняка поместятся в 2 Гб. Общий объём нашего репозитория тогда составлял примерно 20 000 коммитов, распределённых между 4 ветками: master-v101-v102-v103. (Примечание 2020: эх, юность! С тех пор всё стало куда серьёзнее. Коммитов в этом репозитории уже более 100 000, а релизных веток — несколько десятков. При этом мы всё ещё укладываемся в ограничения Github )
Первым делом считаем число коммитов в ветках при помощи команды:
К примеру, в ветке master оказалось примерно 10 000 коммитов. Теперь мы можем воспользоваться расширенным синтаксисом команды git push, а именно:
HEAD~8000:refs/origin/master — это так называемый refspec. Левая часть говорит, что нужно взять коммиты вплоть до коммита, отстоящего от HEAD на 8 000, то есть как раз примерно 2 000 коммитов. А правая часть — что нужно их запушить в удалённую ветку master. Здесь нужен именно полный путь к ветке refs/origin/master.
После этого ветки master пока ещё нет, и, например, git fetch скачать её не сможет. Что неудивительно — ведь коммита, который бы указывал на её HEAD, ещё не существует. Тем не менее, повторив команду git push HEAD~8000:refs/origin/master, мы увидели ответ, что эти коммиты на сервере уже есть, и, значит, работа всё-таки выполнена.
Далее мы подумали, что процесс понятен и оставшуюся часть работы можно поручить скрипту. Последний коммит будет очень большой, так как в него попадут все бинарные файлы. Поэтому на всякий случай последние 10 коммитов заливаем отдельно. Скрипт получился такой:
То есть мы последовательно записываем на сервер все наши ветки, по 2 000 коммитов за один push, и последние 10 коммитов отдельно.
Времени вся эта история заняла немало, и часы показывали уже ближе к 12 ночи. Так что скрипт мы оставили работать на ночь, произнесли полагающиеся молитвы Ктулху (Примечание 2020: тогда он был ещё относительно популярен) и пошли по домам.
Утром, открыв репозиторий на сайте github, мы убедились, что скрипт отработал успешно и все коммиты и ветки на месте.
В итоге: размер репозитория (папка .git) уменьшен с 25 Гб до 7.5 Гб. При этом вся важная история коммитов — всё, кроме бинарных файлов — сохранена. Гейм-дизайнеры выпили больше чая, чем обычно. Программисты получили незабываемый экспириенс. И срочно начали думать, а как бы так сделать, чтобы не надо было коммитить исполняемый файл в репозиторий, но работать при этом было бы удобно.
… Мы разрабатывали Gardenscapes. В нём всё ещё оставались следы старого Gardenscapes под Windows. Он даже был не Match-3, а Hidden Object. И никто даже и представить не мог высот, которых достигнет игра.
И вот в один прекрасный день…
Как всё начиналось
При обращении к репозиторию мы увидели следующее сообщение:
«This repository has been disabled. Access to this repository has been disabled by GitHub staff due to excessive use of resources, in violation of our Terms of Service. Please contact support to restore access to this repository. Read here to learn more about decreasing the size of your repository.»
Как вы уже поняли, мы используем github в качестве хостинга репозиториев git. И вот, внезапно и без объявления войны, github заблокировал наш репозиторий за превышение максимально допустимого размера. Точная цифра у них на сайте не была приведена. На момент блокировки размер папки .git был равен примерно 25 Гб. (Примечание 2020: сейчас лимиты стали побольше, и на сайте github явно указано, что размер репозитория не должен превышать 100 Гб).
Как же мы сумели сделать такой большой репозиторий? Причина понятна: мы храним в нём бинарные файлы. Везде написано, что так делать не рекомендуется, но нам так гораздо проще. Мы хотим, чтобы игра запускалась из репозитория сразу, без дополнительных усилий. Поэтому мы коммитим в репозиторий графику и другие игровые ресурсы.
Но это ещё полбеды. Важный урок, который мы извлекли из всей этой истории: никогда
Борьба за историю
Итак, ни у кого ничего не работает. Мы сказали команде, что день придётся поработать локально, но не очень стараться, а то потом конфликты разгребать (все очень расстроились и сразу ушли пить чай). И стали думать, что делать. Понятно, что нужен новый репозиторий, но что туда закоммитить? Простой способ — текущее состояние всех веток. Но нам так не понравилось, потому что будет утрачена история изменений, всеми любимая команда git blame сломается, и всё пойдёт кувырком. Поэтому мы решили сделать так: стереть историю бинарных файлов, а историю текстовых файлов сохранить.
Шаг 1. Удаляем историю бинарных файлов
У нас была полная локальная копия репозитория. Первым делом мы нашли отличную утилиту BFG Repo-Cleaner. Она очень простая и одновременно очень быстрая, и название хорошее.
Примерный сценарий выполнения:
java -jar bfg.jar bfg --delete-files *.{pvrtc,webp,png,jpeg,fla,swl,swf,pbi,bin,mask,ods,ogv,ogg,ttf,mp4} path_to_repository
В параметрах — все расширения бинарных файлов, которые мы смогли придумать. Из всех на свете коммитов удалятся сведения о файлах с этими расширениями. Утилита умная и при удалении истории файла оставляет его самую последнюю версию. Вдобавок эта последняя версия будет включена в самый последний коммит ветки. Мы хотели ещё удалить историю exe- и dll-файлов, но утилита выдавала ошибку. Видимо, по каким-то причинам обработка в виде *.exe запрещена. При этом если явно указать файл, например, gardenscapes.exe, то всё работает. (Примечание 2020: возможно, баг уже исправлен).
Шаг 2. Сжимаем репозиторий
После выполнения первого шага размер репозитория всё ещё большой. Причина этому — особенности работы git. Мы удалили только ссылки на файлы, а сами файлы остались.
Чтобы файлы удалились физически, нужно выполнить команду git gc, а именно:
git reflog expire --expire=now --all
и потом:
git gc --prune=now --aggressive
Именно такая последовательность команд рекомендована автором утилиты. Вот gc действительно долго работает. Кроме того, при настройках репозитория по умолчанию клиенту git не хватает памяти, чтобы завершить операцию, и нужны некоторые пляски с бубном. (Примечание 2020: тогда у нас была 32-битная версия git. Скорее всего, в 64-битной версии этих проблем уже нет).
Шаг 3. Записываем коммиты в новый репозиторий
Это оказалось самой интересной частью квеста.
Чтобы понимать дальнейшее, нужно представлять, как работает git. Подробнее почитать про git можно много где, включая наш блог:
Итак, у нас локально есть очень-очень много коммитов, эти коммиты правильные, то есть без истории бинарных файлов. Казалось бы, достаточно выполнить git push и всё отработает само. Но нет!
Если выполнить просто команду git push -u master, то git бодро начинает процесс загрузки данных на сервер, но вылетает с ошибкой примерно на отметке в 2 Гб. Значит, так много коммитов за 1 раз залить не получится. Будем кушать слона по частям. Мы прикинули, что 2 000 коммитов наверняка поместятся в 2 Гб. Общий объём нашего репозитория тогда составлял примерно 20 000 коммитов, распределённых между 4 ветками: master-v101-v102-v103. (Примечание 2020: эх, юность! С тех пор всё стало куда серьёзнее. Коммитов в этом репозитории уже более 100 000, а релизных веток — несколько десятков. При этом мы всё ещё укладываемся в ограничения Github )
Первым делом считаем число коммитов в ветках при помощи команды:
git rev-list --count <branch-name>
К примеру, в ветке master оказалось примерно 10 000 коммитов. Теперь мы можем воспользоваться расширенным синтаксисом команды git push, а именно:
git push -u origin HEAD~8000:refs/origin/master
HEAD~8000:refs/origin/master — это так называемый refspec. Левая часть говорит, что нужно взять коммиты вплоть до коммита, отстоящего от HEAD на 8 000, то есть как раз примерно 2 000 коммитов. А правая часть — что нужно их запушить в удалённую ветку master. Здесь нужен именно полный путь к ветке refs/origin/master.
После этого ветки master пока ещё нет, и, например, git fetch скачать её не сможет. Что неудивительно — ведь коммита, который бы указывал на её HEAD, ещё не существует. Тем не менее, повторив команду git push HEAD~8000:refs/origin/master, мы увидели ответ, что эти коммиты на сервере уже есть, и, значит, работа всё-таки выполнена.
Далее мы подумали, что процесс понятен и оставшуюся часть работы можно поручить скрипту. Последний коммит будет очень большой, так как в него попадут все бинарные файлы. Поэтому на всякий случай последние 10 коммитов заливаем отдельно. Скрипт получился такой:
git push origin HEAD~6000:refs/origin/master
git push origin HEAD~5000:refs/origin/master
git push origin HEAD~4000:refs/origin/master
git push origin HEAD~3000:refs/origin/master
git push origin HEAD~2000:refs/origin/master
git push origin HEAD~1000:refs/origin/master
git push origin HEAD~10:refs/origin/master
git push origin master
git checkout v101
git push -u origin HEAD~1000:refs/origin/v101
git push origin HEAD~10:refs/origin/v101
git push origin v101
git checkout v102
… и т.д.
То есть мы последовательно записываем на сервер все наши ветки, по 2 000 коммитов за один push, и последние 10 коммитов отдельно.
Времени вся эта история заняла немало, и часы показывали уже ближе к 12 ночи. Так что скрипт мы оставили работать на ночь, произнесли полагающиеся молитвы Ктулху (Примечание 2020: тогда он был ещё относительно популярен) и пошли по домам.
Финал. Хэппи-энд
Утром, открыв репозиторий на сайте github, мы убедились, что скрипт отработал успешно и все коммиты и ветки на месте.
В итоге: размер репозитория (папка .git) уменьшен с 25 Гб до 7.5 Гб. При этом вся важная история коммитов — всё, кроме бинарных файлов — сохранена. Гейм-дизайнеры выпили больше чая, чем обычно. Программисты получили незабываемый экспириенс. И срочно начали думать, а как бы так сделать, чтобы не надо было коммитить исполняемый файл в репозиторий, но работать при этом было бы удобно.