Существует множество ответов о как удалить конфиденциальные коммиты, например, Удалить конфиденциальные файлы и их коммиты из истории Git . Любой хороший ответ предупреждает вас, что в любом случае, вероятно, уже слишком поздно, и это правда. Не так много подробностей о , когда и почему слишком поздно, но ответ довольно прост: не так уж много использования . Остальная часть этого ответа о том, когда и почему слишком поздно, и почему просто удалить коммит с интерактивной перебазировкой недостаточно.
Суть проблемы в том, что коммиты не могут быть изменены, и Git готов добавить new коммитов. Удаление старых / мертвых коммитов (и других мертвых объектов) происходит как побочный эффект, с небольшим контролем с вашей стороны. Когда вы делаете что-либо практически - независимо от того, что: git commit --amend
, git rebase -i
, git reset --hard
, ничего из этого не имеет значения - любой существующий коммит остается в вашей базе данных коммитов, неизменным, невозмущенным и все еще доступным по хэш-идентификатору. Тем не менее, можно реально удалить коммит. Просто сложно сделать это контролируемым и правильным образом.
Представление и нахождение коммитов
Каждый коммит - фактически, каждый объект 1 в основной базе данных Git - доступен по его хэш-идентификатору. Идентификатор хэша last commit в ветви находится во второй, меньшей базе данных. По сути, имя ветви, такое как master
, говорит: коммит-коммит master
равен a123456...
, который предоставляет хэш-идентификатор объекта фиксации, чтобы вы - или Git - могли вернуться к Основная база данных и сказать: Получить мне объект a123456...
.
Каждый коммит может перечислять хеш-код (ы) некоторых предыдущих или родительских коммитов. То есть, получив объект a123456...
, вы можете ловить рыбу внутри него для родительских хеш-идентификаторов. Если (один) родительский хэш-идентификатор a123456...
равен 9876543...
, вы возвращаетесь в основную базу данных и говорите: * Get me object 9876543...
, и у вас есть предыдущий коммит. Вот как вы - и Git - можете начинать с конца ветви и работать в обратном направлении, по одному коммиту за раз:
... <-grandparent <-parent <-last-commit <--branchname
Если мы используем одиночные заглавные буквы для обозначения хеш-идентификаторов и просто помним , что стрелки (от дочернего к родительскому) всегда указывают назад, мы получим нечто, что будет легче нарисовать, когда у вас несколько ветвей :
...--E--F--G <-- master
\
H <-- develop
Но во всех случаях, когда вы делаете что-то, чтобы «изменить» свою историю - например, если мы решим, что фиксация G
плохая и ее необходимо заменить - вы на самом деле ничего не измените . Вместо этого Git по сути просто удаляет плохой коммит с пути:
G
/
...--E--F--I <-- master
\
H <-- develop
Основная база данных объектов не очищена немедленно, и если у вас есть любой способ запомнить хэш-идентификатор commit G
, вы можете попросить Git о G
по этому хэш-идентификатору. Git представит его вам, потому что находится в базе данных!
Это то же самое описание верно, независимо от того, как вы «удаляете» или «меняете» коммит: Git просто делает копии каждого другого коммита, так что «удаленный» или «измененный» коммит (здесь G
должно быть удалено) теперь находится на другой ветке:
...--o--F--G--H--J--... <-- branch
становится:
G--H--J--... [previous branch, now abandoned]
/
...--o--F--H'-J'-... <-- branch
, где H'
- это копия H
, настроенная на добавление после F
вместо G
, J'
- копия J
, адаптированная на поставку после H'
, и так далее. Опять же, G
на самом деле не ушло , оно просто сброшено с пути вместе со всеми его потомками. Все его потомки заменены слегка измененными копиями с новыми разными хэш-идентификаторами.
1 Существует четыре типа объектов. Commit , tree и blob объекты работают вместе для хранения файлов в коммитах, с аннотированным тегом объектов четвертого типа. Каждый коммит относится к одному дереву; это дерево ссылается на дополнительные поддеревья, если необходимо, и на BLOB-объекты для хранения файлов, которые сопровождают этот коммит.
Удаление коммц
Итак, когда - и как и почему - коммиты в конечном итоге исчезают? Ответ заключается в том, что Git имеет команду обслуживания git gc
, задача которой состоит в том, чтобы обходить всю основную базу данных каждого объекта, а также обходить другую базу данных всех имен, по которой можно найти объекты. Если есть имя no , по которому мы можем найти commit G
, после операции, подобной описанной выше, git gc
определит, что это так, и - в конечном итоге - выкинет G
из Основная база данных, использующая любые обычные функции удаления операционной системы для удаления файла. 2
Более формально, чтобы git gc
удалить объект из основной базы данных, объект должен быть недоступен . Для подробного обсуждения понятия достижимости см. Think Like (a) Git . К сожалению, для вашего конкретного случая использования набор имен, по которым мы можем достичь коммитов, включает в себя любой коммит в любом reflog .
2 Как правило, это небезопасное удаление, так что если у вас есть контроль над базовым носителем, вы все равно можете получить данные обратно таким образом, но теперь очевидно, что гораздо труднее. В любом случае, теперь никто не может просто попросить Git-репозиторий для коммита G
по хеш-идентификатору. Остерегайтесь файловых систем, которые поддерживают моментальные снимки, однако: вы можете просто вернуться к предыдущему снимку и восстановить весь репозиторий, каким он был на момент моментального снимка!
Поиск коммитов часть 2: reflogs
Существует рефлог для каждого имени ветви, например master
, плюс один для HEAD
. (Возможно, есть дополнительные reflogs, но здесь есть два важных.) В приведенном выше примере commit G
больше не доступен с именем master
, но есть еще две записи reflog, master@{1}
и HEAD@{1}
, оба сервера для поиска коммита G
. Так что git gc
не удалит коммит G - пока нет.
Записи reflog, которые найдут G
, будут удалены, в конце концов. В частности, git reflog expire
автоматически удаляет достаточно старые и, следовательно, expired reflog записей. Сколько лет вам достаточно - это то, что вы можете настроить, но по умолчанию оно составляет 30 или 90 дней, 3 , а в данном случае - 30 дней.
Что означает , так это то, что по умолчанию G
будет оставаться до тех пор, пока git gc
не использует git reflog
для удаления записей reflog, как только они станут достаточно старыми, т. Е. Не менее 30 дней с этого момента. Вы можете использовать git reflog
(см. документацию ), чтобы быстрее удалить или истечь записи для G
, если вы хотите ускорить эту часть; или см. клонирование ниже.
Как только записи reflog исчезнут, так что G
действительно (глобально) недоступен, git gc
удалит его. Вы можете сказать, что это произошло, потому что git show <em>hash</em>
и git rev-parse <em>hash</em>
скажут вам, что они понятия не имеют, о каком хэш-идентификаторе вы говорите.
Помните также, что если ваш Git связался с другим Git, ваш Git мог дать этому другому Git коммит G
. В частности, когда вы запускаете git push
, ваш Git вызывает другой Git и передает их коммитам. Если вы дали их коммит G
, то ничто из того, что вы делаете в своем собственном хранилище, не сможет вернуть это. Если вы разрешите другим пользователям git fetch
из вашего хранилища, они, возможно, взяли копию G
, и опять же, ничто из того, что вы делаете в своем собственном хранилище, не может забрать это обратно: вы должны убедить их в отменить коммит.
ReflogФайлы git clone
не копируются, поэтому другой способ избавиться от G
без ожидания - клонировать свой собственный репозиторий. git clone
создает новый репозиторий, а затем извлекает его из исходного репозитория. Коммиты, которые получает выборка, - это те, которые доступны из имен, которые предоставляет исходный репозиторий. Таким образом, вместо ручного истечения срока действия некоторых записей reflog и последующего запуска git gc
, вы можете просто клонировать свой собственный репозиторий. Здесь есть один недостаток: вы теряете сеть безопасности всех своих повторных журналов, и ваши собственные имена ветвей становятся именами origin/*
вашего нового хранилища. 4
3 Выбор между 30 и 90 днями здесь зависит от того, достижимо ли значение в reflog из фиксации, на которую указывает сама ссылка. В этом случае имя master
указывает, например, на фиксацию I
, и невозможно вернуться назад от I
к G
, поэтому значение в master@{1}
, которое указывает на G
, недоступно из значения master
. Это означает, что срок действия gc.reflogExpireUnreachable
- тот, который по умолчанию равен 30 дням, а не gc.reflogExpire
, который по умолчанию равен 90 дням.
Обратите внимание, что мы опять зависим от концепции достижимости через ориентированный граф. Это один из ключей к пониманию Git.
4 Вы можете использовать git clone --mirror
, но вы получите bare хранилище и хранилище с неподходящим значением по умолчанию fetch
. Затем вы можете исправить эти два, но если вы знаете, как все это сделать, вы все равно, вероятно, захотите использовать что-то отличное от --mirror
. ?
Резюме
Если:
- вы не поделились нежелательными коммитами ни с кем (без извлечений или толчка), и
- Вы удаляете все ссылки на коммиты или ждете 30 дней, а затем запускаете
git gc
затем фиксация действительно исчезнет, если не произойдет никакого воскрешения через моментальные снимки уровня файловой системы. Вы можете передать хеш-код на git show
или git rev-parse
, чтобы убедиться, что он пропал. Но если фиксация могла быть скопирована где-либо еще, вы больше не можете это контролировать.
Безопасное значение по умолчанию - предполагать, что если коммит был виден кому-либо еще в течение какого-либо периода времени, он был скопирован , и секреты, которые были в нем, больше не являются секретными.