Как в git заменить master на другую ветку без использования push --force (перенос стейта одной ветки на другую)

6fa984050bdef30daa61dc13d9d2506d

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

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

Итак: есть 2 ветки, запоротый master и новый newMaster.

Задача: использовать состояние целиком из newMaster, но продолжить разработку из master. Т.е. как бы мердж, но не мердж. Можно бы сделать через перевешивания мастера на новую ветку и push --force, но это запрещено на нашем гите и создаст проблемы коллегам. Поэтому сделаем так, будто разработка в ветке была замержена в мастер.

Сначала надо всё запушить и запулить (в обоих ветках), если что-то сломается, гораздо приятнее жить зная что ничего точно не потеряно.
Все подобные операции я провожу собирая и подписывая все хешики команд в блокнотик рядом.
Если вы не тимлид, спросите тимлида, перед тем как это делать. Совсем намертво сломать ничего не сломается, коммиты в master после отделения newMaster, останутся только в истории, но не потеряются совсем.

Самое главное знание

git оперирует не изменениями, а состояниями файлов, поэтому мы можем сконструировать коммит с нужным нам состоянием:

Для начала установим хеши коммитов на которые смотрят наши бранчи:

git rev-parse master
193cf791417f6fb8a48fd8eb123c1bd53ffac10a
git rev-parse newMaster
338cb13ada6efdbd9a3610adeab7700fb1ba91d2

перейдём в ветку newMaster

git checkout newMaster
Switched to branch 'newMaster'
Your branch is up to date with 'origin/newMaster'.

Далее распечатаем содержимое того коммита, состояние которого мы хотим использовать (который в newMaster):

git cat-file -p 338cb13ada6efdbd9a3610adeab7700fb1ba91d2

tree 6fac19212aba1d31d2df5ad6498cdb4b111a2022
parent f719d58b010c0b70e8fdefcff2ecbc3e7fddda54
author ***** <*****> 1699870138 +0300
committer ***** <*****> 1699870138 +0300

Bindable event removed

tree как раз является идентификатором дерева, которое описывает все состояния всех файлов, которые мы получим счекаутив коммит на который мы смотрим.

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

  1. Содержать нужное нам состояние файлов (дерево)

  2. Быть мердж коммитом: т.е. иметь два предка, коммит из поломанного мастера и коммит из нового мастера, порядок указания предков определяет как графические утилиты будут показывать направление мерджа, будет выглядеть как мердж второго аргумента в первый.

  3. обладать нужным нам мессаджем

git commit-tree 6fac19212aba1d31d2df5ad6498cdb4b111a2022 -p 193cf791417f6fb8a48fd8eb123c1bd53ffac10a -p 338cb13ada6efdbd9a3610adeab7700fb1ba91d2 -m "manual force commit newMaster to master with fake merge, because old master is obsolete"
74092e5acd734a7df459ca4b08f4e88f451096f9

git commit-tree создал для нас коммит и вернул его хеш: 74092e5acd734a7df459ca4b08f4e88f451096f9 который не находится ни в каком бранче. Если на этом этапе просто забить на всё и забыть, то в результате push этот коммит никуда не будет отправлен, т.к. на него нет ссылок с действующих бранчей, и через некоторое время git gc его сожрёт. Теперь надо сказать что наш старый мастер смотрит на этот коммит.

git branch -f master 74092e5acd734a7df459ca4b08f4e88f451096f9

Как мы помним, выше мы переключались на newMaster, именно для того чтобы branch -f master сработал, ибо запрещено менять ветку на который сейчас находишься.

Готово, теперь master содержит абсолютно легитимный мерж коммит, который с точки зрения гита продолжает историю мастера и является мерж коммитом из newMaster. Теперь можно переключиться на мастер и запушить.

git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 26 commits.
  (use "git push" to publish your local commits)

И здесь мы видим что в master появились 26 коммитов которые нужно запушить, ровно так выглядит мердж ветки.
Пробуем запушить:

git push
Locking support detected on remote "origin". Consider enabling it with:
  $ git config lfs.https://*****.git/info/lfs.locksverify true
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 255 bytes | 255.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To *****.git
   193cf79..74092e5  master -> master

Здесь мы видим что по факту ушёл всего один объект, тот самый наш коммит, который содержит ссылку на уже существующее дерево.
Поскольку этот коммит имеет в предках последний коммит из master всё проходит нормально

Ссылки на документацию по применённым командам:
https://git-scm.com/docs/git-rev-parse
https://git-scm.com/docs/git-cat-file
https://git-scm.com/docs/git-commit-tree
https://git-scm.com/docs/git-branch
https://git-scm.com/docs/git-gc

Дополнительно чтиво https://stackoverflow.com/questions/4911794/git-command-for-making-one-branch-like-another/4912267

© Habrahabr.ru