Облегчаем 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 я не коммичу.
Получается вот так. Без причины, я лично, не стану использовать ни один из этих методов. Но при необходимости теперь буду знать, что и как можно подчистить :)