Ветки и хирургия

Удаление файла из истории

git rm перестаёт показывать файл в новых коммитах. С уже сделанными он не делает ничего: блоб по-прежнему в базе объектов, по-прежнему в каждом клоне и по-прежнему в одном git show от вас.

Это важно в двух случаях — когда файл был ключом доступа и когда он весил 400 МБ.

⌘K → Удалить файл из истории, либо правый клик по файлу — в дереве проекта, в списке файлов коммита или в композере коммита. Обычно человек осознаёт, что файл всё ещё в истории, именно на том коммите, который его удалил, — поэтому выход находится и в этом меню.

Как найти путь

Два способа, потому что они отвечают на разные вопросы.

Ввести вручную — путь относительно корня репозитория, без ведущего слеша — когда вы уже знаете, что пришли удалять.

Просмотреть историю — когда не знаете. Здесь перечислены все пути, которые когда-либо коммитились, самые тяжёлые сверху, с числом версий у каждого и признаком, отслеживается ли он до сих пор. Удалённые пути помечены как таковые и обычно это как раз то, что нужно: файл, которого нет в рабочем дереве, но который есть в каждом клоне, — ровно тот случай, который обычный файловый диалог показать не может, потому что выбирать там нечего.

Тот же список отвечает и на другую причину прийти сюда — почему этот клон весит два гигабайта, — поскольку он отсортирован по числу байт, которые реально занимают блобы каждого пути. Выбор строки сразу же запускает измерение.

Все пути, когда-либо попадавшие в коммиты, самые тяжёлые сверху, с пометкой удалённых

Измерьте, прежде чем соглашаться

Нажмите Измерить (или выберите строку). Пока ничего не записывается. Вы получаете:

Коммитов будет переписано Каждый коммит, начиная с первого, где файл присутствовал
Ветки / теги Ссылки, которые сдвинутся
Занято блобами Байты, которые эти версии реально занимают
Первый коммит Место, откуда начинается переписывание — всё после него получит новый хеш

Измерение: сколько коммитов будет переписано, какие ссылки затронуты, сколько байт занято, и предупреждение всё равно сменить секрет

Если счётчик нулевой — путь неверный. Обычно это опечатка или потерянный префикс каталога, а не отсутствие файла.

Что переписывание делает на самом деле

Gitcito копирует каждую ветку и каждый тег в refs/gitcito/pre-purge/<timestamp>/…, а затем запускает:

git filter-branch --force \
  --index-filter 'git rm --cached --ignore-unmatch -- <path>' \
  --prune-empty --tag-name-filter cat -- --branches --tags

--index-filter переписывает индекс напрямую, вместо того чтобы выгружать каждый коммит, — это разница между минутами и часами. --branches --tags вместо --all выбрано намеренно: --all захватил бы и резервные ссылки, и переписывание съело бы собственную страховку.

Коммиты, в которых не было ничего, кроме удаляемого файла, выбрасываются (--prune-empty). Теги перенаправляются на свои переписанные коммиты.

Бэкап и почему место пока не возвращается

Чистку можно отменить, и цена этого в том, что место на диске не освобождается, пока вы не скажете. Пока существует бэкап, старые коммиты остаются достижимыми, и git их не соберёт.

Действие Эффект
Восстановить Каждая ветка и каждый тег возвращаются на тот коммит, где они были до чистки; вместе с ними возвращается и файл
Удалить бэкап Удаляет резервные ссылки, чистит reflog, запускает git gc --prune=now — место возвращено, чистка теперь необратима

Два шага, а не один, потому что первый — это обратимая половина, а второй — нет.

Секрет всё равно смените

Если ключ хоть раз был отправлен на сервер, переписывание вашей истории не отменяет утечку. Кто-то мог его вытянуть; серверы форжей держат у себя объекты без ссылок; лог CI мог его напечатать. Переписывание останавливает дальнейшее распространение — оно не отменяет самого раскрытия.

Смените ключ. А потом чистите историю, чтобы следующий клонирующий его там не нашёл.

Чего это не сделает

  • Оно не запушит. Переписывание локально. Опубликовать результат — значит сделать форс-пуш в каждую затронутую ветку, а всем остальным придётся заново клонировать или делать hard reset; за это решение отвечает защита от форс-пуша.
  • Оно откажется работать на грязном рабочем дереве или посреди слияния/ребейза. Переписывание многократно двигает HEAD, и делать это вокруг незакоммиченной работы — верный способ её потерять.
  • Оно переписывает по пути, а не по содержимому. Удалить секрет, вставленный прямо в исходник, а не живущий в отдельном файле, — задача для фильтра по содержимому: это территория git filter-repo --replace-text, и Gitcito её не оборачивает.
  • filter-branch медленный на очень больших историях. Зато он есть в комплекте с git везде — поэтому его Gitcito и использует. На репозитории с десятками тысяч коммитов git filter-repo в терминале — более быстрый инструмент.
  • Чужие клоны — не ваш репозиторий. Там останется старая история, пока их не переклонируют.

Улучшить этот перевод на GitHub