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

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

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

UPD: оказалось что практически тоже самое можно сделать одной командой https://git-scm.com/docs/git-merge#Documentation/git-merge.txt-ours-1, находясь на newMasterсделать:

git merge -s ours master 

И будет прямо тоже самое, кроме того, что master станет вторым парентом а не первым, что, если подумать, даже более правильно, так как отражает что основной веткой разработки в этот момент был newMaster. А в документации даже мотивация применения команды точно такая же как и у меня.

Спасибо @fshp за коммент.

Конец upd.

Задача: использовать состояние целиком из 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

Дополнительное чтиво

Комментарии (25)


  1. fshp
    13.11.2023 15:57
    +14

    Поздравляю, вы изобрели мерж стратегию ours

    Только вместо одной команды ввели десять.


  1. datacompboy
    13.11.2023 15:57
    +1

    А для мимокрокодилов... в чем отличие от ребейза?


    1. Aquahawk Автор
      13.11.2023 15:57
      +1

      Это другой путь. Да, можно отребейзить, но у вас возникнет много конфликтов, которые надо будет решать, и да, это можно сделать дополнительными скриптами. Здесь задача была быстро и просто получить состояние как у ветки и быть уверенным что я точно ничего не проморгал, и явное указание дерева гарантирует мне что я получил точно то, что заказал.


  1. bfDeveloper
    13.11.2023 15:57
    +2

    Это своего рода эквивалент того, что все файлы из старого мастера заменили файлами из нового во время мержа. Внутри гита отличия будут, потому что это два разных дерева файлов, но с точки зрения пользователя отличий нет. Даже blame будет одинаковым.

    Но вариант из поста сильно быстрее, если знаешь, что делаешь - не нужен второй чекаут из которого копировать файлики.


    1. Aquahawk Автор
      13.11.2023 15:57
      +1

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


    1. Aquahawk Автор
      13.11.2023 15:57

      git cat-file -p 74092e5acd734a7df459ca4b08f4e88f451096f9
      tree 6fac19212aba1d31d2df5ad6498cdb4b111a2022
      parent 193cf791417f6fb8a48fd8eb123c1bd53ffac10a
      parent 338cb13ada6efdbd9a3610adeab7700fb1ba91d2
      author ***** <*****> 1699871047 +0300
      committer ***** <*****> 1699871047 +0300
      
      manual force commit newMaster to master with fake merge, because old master is obsolete
      git cat-file -p 7476ca3bbeecf04eeb9684b3f4c3960a7cdb3a0b
      tree 6fac19212aba1d31d2df5ad6498cdb4b111a2022
      parent f719d58b010c0b70e8fdefcff2ecbc3e7fddda54
      author ***** <*****> 1699885027 +0300
      committer ***** <*****> 1699885027 +0300
      
      test commit

      Воспроизвёл, да одно и то же дерево


    1. Mingun
      13.11.2023 15:57
      +1

      Вот тот самый комментарий, который лучше статьи. Ведь git нам нужен не как хранилище файлов, а для просмотра истории. И вот без объяснения, как эти ручные перлы будут восприниматься не такими подкованными разработчиками как автор, очень важно. А то потом он уволится, а кому-то придется разбираться, что же тут произошло...


  1. fshp
    13.11.2023 15:57
    +14

    Поздравляю, вы изобрели мерж стратегию ours

    Только вместо одной команды ввели десять.


    1. Aquahawk Автор
      13.11.2023 15:57
      -4

      https://git-scm.com/docs/git-merge#Documentation/git-merge.txt-ours

      ours
      This option forces conflicting hunks to be auto-resolved cleanly by favoring our version. Changes from the other tree that do not conflict with our side are reflected in the merge result. For a binary file, the entire contents are taken from our side.

      удачи потом разгрести


      1. fshp
        13.11.2023 15:57
        +2

        Это опция стратегии ort

        Стратегия ours ниже


        1. Aquahawk Автор
          13.11.2023 15:57
          +2

          Признаю, да, она делает ровно то, что я изобразил

          ours

          This resolves any number of heads, but the resulting tree of the merge is always that of the current branch head, effectively ignoring all changes from all other branches. It is meant to be used to supersede old development history of side branches. Note that this is different from the -Xours option to the recursive merge strategy.

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

          Единственное отличие в том что это мердж в себя, и сделать так как я написал, когда это мердж в master, не получится, это будет другой порядок парентов, но, да, это будет _ровно то же самое_ что я тут написал в кучу кода


          1. bfDeveloper
            13.11.2023 15:57

            Чтобы был нужный порядок парентов можно взять theirs

            UPD. Попутал, нет theirs как стратегии.

            note that, unlike ours, there is no theirs merge strategy to confuse this merge option with.


            1. fshp
              13.11.2023 15:57
              +1

              Можно было бы, существуй такая стратегия.

              А пока есть только опция у ort, которая только конфликты автоматически разрешает в нужном направлении.


          1. fshp
            13.11.2023 15:57
            +1

            пойду расковыряю когда она появилась.

            Почти 20 лет назад.


        1. Aquahawk Автор
          13.11.2023 15:57

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


        1. Aquahawk Автор
          13.11.2023 15:57

          Ещё раз спасибо, в пост внесён апдейт.


  1. slonopotamus
    13.11.2023 15:57

    нужно просто забыть старый мастер

    Но вы ничего не забыли, оно по-прежнему живёт в истории.


    1. Aquahawk Автор
      13.11.2023 15:57

      да, живёт, и пусть там лежит. Удалить запушенное без форса нельзя, да и зачем? Эффектов оно из истории никаких не создаёт.


      1. slonopotamus
        13.11.2023 15:57
        +1

        Откуда я знаю зачем, это вы сформулировали так задачу.


  1. sergiodev
    13.11.2023 15:57
    +2

    Прикольно, не знал что так можно. Я однажды делал что-то похожее но немного по-тупому - через git merge с конфликтами и потом git rm -rf . && git checkout . -- newMaster && git commit


    1. Aquahawk Автор
      13.11.2023 15:57

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


  1. AlexGluck
    13.11.2023 15:57
    +2

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


    1. Aquahawk Автор
      13.11.2023 15:57
      +1

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


      1. sergey-kuznetsov
        13.11.2023 15:57

        Почему вы считаете, что push --force, упомянутый в заголовке, решает эту задачу? Коллеги не увидят вашего переименования и вам придется попросить их поудалять свои мастеры и распаковать заново вашу новую версию. Причем так потеряется история старого мастера.


        1. Aquahawk Автор
          13.11.2023 15:57

          Потому что именно в такой формулировке я обычно слышу просьбу от коллег которые хотят заменить мастер. И в посте я упоминаю про проблемы коллегам в этом случае.


          1. sergey-kuznetsov
            13.11.2023 15:57

            Их мастер вы заменить не в стостоянии. Он же на их компьютерах.

            Наверное проблема именно в неправильных формулировках. В том числе в том, как сформулирован заголовок вашей статьи. Я несколько раз его перечитал и не понял о чем речь. Лишь далее по тексту немного прояснилась хотелка.

            Наверное надо было уточнить, что хочется не ветку заменить, а состояние проекта в этой ветке. Сама ветка остается там же и коллеги легко продолжат в ней работать обновившись через простой fetch/pull