Как отменить commit и не облажаться

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


Возьмем простую ситуацию: разработчик решает реализовать математические функции. Но на половине пути понимает, что данную задачу было бы хорошо декомпозировать, допустим, на две подзадачи:


  • Реализовать арифметические операции (сложение, вычитание, деление и т.д.)
  • Реализовать числовые операции (максимальное значение, минимальное значение, модуль числа и т.д.)

Проверять будет проще да и тестировать. Но он уже начал ее реализовывать, коммиты уже созданы, и что же делать? Не переписывать же!



Рассмотрим дерево коммитов. Видим, что наш разработчик создал ветку functions, класс Arithmetic, отвечающий за реализацию арифметических операций (коммит А), и класс Numerical, отвечающий за реализацию числовых операций (коммит N). Итого два класса и два коммита.



git revert


Решено, дабы ничего не переписывать, наследоваться от functions и создать две ветки numerical и arithmetic. И соответственно отменить ненужные коммиты. То есть выполнить git revert N в ветке arithmetic и git revert A в ветке numerical. Гениально и просто!



Работа кипит и осталось дело за малым — смерджить мастер с данными ветками.



И что же мы получили? Ни класса Arithmetic, ни класса Numerical!
А все дело в том, что команда git revert создает новый коммит с отменой изменений и не удаляет из истории коммиты. И в нашем случае после слияния веток получается 4 коммита:


A ⟶ N ⟶ revert A ⟶ revert N

То есть вариант с отменой изменений с помощью команды revert вышел нам боком.


git reset


И тут мы вспоминаем, что есть такая команда как reset, вот она в отличии от revert точно удаляет коммиты из истории. Но есть одно НО… она сбрасывает все коммиты до указанного. Такое поведение нам не подходит, так как мы хотим выбрать какие коммиты удалить.


git rebase


Есть еще одно решение — использовать команду git rebase для отмены изменений.
Вернемся к моменту создания двух веток numerical и arithmetic и выполним


git rebase -i –root

Теперь на уровне каждого коммита, который мы хотим отменить заменим pick на drop. И тогда выбранные нами коммиты сбросятся из истории. Например в ветке numerical:



Тогда в истории у нас останутся только нужные нам коммиты.
Теперь при слиянии веток в master получим оба класса.



Данный метод рабочий, только при условии работы в частной ветке, но если эти манипуляции провести в общей ветке, то при публикации (git push) git сообщает, что ветка устарела, так как в ней отсутствуют коммиты и отменяет публикацию.


Чтобы не бороться с git, старайтесь декомпозировать задачи заранее, а то можете словить сюрприз. Сталкивались ли вы с такими ситуациям, и если да, то как выходили из них?

Комментарии 22

    +3

    Ребейз на root? Да вы, батенька, извращенец.


    Пожалуйста, не пытайтесь это повторить в реальном репозитории. Используйте конкретную базу для ребейза.

      0

      В моем случае, когда количество коммитов можно посчитать на пальцах одной руки, то почему бы нет? Но в реальной жизни, возможно, да, не лучший вариант с root.

        +1

        Количество коммитов это полбеды (но да, в репозитории подобному Linux это будет больно). В вашем варианте будут убраны из истории ветки мерж-коммиты (которых в вашем примере не было, а они будут в реальности), коммиты продублируются. Это в лучшем случае. В худшем вы потеряете изменения, которые были внесены в мерж-коммитах при разруливании конфликтов и сами словите их.
        image
        Можно попробовать использовать rebase-merges, однако он гораздо сложнее. В ежедневной практике никто не будет его использовать, это уже исключительные случаи.


        Я люблю ребейз и постоянно его использую. Я всегда призываю своих коллег использовать ребейз фичеветок на последний мастер, у нас даже в гитлабе стоит запрет на мерж веток, которые не могут быть смержены через ff.
        Однако ваша статья из разряда вредных советов. Если новичок будет выполнять ваши примеры, то в лучшем случае плюнет на все и склонирует репозиторий заново (ведь вы не рассказали как откатить изменения через reflog).
        Вам бы стоило рассказать про rebase на конкретный коммит, onto rebase и пожалуй про fork-point (раз уж вы косвенно упомнули force push).

      +4

      git rebase -i HEAD~2


      И вот они, ваши два коммита. Делайте что хотите. Хоть pick, хоть drop.


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

        0
        После reset теряются атрибуты коммита (сообщение, дата, автор), и их потребуется воссоздавать вручную.
          0

          Если коммитишь своё, то дата не нужна, автор подставлен автоматически, а сообщение можно вытащить, зная id коммита (а его можно по reflogʼу найти). Копировать не всегда удобно, да (в консоли это значит после вставки удалить 4 колонки), но тоже поправимо (vim умеет из коробки).
          Я делал, и в небольших объёмах не страшно. Но лучше, да, использовать другие трюки — например, через git restore накатить состояние рабочей копии по другому коммиту и затем его частично принять.

        +1
        Предложение «декомпозировать задачи заранее», не очень практичное, на мой взгляд. Что плохого в том, чтобы наследовать 2 новые ветки из коммита А: А1 и N1?
        В А1 продолжать разработку Arithmetic, а в N1 залить коммит N и продолжать разработку
        Numerical.
          0

          Тогда в N1 будет Arithmetic...

            0
            Вот moiseir и спрашивает: что в этом плохого?
              +1

              Так суть была в том, чтобы полностью изолировать эти задачи. А так получаем не относящийся к задаче код, это ухудшает понимание у проверяющего. У него на руках будет два mr, в одном из которых половина от другого (и скорей всего уже измененная). И как ему быть? Что из этого он должен считать верным?

          +1
          то при публикации (git push) git сообщает, что ветка устарела, так как в ней отсутствуют коммиты и отменяет публикацию.

          Что-то странное. Оно должно сказать "не fast forward, пушьте насильно или не пушьте".
          Но если ветка ушла на публику, да, менять уже поздно. Но обычно и незачем.

            0

            Любители rebase based флоу с вами не согласятся

              0

              В каком это смысле и кто эти любители, с которыми должен спорить? Я сам предпочитаю ребейз мержу почти во всех случаях, но публичную ветку менять это особый случай и нужна особая мотивация.
              Линуксовая манера регулярно ребейзить ветки, из которых берутся коммиты для основных веток (хоть они и публичные) — такой особый случай.

                +3

                В некоторых компаниях/проектах/командах принято ветки на код-ревью и тесты отдавать в виде готовом к fast-forward с основными ветками. И причёсывать историю после фиксов замечаний-багов. И перед вливанием в основную ещё раз ребейзить. Естественно код-ревью или тесты — это уже не личная ветка.

                  0
                  Естественно код-ревью или тесты — это уже не личная ветка.

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


                  В некоторых компаниях/проектах/командах принято ветки на код-ревью и тесты отдавать в виде готовом к fast-forward с основными ветками.

                  Ну у нас в некоторых репах это выставлено в правилах Gerritʼа. И это ничем не мешает по результатам замечаний выкатить K+1-ю ревизию.

                    0

                    Личные ветки для меня личные в том смысле, что не предполагается, что кто-то их будет пуллить, а если будет, то на свой страх и риск. А публичные ровно наоборот, предназначены для того чтобы кто-то их пуллил. И у этого кого-то предполагается минимум квалификации git checkout $branch && git pull не должні вызывать никаких проблем в ежедневном использовании. rebase же вызывает

                      0
                      А публичные ровно наоборот, предназначены для того чтобы кто-то их пуллил.

                      Тогда это 1:1 с тем, что я говорил.


                      rebase же вызывает

                      Ну по крайней мере свои ветки ребейзить — у меня в отделе проблем нет, народ учится очень быстро.

                        +1
                        Тогда это 1:1 с тем, что я говорил.

                        Не совсем, по-моему. Например, для меня моя ветка становится публичной в момент, когда я создаю пулл-реквест без WIP или убираю WIP. Код-ревью, тестирование и т. п. — это уже публичные стадии работы над задачей.


                        Ну по крайней мере свои ветки ребейзить

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

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

                          Да, норма.


                          Потом говорит мне, что пофиксил, я опять делаю гит пулл и у меня конфликты,

                          А зачем вы делаете pull в этом случае поверх существующего (прошлого) состояния? Единственный нормальный подход при этом — fetch нового состояния без участия своей локальной истории.


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


                          Не совсем, по-моему. Например, для меня моя ветка становится публичной в момент, когда я создаю пулл-реквест без WIP или убираю WIP. Код-ревью, тестирование и т. п. — это уже публичные стадии работы над задачей.

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

                            0
                            А зачем вы делаете pull в этом случае поверх существующего (прошлого) состояния?

                            Как говорит git book "an easier or more comfortable workflow". А любители править историю без уведомления всех уже спулливших — ломают этот простой и удобный воркфлоу. И хорошо если поломка пройдёт явно, в виде ошибки, а не что-то там смержится под капотом, а потом запушится.

                              0
                              Как говорит git book "an easier or more comfortable workflow".

                              Ну тогда таки книгу лечить — что для такого случая даёт кривой рецепт.


                              Я на такое не попадался — наверно, потому, что там, где пошли подобные сложные действия, воткнули Gerrit. А у него явная подсказка в виде команды "git fetch <длинный путь> && git checkout FETCH_HEAD".

            +4
            Иногда бывает проще, чем делать rebase -i в «неправильной» ветке, создать новые «правильные» ветки, и растащить туда коммиты посредством cherry-pick.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое