- Андрей Куманяев/
- Git: руководства и команды/
- git filter-branch: переписывание истории и удаление файлов/
git filter-branch: переписывание истории и удаление файлов
git filter-branch — инструмент для переписывания истории репозитория. Позволяет удалить файл из каждого коммита, изменить email автора, трансформировать дерево файлов. Команда мощная, но опасная и медленная. Современная альтернатива — git filter-repo.
Когда используется filter-branch #
Типичные сценарии:
- Случайно закоммитили пароли или API ключи
- Нужно удалить большой файл из всей истории
- Изменить email/имя автора во всех коммитах
- Вычленить подпапку в отдельный репозиторий
- Объединить несколько репозиториев
Важно: после filter-branch все SHA-хеши коммитов изменятся. Все, у кого есть клон репозитория, должны будут заново клонировать.
Удаление файла из всей истории #
Наиболее частый сценарий — удаление файла с секретами:
# Клонировать репозиторий (работать с копией!)
git clone [email protected]:username/project.git
cd project
# Удалить файл из каждого коммита
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch secrets.env' \
--prune-empty --tag-name-filter cat -- --all
# --force — перезаписать существующие refs
# --index-filter — фильтр для индекса (быстрее tree-filter)
# git rm --cached — удалить из индекса
# --ignore-unmatch — не ошибаться если файла нет в коммите
# --prune-empty — удалить коммиты ставшие пустыми
# --tag-name-filter cat — обновить теги
# -- --all — обработать все ветки
# Очистить refs и собрать мусор
git for-each-ref --format="delete %(refname)" refs/original | git update-ref --stdin
git reflog expire --expire=now --all
git gc --prune=now --aggressive
# Force push
git push origin --force --all
git push origin --force --tags
Удаление папки из истории #
# Удалить папку node_modules из всей истории
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch -r node_modules/' \
--prune-empty --tag-name-filter cat -- --all
# После очистки
git gc --prune=now --aggressive
Изменение email автора во всех коммитах #
git filter-branch --env-filter '
OLD_EMAIL="[email protected]"
NEW_NAME="New Name"
NEW_EMAIL="[email protected]"
if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]
then
export GIT_COMMITTER_NAME="$NEW_NAME"
export GIT_COMMITTER_EMAIL="$NEW_EMAIL"
fi
if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]
then
export GIT_AUTHOR_NAME="$NEW_NAME"
export GIT_AUTHOR_EMAIL="$NEW_EMAIL"
fi
' --tag-name-filter cat -- --all
–tree-filter vs –index-filter #
# --tree-filter: работает с файлами на диске (медленнее, но проще)
git filter-branch --tree-filter 'rm -f secrets.env' -- --all
# --index-filter: работает с индексом Git (быстрее)
git filter-branch --index-filter \
'git rm --cached --ignore-unmatch secrets.env' -- --all
# Для больших репозиториев: --index-filter намного быстрее
Вычленение подпапки в отдельный репозиторий #
# Оставить только содержимое папки src/
git filter-branch --subdirectory-filter src/ -- --all
# Теперь содержимое src/ стало корнем репозитория
# Историю файлов вне src/ нет
Современная альтернатива: git filter-repo #
git filter-branch официально рекомендован к замене на git filter-repo. Причины:
- В 10-50 раз быстрее
- Безопаснее (не оставляет резервных refs)
- Более удобный синтаксис
- Активно поддерживается
# Установить git-filter-repo
pip install git-filter-repo
# или
brew install git-filter-repo
# Удалить файл из всей истории
git filter-repo --path secrets.env --invert-paths
# Удалить папку
git filter-repo --path node_modules/ --invert-paths
# Изменить email автора
git filter-repo --email-callback '
return email if email != b"[email protected]" else b"[email protected]"
'
# Вычленить подпапку
git filter-repo --subdirectory-filter src/
# Force push (после filter-repo нужен fresh clone или --force)
git push origin --force --all
После переписывания истории #
Когда история переписана, нужно уведомить команду:
# 1. Force push на все ветки
git push origin --force --all
git push origin --force --tags
# 2. Инструкции для других разработчиков:
# Все локальные клоны устарели после force push
# Каждый разработчик должен:
git fetch origin
# Затем для каждой ветки:
git checkout main
git reset --hard origin/main
# ИЛИ проще:
git clone <repo-url> fresh-clone
Отмена filter-branch #
Если filter-branch создал refs/original:
# Вернуться к исходному состоянию (если ещё не gc)
git reset --hard refs/original/refs/heads/main
# Удалить резервные refs (после подтверждения результата)
git for-each-ref --format="delete %(refname)" refs/original | \
git update-ref --stdin
Проверка результата #
# Убедиться что файл удалён из истории
git log --all --full-history -- secrets.env
# (должен быть пустой вывод)
# Проверить что файл не остался в каком-либо коммите
git grep 'password' $(git log --all --oneline | awk '{print $1}')
Часто задаваемые вопросы #
Нужно ли также инвалидировать скомпрометированные ключи? Да, обязательно. Удаление файла из истории Git не означает, что секрет в безопасности — он мог быть уже скопирован кем-то из просмотра истории. Смените все скомпрометированные ключи/пароли немедленно.
Можно ли отменить filter-branch после force push? Нет, если у других нет копии старой истории. Именно поэтому важно работать с клоном и убедиться в результате перед force push.
Почему нужен git gc после filter-branch? Git сохраняет старые объекты как мусор. Без git gc --prune=now старый файл останется в .git/objects и технически ещё доступен.
Что такое –prune-empty? После удаления файлов некоторые коммиты могут остаться пустыми (без изменений). --prune-empty удаляет такие коммиты из истории.
git filter-repo или BFG? Оба быстрее и удобнее filter-branch. git filter-repo предпочтительнее для большинства задач — активно разрабатывается. BFG проще для базовых сценариев (удалить файл, очистить большие файлы).
Заключение #
git filter-branch — мощный но устаревший инструмент для переписывания истории. Для новых проектов используйте git filter-repo (быстрее, проще, безопаснее). Основные сценарии: удаление секретов (--path file --invert-paths), изменение авторов (--email-callback), вычленение подпапки (--subdirectory-filter). После любого переписывания истории — смените скомпрометированные ключи и уведомьте команду о необходимости пересинхронизации. Подробнее об истории коммитов — git reflog.