Очистка истории: удалить файл/секрет, filter-repo
Большой бинарник или утёкший пароль попал в репозиторий — учимся вычищать его не из одного коммита, а из всей истории целиком.
Удалить файл одним коммитом мало: он остаётся во всех прошлых коммитах. Полная очистка требует переписать историю инструментами вроде
git filter-repoили BFG.
Это самая мощная и самая опасная операция в книге. Она нужна в двух ситуациях: репозиторий распух из-за давно закоммиченного большого файла, либо в историю утёк секрет (пароль, токен, приватный ключ). Обычное git rm тут бессильно.
Почему обычное удаление не помогает
Удалив файл и закоммитив, вы убираете его лишь из будущего. Git хранит снимок состояния в каждом коммите, поэтому файл целиком лежит во всех прежних коммитах — его легко достать из истории:
git rm secret.env
git commit -m "Удалить секрет"
git show HEAD~5:secret.env # секрет по-прежнему доступен из старого коммита
Для большого файла это значит, что вес репозитория не уменьшится: каждый git clone по-прежнему тащит весь старый бинарник. Для секрета — что он по-прежнему скомпрометирован и доступен любому, у кого есть история.
Сначала — экстренные меры для секрета
Если утёк живой секрет — первым делом отзовите и смените его, и только потом чистите историю.
Это критично: пока вы переписываете коммиты, секрет уже мог быть скачан. Очистка истории не отменяет компрометацию. Поэтому порядок такой: ротация ключа/пароля → инвалидация токена на стороне сервиса → и лишь затем зачистка репозитория, чтобы не светить мёртвый секрет дальше.
git filter-repo — рекомендуемый инструмент
Раньше для этого использовали встроенный git filter-branch, но он медленный и капризный; официально рекомендуют отдельную утилиту git-filter-repo (ставится через pip install git-filter-repo или менеджер пакетов). Перед запуском сделайте резервную копию или работайте на свежем клоне — операция необратима.
Удалить файл из всей истории
git filter-repo --path secret.env --invert-paths
--path задаёт цель, --invert-paths переворачивает смысл: «оставить всё, кроме указанного». Можно удалить целую папку (--path build/ --invert-paths) или по маске.
Вырезать большой файл по размеру
git filter-repo --strip-blobs-bigger-than 10M
Уберёт из истории все blob'ы тяжелее 10 МБ — удобно, когда вы не знаете точное имя распухшего файла.
Заменить текст секрета
Если нужно не удалить файл, а затереть утёкшую строку в нём по всей истории, готовим файл с правилами замены и скармливаем его:
git filter-repo --replace-text secrets.txt
# содержимое secrets.txt: ghp_REALTOKEN123==>REMOVED
BFG Repo-Cleaner — быстрая альтернатива
BFG — отдельная (Java) утилита, заточенная под две частые задачи: удалить большие файлы и почистить секреты. Она проще filter-repo для типовых случаев и заметно быстрее:
bfg --delete-files secret.env # удалить файл по имени из истории
bfg --strip-blobs-bigger-than 10M # выкинуть крупные blob'ы
bfg --replace-text passwords.txt # заменить секреты на ***REMOVED***
BFG не трогает текущий коммит (HEAD) — предполагается, что в нём вы файл уже удалили обычным способом.
Как это работает под капотом
Любой такой инструмент делает одно: проходит по всем коммитам и пересоздаёт каждый уже без нежелательного содержимого. Раз меняется содержимое — меняется и хеш коммита, а раз меняется коммит — меняются и все его потомки. Иными словами, переписывается вся история, начиная с первого затронутого коммита. Старые объекты остаются в базе как «висячие», пока их не выметет сборщик мусора (его можно поторопить: git reflog expire --expire=now --all && git gc --prune=now).
Публикация: force-push и --force-with-lease
История переписана локально — теперь её надо отправить, перезаписав старую на сервере. Обычный push будет отвергнут (истории разошлись), нужен принудительный. Но --force опасен: он затрёт удалёнку безусловно, в том числе чужие коммиты, которые вы могли не успеть забрать. Поэтому используют предохранитель:
git push --force-with-lease
--force-with-leaseотправит, только если удалённая ветка с момента вашего последнего fetch не изменилась. Кто-то успел запушить — push отклонят, и вы не сотрёте чужую работу.
Когда история общая
Переписав опубликованную историю, вы нарушаете золотое правило умышленно — другого пути вычистить секрет нет. Поэтому предупредите команду заранее. После вашего force-push всем коллегам придётся синхронизировать локальные клоны (как правило, заново клонировать или аккуратно сделать rebase поверх новой истории), иначе они вернут удалённый файл обратно. На GitHub помните: данные могли осесть в форках, кешах и Pull Request'ах — для утёкшего секрета ротация важнее любой зачистки.
Частые ошибки
- Чистят историю, но не меняют секрет. Самая опасная иллюзия безопасности: удаление из репозитория не делает утёкший токен снова секретным. Сначала ротация.
- Запускают filter-repo без бэкапа. Операция необратима локально. Делайте копию каталога или гоняйте на свежем клоне.
- Толкают результат через
--forceвместо--force-with-lease. Голый force легко затирает чужие коммиты на общей ветке. - Удалили большой файл, но репозиторий не похудел. Старые объекты ещё висят — нужен прогон сборщика мусора (
git gc --prune=now) и время на стороне хостинга. - Забывают про форки и кеши. На публичном GitHub секрет мог разойтись по форкам и кешу PR; единственная надёжная защита — отозвать сам секрет.
Итоги
git rmубирает файл только из будущего — в прошлых коммитах он остаётся целиком.- Полная очистка переписывает всю историю; рекомендуемый инструмент —
git filter-repo, быстрая альтернатива для типовых задач — BFG. - Для утёкшего секрета порядок строгий: сперва отозвать и сменить секрет, и только потом чистить историю.
- Перед операцией — резервная копия; после — отправка через
git push --force-with-lease, не--force. - Перезапись общей истории требует предупредить команду и пересинхронизировать клоны коллег.