Представь утро. Кто-то закоммитил в репозиторий файл config/secrets.yml с боевым паролем от базы. Заметили через неделю. За эту неделю было сорок коммитов, три релиза, репо склонировали двадцать человек. Ты удаляешь файл одним коммитом - и думаешь, что закрыл вопрос. Не закрыл. Файл с паролем по-прежнему живёт в истории: git log сорок коммитов назад покажет его целиком, git checkout того коммита вернёт его на диск. История в Git неизменяема по дизайну, и удаление в новом коммите ничего из прошлого не стирает.
Вторая классика. Репозиторий весит 4 гигабайта, клонируется десять минут. Лезешь в потроха и находишь, что год назад кто-то закоммитил дамп базы на 800 мегабайт, потом удалил. Но blob этого дампа намертво вшит в один из старых коммитов, и каждый клон тащит его целиком.
И третья. Юридический отдел просит сменить email автора во всех коммитах: человек коммитил с личной почты, а надо с корпоративной. Или ты хочешь выдрать подкаталог frontend/ в отдельный репозиторий, сохранив только его историю.
Все четыре задачи - это массовое переписывание истории. Не один коммит, а пересборка всей цепочки от корня. Дальше - как это делать правильно инструментом git filter-repo, чем плох старый filter-branch, что такое git replace как безопасная альтернатива, и почему без ротации секрета вся затея бесполезна.

Почему git filter-repo, а не filter-branch
Исторически штатным инструментом был git filter-branch. На 2026 он официально не рекомендован. Это не вкусовщина - это позиция самого проекта Git. В документации прямым текстом: безопасность и производительность filter-branch нельзя исправить обратно совместимо, используйте альтернативу вроде git filter-repo. На пути к Git 3.0 команду прячут за флаг готовности к удалению. Уже сейчас при запуске она печатает большое предупреждение и заставляет ставить переменную окружения, чтобы согласиться продолжить.
Что с ним не так на практике. Во-первых, дико медленно: filter-branch на каждый коммит форкает шелл и запускает внешние процессы, на репо в десятки тысяч коммитов это часы. Во-вторых, ловушки на каждом шагу: он не чистит рефлог и оригинальные рефы автоматически (оставляет их в refs/original/), легко получить полупереписанную историю и думать, что всё готово. В-третьих, дефолты небезопасны.
git filter-repo - это сторонний инструмент, один Python-скрипт за авторством Элайджи Ньюрена (он же мейнтейнер Git). Важно: в сам Git он не входит, ставится отдельно. Но именно его рекомендует апстрим. Он на порядки быстрее (один проход через быстрый поток fast-export/fast-import), по умолчанию делает безопасные вещи: чистит оригинальные рефы, прогоняет gc, и - ключевое - он заточен под свежий клон и сопротивляется запуску на репозитории, где есть незакоммиченное или нестандартная конфигурация, чтобы ты не выстрелил себе в ногу.
Установка и первый запуск
filter-repo - один файл, но проще через пакетный менеджер.
Код: Выделить всё
# macOS
brew install git-filter-repo
# Debian/Ubuntu
sudo apt install git-filter-repo
# через pip в любой системе
pip install git-filter-repo
# проверка
git filter-repo --version
Правило номер один, которое инструмент навязывает сам: работай на свежем клоне. filter-repo по умолчанию откажется запускаться, если репозиторий не выглядит как чистый клон, потому что после переписывания старая история не восстанавливается. Делай так:
Код: Выделить всё
git clone --no-local /path/to/repo repo-clean
cd repo-clean
Удалить файл из истории git
Самая частая задача - удалить секрет из git или выкинуть раздутый файл. Делается через --path и --invert-paths.
--path выбирает пути, --invert-paths инвертирует выбор: вместо "оставить только это" получается "удалить это, оставить всё остальное".
Код: Выделить всё
# выкинуть config/secrets.yml из ВСЕЙ истории
git filter-repo --path config/secrets.yml --invert-paths
Код: Выделить всё
Parsed 312 commits
New history written in 1.84 seconds; now repacking/cleaning...
Repacking your repo and cleaning out old unneeded objects
HEAD is now at a1f3c8e Fix login redirect
Completely finished after 3.12 seconds.
Несколько путей за раз - повторяй --path:
Код: Выделить всё
git filter-repo --path dumps/ --path build/artifacts.bin --invert-paths
Код: Выделить всё
# оставить только frontend/, поднять его в корень нового репо
git filter-repo --path frontend/ --path-rename frontend/:
Замена текста и смена автора через mailmap
Иногда секрет нельзя просто удалить файл целиком - пароль зашит в строке конфига, который нужен. Тогда --replace-text заменяет содержимое во всех версиях всех файлов. Готовишь файл правил:
Код: Выделить всё
# expressions.txt
supersecretpassword==>***REMOVED***
regex:AKIA[0-9A-Z]{16}==>AWS_KEY_REMOVED
Код: Выделить всё
git filter-repo --replace-text expressions.txt
Код: Выделить всё
# .mailmap: Правильное Имя <new@corp.com> Старое Имя <old@personal.com>
Ivan Petrov <ivan@corp.com> <ivan.personal@gmail.com>
Ivan Petrov <ivan@corp.com> ipetrov <ipetrov@laptop.local>
Код: Выделить всё
git filter-repo --mailmap .mailmap
BFG Repo-Cleaner как альтернатива
Есть второй популярный инструмент - bfg repo cleaner. Это Java-утилита (jar), заточенная под две задачи: удалить большие файлы и удалить секреты. Она проще filter-repo в типовых случаях и ощутимо быстрее filter-branch.
Код: Выделить всё
# выкинуть все блобы тяжелее 50 мегабайт
bfg --strip-blobs-bigger-than 50M repo.git
# заменить тексты-секреты из файла
bfg --replace-text passwords.txt repo.git
# после BFG обязательно дочистить:
cd repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git replace: переписать историю, не переписывая её
Иногда переписывание - перебор. Все хеши меняются, все клоны ломаются. Есть хирургически точная альтернатива - git replace. Идея: положить в репозиторий правило подмены одного объекта другим, не трогая сами исторические объекты. Старый объект остаётся на месте, но Git при чтении видит вместо него подменный.
Механика простая и красивая. git replace создаёт ссылку в namespace refs/replace/. Имя ссылки - это хеш заменяемого объекта, содержимое - хеш замены. Всё. Когда Git встречает объект, у которого есть запись в refs/replace, он молча подставляет замену.
Код: Выделить всё
git replace <старый-объект> <новый-объект>
git replace --list # показать все подмены
git replace -d <старый> # удалить подмену
Код: Выделить всё
# у коммита-корня new-root сделать родителем old-tip из архива
git replace --graft <new-root> <old-tip>
Код: Выделить всё
# превратить виртуальные подмены в реальную переписанную историю
git filter-repo --replace-refs delete-and-add
После переписывания: force-push и обязательная ротация
Вот тут ломаются почти все. После любого rewrite все существующие клоны невалидны. У них старые хеши, у тебя новые. Обычный git push отклонит как non-fast-forward, обычный git pull у коллег устроит чудовищный мердж старой истории с новой.
Заливаешь так:
Код: Выделить всё
git push --force-with-lease origin --all
git push --force-with-lease origin --tags
Все коллеги после этого делают свежий клон. Не pull, не fetch - именно заново clone. Любые их локальные ветки на старой истории надо переносить вручную через rebase --onto или, проще, переделать поверх новой базы.
И теперь главное, что путают новички. Ты переписал историю и убрал пароль. Это не значит, что пароль в безопасности. Во-первых, он уже неделю лежал в публичном (или внутреннем) репо - его могли скопировать, проиндексировать, забрать боты. Во-вторых, на форках, в чужих клонах, в кэшах CI, в зеркалах - старые объекты с секретом по-прежнему существуют, и их хеш не изменился. GitHub вообще может ещё какое-то время отдавать старый коммит по прямому хешу.
Вывод железный: переписывание истории не отменяет утечку, оно только убирает секрет из будущих клонов. Утёкший секрет надо ротировать - сменить пароль, отозвать токен, перевыпустить ключ. Считай скомпрометированным всё, что хоть раз попало в коммит. Сначала ротация, потом - для гигиены - чистка истории. Не наоборот.
Типичные грабли
- Запуск на рабочем репо без клона. Один --force-промах, и восстановить нечего. Всегда свежий git clone --no-local.
- Думать, что git rm -rf + новый коммит чистит историю. Нет. Старый blob жив в старых коммитах целиком.
- Забыть про теги. --all пушит ветки, теги пушатся отдельно. filter-repo переписывает и теги, но залить их надо явно.
- Чистка без ротации. Самая опасная ошибка: секрет считается утёкшим в момент первого коммита, а не в момент обнаружения.
- filter-branch в 2026. Печатает варнинг, требует переменную, медленный, оставляет refs/original. Бери filter-repo.
- Долго жить на git replace. Отключённый commit-graph душит производительность. Replace - временный мост, не постоянное жильё.
- Коллеги делают pull вместо clone. Получают перемешанную старую и новую историю. Только полный новый клон.
Повтори по шагам, это пять минут.
Код: Выделить всё
# 1. песочница с "утёкшим" секретом
mkdir leak-lab && cd leak-lab
git init -b main
echo "db_password = hunter2" > secrets.env
git add secrets.env && git commit -m "add config"
echo "app code" > app.py
git add app.py && git commit -m "feature"
git rm secrets.env && git commit -m "remove secrets (naive)"
# 2. докажи что секрет ЖИВ в истории
git log --all --oneline -- secrets.env
git show HEAD~2:secrets.env # видишь hunter2 - он на месте
# 3. свежий клон для чистки
cd .. && git clone --no-local leak-lab leak-clean
cd leak-clean
# 4. вычистить файл из ВСЕЙ истории
git filter-repo --path secrets.env --invert-paths
# 5. проверь что секрет исчез отовсюду
git log --all --oneline -- secrets.env # пусто
git log --oneline # хеши коммитов изменились
Контрольные вопросы
- Ты удалил файл с паролем командой git rm и закоммитил. Почему пароль всё ещё в репозитории и как его убрать по-настоящему?
- Чем git filter-repo принципиально лучше git filter-branch и почему filter-branch не советуют в 2026?
- В чём разница между git replace --graft и реальным переписыванием через filter-repo? Когда какой подход выбрать?
- Ты вычистил секрет из истории и сделал force-push. Считается ли инцидент закрытым? Что обязательно сделать ещё?
Массовое переписывание истории - это пересборка всей цепочки коммитов от точки правки до вершины, после которой меняются все хеши и ломаются все клоны. Штатный инструмент 2026 - сторонний git filter-repo: --invert-paths чтобы удалить файл из истории git, --replace-text для замены секретов, --mailmap для смены автора, --path-rename для выделения подкаталога. filter-branch не рекомендован и уходит из Git 3.0. BFG - быстрая альтернатива для больших блобов. git replace даёт хирургическую подмену объекта без rewrite (особенно --graft для склейки истории), но отключает commit-graph, поэтому он временный. И запомни намертво: после чистки нужен force-with-lease, свежий клон у всех коллег и обязательная ротация утёкшего секрета - хеш с паролем уже разошёлся, и только смена самого пароля закрывает дыру.[/body]
</invoke>