С ПОЛЕЙ РАЗРАБОТКИ: REVERT MERGE COMMIT
Я работаю с git только через консоль.
Мне не нравятся все GUI, которые имеются на рынке. Т.к. часто они плохо решают узкие, сложные кейсы, которые порой возникают.
Текущий пост прекрасный тому пример, ибо через git GUI встроенный в райдер я не смог решить проблему ниже.
Ситуация:
Есть баг, которые вы правите перед релизом. У вас в голове два решения:
🔸 По уму, но дольше и большим объемом регресса.
🔹 Поправить точечно, грязно, но быстро, внедрившись в другую систему.
Конечно как и подобает доблесть, выбираем первый вариант. Делаем фикс, примерно в 3-6 коммитов, проверяем в редакторе, отправляем в QA.
И вроде опыта достаточно, много всего знаешь и проверил все перед заливкой, но:
🔹 На мобилках фикс не работает.
Там другое графическое API, отличное от редактора и там твой баг вообще не исправлен, а наоборот приводит к hard lock'ам из-за неправильной сортировки UI элементов в очереди на отрисовку.
И вот вы с лидом (или как лид) думаете как исправить это.
Изменения расползлись по другим веткам, а с момента вашего merge commit'a уже прошло пару дней.
Надумали два путя:
🔹 Удалить фикс всех веток.
Можно сделать как через rebase всех изменений после ваших изменений, а потом force push'ем обновить все ветки.
Можно ручками сделать cherry-pick, а потом force push'ем обновить все ветки.
🔹Сделать revert merge коммита.
Проблема с первым:
🔸Придется блокировать работу во всех ветках на время отката.
🔸Во время rebase могут возникнуть конфликты из-за которых можно накуролесить на --выходные и ++пара бессонных ночей.
Со вторым проблема в том, что ХЗ как сделать revert целого merge'а.
И ответ совсем не очевидный:
git revert -m 2 или 1 <merge_commit_hash>
С revert понятно, а что за магия с 2 или 1?
Приготовьтесь, инфу про git оч сложно запаковывать компактно 😅
git — это направленный ацикличный граф, ноды которого всегда хранят в себе hash родителя(-ей).
Т.е. каждый git commit -m "the best changes"
создает новую ноду в которую записывает hash parent коммита (hash коммита предшествующий текущему) и hash object-tree.
Для простоты понимания git — это два независимых LinkedList'а один - для изменений (object-tree) второй - для коммитов (commit-tree).
Только LinkedList'ы однонаправленные (нету Next) и могут иметь несколько нод (Node<T>[] Previous
).
Это значит:
🔸 Все коммиты в ветке однонаправленно связаны с самым первым Initial commit
.
Т.е. из любого коммита в ветке можно дойти до самого начала графа.
🔸 Каждый коммит, без указания parent'а, как нода из LinkedList'а, Previous
которой равен null
.
Угадайте как их можно удалить? 🙃
🔸 Эта структура отлично подходит для добавления/изменения/удаления.
С поиском сложнее, но благодаря сортировке префиксов по алфавиту (см. папку objects), используется бинарный поиск.
С устройством разобрались. Дальше порядок работы merge:
1️⃣ Находим общий коммит.
Просто идем по графу, пока parent'ы не совпадут.
2️⃣ Ищем diff от общего коммита до последних коммитов в двух ветках.
3️⃣ Полученный diff пишем в merge-commit.
4️⃣ В parent у merge-commit'a прописываем последниE коммитЫ из двух веток.
Т.е. merge-commit, в отличии от обычного имеет 2 parent'а.
Это фактически значит:
🔻git merge не меняет родителей у существующих коммитов (в отличие от rebase), а создает новый, с ссылками на 2 последних коммита в ветках.
А revert-commit может иметь лишь один parent.
Значит, чтобы сделать все правильно, нужно указать изменения какого именного из parent'ов мы хотим revert'нуть.
Или по другому: diff какой из ветки мы должны revert'нуть
За это как раз и отвечает магическое 1 или 2 — номер parent'а, изменения которого мы хотим revert'нуть.
Лично у меня случилось 2 озарения:
🔶 Именно merge-commit несет в себе все изменения.
Revert'нули его == revert'нули изменения всех коммитов.
🔶 Все, rebase, cherry-pick есть маленькая merge операция. А не наоборот.
До этого я думал что merge - аналог rebase 🤯
Но помните, вы этим самым, вы делаете revert изменений, не истории.
Ставь 👍 если такая движуха тебе по кайфу!
Источники:
Ответ с SO
Статья раз
Статья два
@UniArchitect #будни