Как безопасно отменить локальные и удаленные коммиты в Git

Безопасно отменяйте коммиты Git с помощью reset, revert, reflog и force-with-lease, не теряя работу и не нарушая общие ветки.

Как безопасно отменить локальные и удаленные коммиты в Git

Отменить коммит Git легко. Отменить правильное действие, не потеряв работу и не удивив всех остальных в ветке, — вот что требует суждения.

Первый вопрос не "какую команду запустить?" Это "кто видел этот коммит?" Если коммит существует только на вашем ноутбуке, вы обычно можете переписать его с помощью git reset или git commit --amend. Если коммит уже отправлен в ветку, которую используют другие, предпочтительнее git revert. Это сохраняет историю нетронутой и создает новый коммит, который откатывает плохое изменение.

Прежде чем трогать историю, сделайте быстрый снимок текущего состояния:

git status
git branch backup-before-undo
git log --oneline --decorate -5

Эта временная ветка — дешевая страховка. Если вы сбросите слишком далеко или передумаете, старый коммит все еще будет иметь имя.

Если коммит не был отправлен

Для локального коммита, который вы не отправляли, git reset обычно является самым чистым инструментом. Он перемещает указатель вашей ветки назад. Выбранный вами режим определяет, что произойдет с файлами.

Используйте --soft, когда сообщение коммита было неправильным или вы забыли один маленький файл:

git reset --soft HEAD~1

Ваш последний коммит исчезает, но изменения остаются в индексе. Вы можете добавить недостающий файл и снова сделать коммит:

git add missing-file.yml
git commit -m "Обновить конфигурацию развертывания"

Используйте смешанный сброс по умолчанию, когда хотите вернуть изменения в рабочее дерево, не в индексе:

git reset HEAD~1

Это повседневная команда "отменить коммит, но сохранить мои правки". Она полезна, когда один коммит должен стать двумя меньшими коммитами, или когда вы закоммитили отладочный вывод вместе с реальным кодом.

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

git reset --hard HEAD~1

Это сбрасывает отслеживаемые файлы до более раннего коммита. Он не вежливо спрашивает, имели ли вы это в виду. Если у вас есть незакоммиченная работа в отслеживаемых файлах, она может исчезнуть из рабочего дерева. Сначала проверьте git status и stash или создайте ветку для того, что может понадобиться.

Для небольшого исправления последнего локального коммита git commit --amend часто лучше, чем reset:

git add corrected-file.js
git commit --amend

Это заменяет последний коммит новым. Применяется то же правило: свободно изменяйте до отправки; будьте осторожны после отправки.

Если коммит уже был отправлен

В общей ветке используйте git revert, если у вас нет веской причины переписывать историю. Revert создает новый коммит, который применяет противоположный патч.

git revert a1b2c3d

Эта команда открывает ваш редактор с сгенерированным сообщением. Сохраните его, и Git создаст новый коммит. Исходный коммит остается в истории, что именно то, что вам нужно в main, master, develop, релизных ветках и любой ветке, которую могли получить коллеги.

Если вам нужно отменить несколько последовательных коммитов, вы можете сделать это одним подготовленным пакетом:

git revert --no-commit HEAD~3..HEAD
git status
git commit -m "Отменить последние изменения развертывания"

Внимательно прочитайте диапазон. HEAD~3..HEAD означает последние три коммита, не включая сам HEAD~3. Если сомневаетесь, сначала выведите список коммитов:

git log --oneline HEAD~3..HEAD

Слияния коммитов требуют одного дополнительного решения. У слияния более одного родителя, поэтому Git нужно знать, какую сторону следует рассматривать как основную линию:

git revert -m 1 <sha-коммита-слияния>

Большинство команд используют -m 1 при отмене слияния функциональной ветки из целевой ветки, но не запускайте это вслепую. Посмотрите на коммит слияния с помощью git show --summary <sha> и подтвердите порядок родителей.

Если вы отправили неправильную вещь и должны удалить ее

Иногда revert недостаточно. Если вы отправили секрет, огромный бинарный файл или личные данные клиента, revert удаляет их только из последнего дерева. Конфиденциальное содержимое все еще существует в истории. Это становится проблемой реагирования на инциденты, а не просто проблемой очистки Git.

Для секретов сначала смените учетные данные. Затем удалите данные из истории с помощью подходящего инструмента перезаписи истории, такого как git filter-repo, BFG Repo-Cleaner или специфичного для хоста процесса удаления секретов. Скоординируйтесь с владельцем репозитория и предполагайте, что любой, у кого есть доступ, мог получить плохой коммит до того, как вы его удалили.

Для обычного ошибочного коммита в вашей собственной функциональной ветке перезапись удаленной ветки может быть приемлемой. Сбросьте локально, затем выполните принудительную отправку с защитой:

git reset --hard HEAD~1
git push --force-with-lease origin my-feature-branch

--force-with-lease — более безопасная форма, потому что она отказывается обновлять удаленную ветку, если кто-то другой отправил новую работу после вашего последнего получения. Это все еще принудительная отправка. Она все еще перезаписывает ветку. Используйте ее на личных или согласованных функциональных ветках, а не casually на общих ветках.

Хорошая привычка перед принудительной отправкой:

git fetch origin
git log --oneline --left-right --graph origin/my-feature-branch...my-feature-branch

Это показывает, что существует только на удаленной ветке и что существует только локально. Если левая сторона содержит чьи-то чужие коммиты, остановитесь и поговорите с ними.

Восстановление с помощью reflog

git reflog — это команда, которая спасает многие плохие дни. Она записывает, куда недавно указывали ваши локальные HEAD и ссылки на ветки. Если вы сбросили не туда, удалили ветку или изменили не тот коммит, reflog часто знает старый SHA коммита.

git reflog

Вы можете увидеть что-то вроде:

7cc8a91 HEAD@{0}: reset: moving to HEAD~1
2b41f0d HEAD@{1}: commit: add retry around deploy step

Чтобы восстановить старый коммит, создайте ветку на нем:

git branch recovered-deploy-work 2b41f0d

Я предпочитаю создавать ветку вместо немедленного выполнения еще одного жесткого сброса. Это дает вам стабильный указатель, и вы можете спокойно просмотреть восстановленную работу:

git show recovered-deploy-work
git switch recovered-deploy-work

Reflog локальный. Reflog вашего коллеги не будет содержать ваши потерянные локальные коммиты, и удаленные хосты могут не предоставлять тот же путь восстановления. Он также со временем очищается в соответствии с настройками сборки мусора Git, поэтому относитесь к нему как к недавней сети безопасности, а не как к архивному хранилищу.

Практическое руководство по принятию решений

Если вы закоммитили слишком рано и не отправляли, используйте git reset --soft HEAD~1 или git reset HEAD~1.

Если вы закоммитили неправильный файл и хотите избавиться от правок локально, используйте git reset --hard HEAD~1 только после проверки git status.

Если плохой коммит уже находится в общей ветке, используйте git revert <sha>.

Если ваша функциональная ветка удаленная, но используете ее только вы, git reset плюс git push --force-with-lease обычно приемлемо.

Если коммит содержит секрет или конфиденциальные данные, не полагайтесь на revert. Смените секрет, перепишите историю с помощью правильного инструмента и скоординируйте очистку.

Самый безопасный рабочий процесс отмены Git скучен: сначала проверьте, создайте резервную ветку, выберите команду на основе того, является ли коммит общим, и используйте reflog, когда нужно восстановиться после собственной попытки восстановления.