Облегчаем Git-репы

Мой локальный Git репозиторий, в котором я храню этот сайт, стал пухнуть. Особенно явно это стало после добавления постов из Telegram с картинками. Это логично, бинарники – не сильная сторона Git. Для них есть Git LFS.

LFS требует пары манипуляций. И вот, вычитывая доку, наткнулся на замечание о том, как лучше мигрировать существующие репы. В частности, об очистке репы после git lfs migrate. Процедура простая и не сильно деструктивная. Тут я задумался.

В целом, Git прилично справляется с задачами garbage collection, это отлаженная годами задача. Но вот загвоздка – происходит GC в фоне при выполнении других команд Git. У меня же есть много накопившихся Git репозиториев, над которыми я не работаю. Хочется сэкономить место, при этом не потерять историю коммитов.

Все команды ниже являются деструктивными. Пути назад без полных бэкапов не будет. Если Вы не уверены, что такое reflog в Git и что его очистка значит, рекомендую либо не использовать подходы ниже, либо сначала сделать бэкапы!

Все команды ниже подразумевают: Git репозиторий в этот момент используется эксклюзивно только Вами. Параллельное использование Git может привести к безвозвратной порче директории .git. Будьте предельно осторожны!

Каждый заход эксперимента я буду производить в /tmp, предварительно скопировав туда интересующий Git-репозиторий. Первый подход к снаряду – простой git gc. Это предельно явный пинок для Git, императив вида “сделай GC в foreground!”

$ du -sh .git

1,2G    .git

$ time git gc

Enumerating objects: 7717, done.
Counting objects: 100% (7717/7717), done.
Delta compression using up to 32 threads
Compressing objects: 100% (1262/1262), done.
Writing objects: 100% (7717/7717), done.
Total 7717 (delta 6334), reused 7653 (delta 6301), pack-reused 0

________________________________________________________
Executed in   11.45 secs    fish           external
   usr time   10.99 secs  309.00 micros   10.99 secs
   sys time    0.56 secs  227.00 micros    0.56 secs

$ du -sh .git

1,2G    .git

Это простейший способ, он почистит лишь самые старые репозитории. Те, в которых давно не производилось никаких активных действий. И те, в которых явно много мусора. И сделает он это, к слову, не слишком эффективно. Нужно больше GC!

Более продвинутый способ – удалить ссылочную историю (reflog) для недоступных объектов и явно передать флаги для очистки неиспользуемого мусора из .git.

Недоступные объекты появляются, например, в процессе переписывания истории коммитов и именно поэтому такой подход рекомендуется в гайде у Git LFS. Команда git lfs migrate переписывает историю, заменяя определённые бинарники на ссылки. Старые объекты с бинарниками останутся в .git как недоступные.

Я LFS ещё не подключал, но всё равно стало интересно.

$ du -sh .git

1,2G    .git

$ git reflog expire --expire-unreachable=now --all

$ time git gc --prune=now

Enumerating objects: 7717, done.
Counting objects: 100% (7717/7717), done.
Delta compression using up to 32 threads
Compressing objects: 100% (1262/1262), done.
Writing objects: 100% (7717/7717), done.
Total 7717 (delta 6334), reused 7653 (delta 6301), pack-reused 0

________________________________________________________
Executed in    2.02 secs    fish           external
   usr time    1.78 secs  310.00 micros    1.78 secs
   sys time    0.34 secs  242.00 micros    0.34 secs

$ du -sh .git

507M    .git

Ого! Вот это уже приятный результат. Интересная опция --expire-unreachable=now для reflog expire явно требует пометить все недоступные объекты как протухшие. А опция --prune=now намекнёт gc выкинуть все протухшие объекты уже сейчас.

В такой конфигурации, кстати, GC отработал быстрее, чем по умолчанию.

Это уже отличный результат, который не затрагивает весь актуальный reflog и не делает агрессивное сжатие. Подходит даже для рабочих реп, если они, как и репа для моего сайта, стали расти как на дрожжах. Работает довольно шустро.

А что делать с архивными репозиториями? К тому же, у меня есть много Git реп, которые я время от времени обновляю с помощью fetch, делаю checkout нужного тега и сам собираю софт…

Там мне совсем не нужен reflog, да и куда больше интересует вопрос долгосрочной упаковки данных. Без оглядки на то, сколько времени займёт обработка сейчас.

Если заменить --expire-unreachable на --expire, то Git удалит reflog полностью. Это не очень подходит для рабочих реп, но самое оно для архивных. Использование --aggressive позволяет Git пере-паковать объекты с большей эффективностью. Метод является самым долгим по времени исполнения.

$ du -sh .git

1,2G    .git

$ git reflog expire --expire=now --all

$ time git gc --prune=now --aggressive

Enumerating objects: 7717, done.
Counting objects: 100% (7717/7717), done.
Delta compression using up to 32 threads
Compressing objects: 100% (7563/7563), done.
Writing objects: 100% (7717/7717), done.
Total 7717 (delta 6494), reused 1179 (delta 0), pack-reused 0

________________________________________________________
Executed in   22.80 secs    fish           external
   usr time   42.59 secs  375.00 micros   42.59 secs
   sys time    0.65 secs  278.00 micros    0.65 secs

$ du -sh .git

502M    .git

В данном случае эффект совсем небольшой и не стоит свеч. Терять reflog за 5M? Нет уж спасибо! Другие репы, как например linux-firmware, выигрывают уже около 300 мегов. Намного более весомо и практично, в linux-firmware я не коммичу.

Получается вот так. Без причины, я лично, не стану использовать ни один из этих методов. Но при необходимости теперь буду знать, что и как можно подчистить :)

Если Вы хотите обсудить содержание заметки, задать вопросы или предложить изменения, то со мной можно связаться в Telegram