open atlas
↑ К треку
Git: от нуля до сеньора GIT · 10 · 02

Инцидент: плохой merge

Плохой merge попадает в main и CI краснеет. Под давлением: диагностика через log --graph и show, затем revert merge с -m 1 (никогда reset общей истории) и восстановление force-push коммитов через reflog из клона или с сервера. Revert против reset, решаемое в бою.

GIT Senior ◷ 19 min
Уровень
ОсновыJuniorMiddleSenior

16:40. Кто-то смёржил недоделанную фичеветку в main, CI красный, и деплой-пайплайн заблокирован для всей команды. Давление реальное, и соблазн — снести это через git reset --hard. Этот инстинкт и есть опасный: main — это общая история, и переписывание общей истории под давлением превращает один инцидент в два.

К концу этого урока ты сможешь вести инцидент с плохим merge так, как это делает сеньор: диагностировать виновный merge, решать revert-против-reset по тому, общая ли история, корректно откатывать merge-коммит через -m 1 и восстанавливать коммиты, стёртые паническим force-push, — через git reflog.

Цель

После этого урока ты сможешь реагировать на плохой merge в общей ветке: найти merge-коммит через git log --graph и git show, отменить его через git revert -m 1 вместо reset, восстановить force-push коммиты из reflog коллеги или с сервера и объяснить правило «никогда не переписывай общую историю», которое управляет всем решением.

1

Сначала диагностика: найди точный коммит, сломавший main. Не угадывай — читай граф. Merge-коммит — это тот, у которого два родителя, стоящий там, где main покраснел:

git fetch origin
git log --graph --oneline --decorate origin/main | head -30
git show <merge-sha>          # увидеть, что смёржено, и линию родителей

git show на merge печатает объединённый diff и, что важно, список родителей (Merge: a1b2c3 d4e5f6). Первый родитель — это ветка, на которой ты был в момент merge, — main, а второй — фичеветка, которую подтянули. Этот порядок родителей и есть то, от чего зависит revert на шаге 3.

2

Выбирай инструмент по одному вопросу: эта история запушена и общая? Это всё дерево решений, и оно бинарно:

  • Общая / уже запушена (плохой merge на origin/main): нужно дописать исправление, никогда не переписывать. Используй git revert.
  • Только локальная (merge есть лишь на твоей машине, никогда не пушился): можно переписывать свободно. git reset --hard <до-merge> нормально.

В командном инциденте ответ почти всегда «общая», потому что merge на origin/main и коллеги его уже стянули. Переписывание через reset + force-push стёрло бы коммиты, на которых теперь строят другие, и рассинхронизировало бы каждый клон. Revert оставляет плохой merge в истории и кладёт новый коммит, отменяющий его эффект.

3

Откати merge через -m 1, чтобы назвать магистрального родителя. У merge два родителя, поэтому git revert не может знать, какая сторона — «изменение для отмены», — ты должен сказать ему через -m (mainline). -m 1 значит «оставить первого родителя (main) базой и отменить всё, что принёс второй родитель»:

git revert -m 1 <merge-sha>
git push origin main          # обычный push: revert — новый коммит, без force

Это дописывает один новый коммит, вычитающий изменения фичи. main снова зелёный, полная история сохранена, и ничей клон не инвалидирован. Выбор не того mainline (-m 2) отменил бы собственную работу main — так что сначала подтверди порядок родителей с шага 1.

Граничные случаи

У revert merge есть острое следствие: git теперь записал, что изменения фичи были удалены, поэтому позднее попытка смёржить ту же ветку снова не делает ничего — git считает, что она уже интегрирована. Чтобы снова влить починенную фичу, нужно сначала откатить revert (git revert <revert-sha>) или сделать rebase фичи на новый main, чтобы она пришла как свежие коммиты. Это классический «revert of a revert», и он подставляет команды, которые его не ждут.

4

Если force-push стёр коммиты, восстанови их через reflog — страховку для общих катастроф. Допустим, кто-то «починил» инцидент, сделав force-push и стерев хорошие коммиты с main. Эти коммиты не пропали; они без ссылок, и reflog всё ещё называет их на любой машине, где они были:

git reflog                    # локально: каждая позиция, что держал HEAD, с sha
git reflog show origin/main   # на что remote-tracking ref указывал во времени
git branch rescue <good-sha>  # заякорить потерянные коммиты на настоящую ветку

Reflog локален для каждого клона, поэтому восстановление часто происходит на клоне коллеги (или в reflog самого сервера / git fsck --lost-found), который всё ещё держит верхушку до force-push. Найди там последний хороший SHA, заведи ветку, запушь — и потерянная работа вернулась. Reflog — это причина, по которой «я сделал force-push поверх хорошей работы» восстановимо, а не фатально.

5

Сообщи команде, затем влей починку чисто. Техническое восстановление — половина дела. Напиши в канал инцидента, что случилось, что ты откатил, и что main зелёный. Затем верни фичу правильно: ветка от свежего main, revert-the-revert или rebase исходной работы на него, почини настоящий дефект, прогони CI до зелёного и влей через обычный отревьюенный PR. Инцидент кончается не когда main снова собирается, а когда фича влита корректно и команда знает последовательность.

Разбор примера

16:40, main красный — полная реакция.

Ты делаешь fetch и запускаешь git log --graph --oneline origin/main. Merge-коммит 9f3a1c стоит наверху: «Merge branch ‘feat/new-billing’». git show 9f3a1c подтверждает, что он подтянул недоделанный код биллинга и печатает Merge: 4c2d10 8e77ab — родитель 1 (4c2d10) — это старый main, родитель 2 — фича.

Общая ли она? Да — она на origin/main, и трое уже стянули. Значит, никакого reset. Ты запускаешь git revert -m 1 9f3a1c, который создаёт a07bb2 «Revert ‘Merge branch feat/new-billing’», и git push origin main. CI зеленеет за восемь минут. Ты пишешь в #incidents: «Откатил преждевременный merge биллинга (9f3a1c) через a07bb2; main зелёный; влью заново, когда биллинг будет готов».

Через два дня биллинг готов. Поскольку merge был откачен, простой повторный merge ветки будет no-op, так что ты делаешь git revert a07bb2 на свежей ветке, чтобы восстановить код, чинишь настоящий баг сверху и открываешь обычный PR. Чистое повторное влитие, без переписанной общей истории, полный аудиторский след того, что случилось и почему.

Частая ошибка

Фатальная ошибка — потянуться за git reset --hard плюс git push --force на main в панике. Это выглядит как уборка беспорядка — плохой merge исчезает, — но это переписывает общую историю: клон каждого коллеги теперь расходится с origin/main, их следующий pull создаёт фантомные merge или «потерянные» коммиты, а любой, кто ответвился от откаченного диапазона, оказывается брошен. На общих ветках правило абсолютно: отменяй дописывая revert, никогда не переписывая. Reset — только для локальной истории.

Проверь себя
Викторина

Сломанную фичеветку смёржили в origin/main, и CI красный. Коллеги уже её стянули. Как корректно это отменить?

Итог

Плохой merge на main — это инцидент, который ведут по правилу, а не в панике. Диагностируй merge через git log --graph и git show, чтобы подтвердить двух его родителей. Решай по одному вопросу — общая ли история? Если она запушена, нужно дописать, а не переписать. Откати merge через -m 1, чтобы назвать магистрального родителя, затем пушь обычно; плохой merge остаётся в истории, а новый коммит отменяет его эффект. Если force-push уже стёр хорошие коммиты, восстанови их через git reflog на клоне или сервере, всё ещё держащем верхушку. Reset — для локальной истории; на общих ветках ты никогда не переписываешь. Дальше ты сожмёшь весь трек в ментальные модели, которые проверяет сеньорское собеседование.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.